Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Changelog

## Unreleased
## 0.1.3 - 2026-10-09

- Optional `require_all_targets` adds opt-in coverage of declared CSV
targets, detecting expected records with no delivery/evidence rows. Reports
contain only target locations; existing capacity and finding limits remain.
This release candidate is not yet published to npm.
- Source artifact contracts support bounded CSV `reference_exists` checks with
exact string IDs and declared unique target keys. An offline citation example
rejects dangling references without exposing values; the aggregate index is
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Verify Agent Skills before you trust them.** SkillSync checks a local Skill's provenance, compatibility, and changes without executing it.

> **Alpha · v0.1.2 · Node.js 20+**
> **Alpha · v0.1.3 · Node.js 20+**
> SkillSync performs offline checks of local Skill content. It does not execute Skill scripts, does not read credentials, and does not enable live provider, remote-worker, or runtime capabilities.

[![Terminal demo](https://raw.githubusercontent.com/Chumaniac/skillsync/main/docs/assets/verify-demo.svg)](https://github.com/Chumaniac/skillsync/blob/main/docs/assets/verify-demo.svg)
Expand All @@ -18,7 +18,7 @@ npm install -g @chumanic/skillsync@0.1.0
skillsync verify --path . --target codex
```

**From source (latest 0.1.2):**
**From source (latest 0.1.3):**

```bash
git clone https://github.com/Chumaniac/skillsync.git
Expand All @@ -28,7 +28,7 @@ npm run build
node dist/cli/index.js verify --path fixtures/product/trust-loop/review --target codex
```

> `0.1.2` is tagged as `v0.1.2` and will be published via the OIDC provenance workflow (`npm publish --provenance --access public` with `id-token: write`, no long-lived token). Until the Trusted Publisher is verified on npm, use `0.1.0` via `npx` or run `0.1.2` from source. The `skillsync ci init` template pins `0.1.2` by default; override with `--package-version 0.1.0` on npm today.
> `0.1.3` is a release candidate prepared for the OIDC provenance workflow (`npm publish --provenance --access public` with `id-token: write`, no long-lived token). Until the Trusted Publisher is verified on npm, use `0.1.0` via `npx` or run `0.1.3` from source. The `skillsync ci init` template pins `0.1.3` by default; override with `--package-version 0.1.0` on npm today.

The command above verifies the included sample Skill. Replace the fixture path with a directory containing your own `SKILL.md` when you are ready.

Expand All @@ -39,6 +39,8 @@ The command above verifies the included sample Skill. Replace the fixture path w
See [actual artifact checks](docs/artifact-delivery.md); it runs no Skill code.
The [knowledge source inventory](fixtures/product/source-index/README.md) adds
exact declared HTTPS hosts to physical CSV/JSON checks without fetching sources.
Optional target coverage catches expected records with no delivery/evidence rows;
see the [delivery coverage example](fixtures/product/delivery-coverage/README.md).
CSV `reference_exists` checks also bind exact string IDs to a declared unique
CSV target, with a 100,000-cell reference limit and no raw IDs in findings.
The [reference-integrity example](fixtures/product/reference-integrity/README.md)
Expand Down
11 changes: 11 additions & 0 deletions docs/artifact-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ operations data. Only declared reference fields are retained as SHA-256
fingerprints with source row numbers. At most 100,000 referenced cells are
indexed per invocation, counting repeated IDs and deduplicating field declarations.
Exceeding this ceiling releases indexes and fails with `artifact.reference-capacity`.
An optional `require_all_targets: true` also rejects declared target rows that
have no reference in that check's source field. It reports
`artifact.reference-unused` at the target file/field/data-row without its ID.
Omitting the option keeps coverage disabled and preserves existing normalized
contract digests; ordinary reference catalogs may contain unused targets.
The [local delivery coverage candidate](../fixtures/product/delivery-coverage/README.md)
demonstrates expected-order omissions; this release candidate is not yet published to npm.
Target rows and optional source fingerprint sets share the existing 100,000-cell
intake limit and are released together on exhaustion. Findings remain capped at 256.
Coverage means reference presence, not completed work or authenticated approval.

Missing references report relative file, field and data-row number, never the ID
or fingerprint. This checks internal consistency without authenticating records.

Expand Down
2 changes: 1 addition & 1 deletion docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ they can be replaced.

The generated GitHub Action grants `contents: read` and uploads SARIF findings;
it does not execute Skill scripts. The generated consumer command pins the
published SkillSync package version (`@chumanic/skillsync@0.1.2` by default); override it
published SkillSync package version (`@chumanic/skillsync@0.1.3` by default); override it
with `ci init --package-version <version>` when upgrading. Because the current
repository publishes a scoped public package, the generated consumer template can
be used after that package version is available; the repository's own workflow uses
Expand Down
2 changes: 1 addition & 1 deletion docs/domain-adaptation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ change. A `report` with both plan and receipt requires matching plan digests.
These findings are review material, not automatic permission to run a Skill.

The examples are current source additions, not part of the published npm0.1.0
package. Keep npm installation and source0.1.2 capabilities separate. Capability
package. Keep npm installation and source0.1.3 capabilities separate. Capability
profiles are maintained by this project using Agent documentation; a profile is
not vendor certification, and unknown/runtime-dependent features stay explicit.

Expand Down
31 changes: 31 additions & 0 deletions fixtures/product/delivery-coverage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Expected delivery coverage — local candidate

This source-only candidate detects a different omission from dangling references:
all delivered rows can point to valid orders while an expected order has no rows.
The synthetic expected-order table is an explicit checklist, not an authenticated
order system. `require_all_targets: true` requires every declared unique target ID
to appear in the selected source field at least once. The default remains false.

Build the candidate source, then run:

```bash
node dist/cli/index.js artifacts --contract fixtures/product/delivery-coverage/contract.json --path fixtures/product/delivery-coverage/artifacts --format json
```

The complete example passes. In a temporary copy, remove the `DEMO-ORDER-B` item
line while retaining its expected-order row: the report fails with
`artifact.reference-unused` at target data-row 2, without showing the ID. Adding
a line for an undeclared order still fails the existing `reference-missing` check.

Adapt the two tables to declared findings and evidence for code review, required
references and citations for knowledge material, or expected records and actual
delivery rows for operations. Coverage establishes local reference presence only:
it does not establish review quality, source truth, approval, payment or shipment.

Only nonempty exact CSV strings and single-column unique targets are supported.
All relation checks remain within the shared 100,000-cell index and 256-finding
ceilings; the indexes store fingerprints and row numbers rather than ID contents.
No runtime, network, account or real business integration is executed.

The new coverage option is in the 0.1.3 source release candidate; npm publication
is separately verified after its release workflow succeeds.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
order_id
DEMO-ORDER-A
DEMO-ORDER-B
4 changes: 4 additions & 0 deletions fixtures/product/delivery-coverage/artifacts/order-lines.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
order_id,item_id
DEMO-ORDER-A,DEMO-ITEM-1
DEMO-ORDER-A,DEMO-ITEM-2
DEMO-ORDER-B,DEMO-ITEM-3
13 changes: 13 additions & 0 deletions fixtures/product/delivery-coverage/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"schema": "skillsync.artifacts/v1",
"limits": { "max_files": 2, "max_file_bytes": 4096, "max_total_bytes": 8192 },
"files": [
{ "path": "order-lines.csv", "format": "csv", "max_rows": 20,
"columns": [{ "name": "order_id", "type": "string" }, { "name": "item_id", "type": "string" }] },
{ "path": "expected-orders.csv", "format": "csv", "max_rows": 20, "unique_by": ["order_id"],
"columns": [{ "name": "order_id", "type": "string" }] }
],
"checks": [{ "type": "reference_exists", "require_all_targets": true,
"source": { "path": "order-lines.csv", "field": "order_id" },
"target": { "path": "expected-orders.csv", "field": "order_id" } }]
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chumanic/skillsync",
"version": "0.1.2",
"version": "0.1.3",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type CiInitResult = {
};

const DEFAULT_PATHS = [".agents/skills", ".claude/skills", ".cursor/skills"];
const DEFAULT_PACKAGE_VERSION = "0.1.2";
const DEFAULT_PACKAGE_VERSION = "0.1.3";
const PUBLISHED_PACKAGE_NAME = "@chumanic/skillsync";

function validateNodeVersion(value: string): string {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export async function runReport(options: ReportOptions): Promise<EvidenceReport>
...(resolvedIssueBaseline(beforeReport, before.rootDigest) === undefined
? {}
: { baseline: resolvedIssueBaseline(beforeReport, before.rootDigest) }),
toolVersion: options.toolVersion ?? "0.1.2",
toolVersion: options.toolVersion ?? "0.1.3",
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import { renderSarif } from "../reporters/sarif.js";
import { renderText } from "../reporters/text.js";
import { parseOutputFormat } from "./output.js";

const VERSION = "0.1.2";
const VERSION = "0.1.3";

const COMMANDS = [
["scan", "Inspect skill files without executing their instructions."],
Expand Down Expand Up @@ -642,15 +642,15 @@ export function createCli(io: CliIO = defaultCliIO): Command {
.command("init")
.option("--target <target>", "Template target: github or pre-commit", "github")
.option("--node-version <version>", "Node.js version for GitHub Actions", "20")
.option("--package-version <version>", "Pinned published SkillSync package version", "0.1.2")
.option("--package-version <version>", "Pinned published SkillSync package version", "0.1.3")
.option("--path <paths...>", "Project Skill paths")
.option("--apply", "Write the generated file")
.option("--force", "Allow replacing an existing generated file")
.action(async (options: CiCliOptions) => {
const result = await runCiInit({
target: options.target ?? "github",
nodeVersion: options.nodeVersion ?? "20",
packageVersion: options.packageVersion ?? "0.1.2",
packageVersion: options.packageVersion ?? "0.1.3",
paths: options.path ?? [],
apply: options.apply,
force: options.force,
Expand Down
4 changes: 3 additions & 1 deletion src/domain/artifact-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const comparison = z.object({ type: z.literal("csv_summary"), csv: safePath, jso
sums: z.array(z.object({ column: z.string().min(1).max(128), field: z.string().min(1).max(128) }).strict()).max(128),
}).strict();
const referenceField = z.object({ path: safePath, field: z.string().min(1).max(128) }).strict();
const reference = z.object({ type: z.literal("reference_exists"), source: referenceField, target: referenceField }).strict();
const reference = z.object({ type: z.literal("reference_exists"), source: referenceField, target: referenceField,
require_all_targets: z.boolean().optional(),
}).strict();

export const artifactContractSchema = z.object({ schema: z.literal("skillsync.artifacts/v1"),
limits: z.object({
Expand Down
19 changes: 15 additions & 4 deletions src/domain/artifact-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ArtifactContract } from "./artifact-contract.js";
import type { ArtifactCellObserver, ArtifactFinding } from "./artifact-content.js";

export const MAX_REFERENCE_CELLS = 100_000;
type Index = { targets?: Set<string>; sources?: Array<{ digest: string; row: number }> };
type Index = { targets?: Map<string, number>; sources?: Array<{ digest: string; row: number }>; referenced?: Set<string> };

/** One invocation-local index: declared fields only, hashed IDs, fixed aggregate ceiling. */
export function createArtifactReferenceIndex(contract: ArtifactContract) {
Expand All @@ -18,7 +18,8 @@ export function createArtifactReferenceIndex(contract: ArtifactContract) {
};
for (const check of checks) {
index(check.source.path, check.source.field).sources ??= [];
index(check.target.path, check.target.field).targets ??= new Set();
index(check.target.path, check.target.field).targets ??= new Map();
if (check.require_all_targets) index(check.source.path, check.source.field).referenced ??= new Set();
}
let cells = 0, exhausted = false;
return {
Expand All @@ -33,12 +34,13 @@ export function createArtifactReferenceIndex(contract: ArtifactContract) {
if (cells > MAX_REFERENCE_CELLS) {
exhausted = true;
for (const columns of fields.values()) for (const entry of columns.values()) {
entry.targets?.clear(); if (entry.sources) entry.sources.length = 0;
entry.targets?.clear(); entry.referenced?.clear(); if (entry.sources) entry.sources.length = 0;
}
return;
}
const digest = createHash("sha256").update("skillsync.references/v1\0string\0").update(value).digest("hex");
entry.targets?.add(digest); entry.sources?.push({ digest, row });
if (entry.targets && !entry.targets.has(digest)) entry.targets.set(digest, row);
entry.sources?.push({ digest, row }); entry.referenced?.add(digest);
};
},
findings(): ArtifactFinding[] {
Expand All @@ -52,6 +54,15 @@ export function createArtifactReferenceIndex(contract: ArtifactContract) {
if (findings.length >= 256) return findings;
}
}
if (check.require_all_targets) {
const referenced = index(check.source.path, check.source.field).referenced!;
for (const [digest, row] of targets) {
if (!referenced.has(digest)) {
findings.push({ code: "artifact.reference-unused", path: check.target.path, field: check.target.field, row });
if (findings.length >= 256) return findings;
}
}
}
}
return findings;
},
Expand Down
2 changes: 1 addition & 1 deletion src/reporters/sarif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function renderSarif(report: VerificationReport): string {
tool: {
driver: {
name: "skillsync",
version: "0.1.2",
version: "0.1.3",
rules: [...ruleMap.values()],
},
},
Expand Down
4 changes: 2 additions & 2 deletions templates/github/skillsync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: "20"
# Requires the published @chumanic/skillsync@0.1.2 package.
- run: npx --yes @chumanic/skillsync@0.1.2 verify --format sarif --path .agents/skills > skillsync.sarif
# Requires the published @chumanic/skillsync@0.1.3 package.
- run: npx --yes @chumanic/skillsync@0.1.3 verify --format sarif --path .agents/skills > skillsync.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
Expand Down
2 changes: 1 addition & 1 deletion templates/pre-commit/skillsync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ repos:
hooks:
- id: skillsync-verify
name: Verify Agent Skills with SkillSync
entry: npx --yes @chumanic/skillsync@0.1.2 verify --format json --path .claude/skills --path .agents/skills
entry: npx --yes @chumanic/skillsync@0.1.3 verify --format json --path .claude/skills --path .agents/skills
language: system
pass_filenames: false
4 changes: 2 additions & 2 deletions tests/cli/ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("skillsync ci", () => {
cwd: root,
});
expect(planned.applied).toBe(false);
expect(planned.content).toContain("@chumanic/skillsync@0.1.2 verify --format sarif");
expect(planned.content).toContain("@chumanic/skillsync@0.1.3 verify --format sarif");
await expect(access(join(root, ".github/workflows/skillsync.yml"))).rejects.toThrow();

const applied = await runCiInit({
Expand All @@ -51,7 +51,7 @@ describe("skillsync ci", () => {
apply: true,
});
expect(applied.applied).toBe(true);
expect(await readFile(applied.outputPath, "utf8")).toContain("@chumanic/skillsync@0.1.2 verify --format sarif");
expect(await readFile(applied.outputPath, "utf8")).toContain("@chumanic/skillsync@0.1.3 verify --format sarif");

await expect(
runCiInit({
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("skillsync CLI", () => {
it("reports the exact public version and lists the verification commands", async () => {
const version = await runCli(["--version"]);
expect(version.exitCode).toBe(0);
expect(version.stdout).toBe("0.1.2\n");
expect(version.stdout).toBe("0.1.3\n");

const result = await runCli(["--help"]);

Expand Down
4 changes: 2 additions & 2 deletions tests/cli/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe("skillsync report", () => {

const json = await runCli(["report", "--before", beforePath, "--after", afterPath, "--format", "json"]);
expect(JSON.parse(json.stdout).conclusion).toBe("verified");
expect(JSON.parse(json.stdout).toolVersion).toBe("0.1.2");
expect(JSON.parse(json.stdout).toolVersion).toBe("0.1.3");

const sarif = await runCli(["report", "--before", beforePath, "--after", afterPath, "--format", "sarif"]);
const parsedSarif = JSON.parse(sarif.stdout) as {
Expand All @@ -229,7 +229,7 @@ describe("skillsync report", () => {
}>;
};
expect(parsedSarif.version).toBe("2.1.0");
expect(parsedSarif.runs[0]?.tool.driver.version).toBe("0.1.2");
expect(parsedSarif.runs[0]?.tool.driver.version).toBe("0.1.3");
expect(parsedSarif.runs[0]?.results[0]?.properties.issueId).toMatch(/^iss_/);
});

Expand Down
10 changes: 5 additions & 5 deletions tests/docs/documentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("release documentation", () => {
};

expect(readme).toContain("Verify Agent Skills before you trust them.");
expect(readme).toContain("Alpha · v0.1.2 · Node.js 20+");
expect(readme).toContain("Alpha · v0.1.3 · Node.js 20+");
expect(readme).toContain("## Install");
expect(readme).toContain("npx --yes @chumanic/skillsync@0.1.0 verify");
expect(readme).toContain([
Expand All @@ -59,7 +59,7 @@ describe("release documentation", () => {
expect(terminalDemo).toContain(
"$ node dist/cli/index.js verify --path fixtures/product/trust-loop/review --target codex",
);
expect(terminalDemo).not.toContain("@chumanic/skillsync@0.1.2");
expect(terminalDemo).not.toContain("@chumanic/skillsync@0.1.3");
expect(readme).toContain("does not execute Skill scripts");
expect(readme).toContain("does not read credentials");
expect(readme).not.toContain("## Documentation index");
Expand Down Expand Up @@ -129,9 +129,9 @@ describe("release documentation", () => {
expect(repositoryWorkflow).toContain("SkillSync-Complete-Design.md");
expect(repositoryWorkflow).toContain("Competitive-Research-and-Design-Rationale.md");
expect(repositoryWorkflow).toContain("MVP-Implementation-Plan.md");
expect(githubTemplate).toContain("@chumanic/skillsync@0.1.2");
expect(preCommitTemplate).toContain("@chumanic/skillsync@0.1.2");
expect(ci).toContain("@chumanic/skillsync@0.1.2");
expect(githubTemplate).toContain("@chumanic/skillsync@0.1.3");
expect(preCommitTemplate).toContain("@chumanic/skillsync@0.1.3");
expect(ci).toContain("@chumanic/skillsync@0.1.3");
expect(changelog).toContain("## 0.1.2 - 2026-08-31");
expect(changelog).toContain("publish contract");
expect(changelog).toContain("profile registry");
Expand Down
Loading
Loading