diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c13ef0..b4ce8d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 2e0552b..43289e4 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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. @@ -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) diff --git a/docs/artifact-delivery.md b/docs/artifact-delivery.md index 6f2b215..c2bcfd6 100644 --- a/docs/artifact-delivery.md +++ b/docs/artifact-delivery.md @@ -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. diff --git a/docs/ci.md b/docs/ci.md index 3b64fd3..2e7c0a7 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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 ` 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 diff --git a/docs/domain-adaptation.md b/docs/domain-adaptation.md index 89e955b..5b42a62 100644 --- a/docs/domain-adaptation.md +++ b/docs/domain-adaptation.md @@ -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. diff --git a/fixtures/product/delivery-coverage/README.md b/fixtures/product/delivery-coverage/README.md new file mode 100644 index 0000000..49c72e7 --- /dev/null +++ b/fixtures/product/delivery-coverage/README.md @@ -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. diff --git a/fixtures/product/delivery-coverage/artifacts/expected-orders.csv b/fixtures/product/delivery-coverage/artifacts/expected-orders.csv new file mode 100644 index 0000000..b234a97 --- /dev/null +++ b/fixtures/product/delivery-coverage/artifacts/expected-orders.csv @@ -0,0 +1,3 @@ +order_id +DEMO-ORDER-A +DEMO-ORDER-B diff --git a/fixtures/product/delivery-coverage/artifacts/order-lines.csv b/fixtures/product/delivery-coverage/artifacts/order-lines.csv new file mode 100644 index 0000000..d350f9c --- /dev/null +++ b/fixtures/product/delivery-coverage/artifacts/order-lines.csv @@ -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 diff --git a/fixtures/product/delivery-coverage/contract.json b/fixtures/product/delivery-coverage/contract.json new file mode 100644 index 0000000..2dca1e3 --- /dev/null +++ b/fixtures/product/delivery-coverage/contract.json @@ -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" } }] +} diff --git a/package-lock.json b/package-lock.json index 450c337..08eb7a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@chumanic/skillsync", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@chumanic/skillsync", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "commander": "^13.1.0", diff --git a/package.json b/package.json index c856a90..4e87ea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chumanic/skillsync", - "version": "0.1.2", + "version": "0.1.3", "private": false, "publishConfig": { "access": "public" diff --git a/src/cli/commands/ci.ts b/src/cli/commands/ci.ts index dd376aa..fa9adc4 100644 --- a/src/cli/commands/ci.ts +++ b/src/cli/commands/ci.ts @@ -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 { diff --git a/src/cli/commands/report.ts b/src/cli/commands/report.ts index 69320d1..e0fb80d 100644 --- a/src/cli/commands/report.ts +++ b/src/cli/commands/report.ts @@ -278,7 +278,7 @@ export async function runReport(options: ReportOptions): Promise ...(resolvedIssueBaseline(beforeReport, before.rootDigest) === undefined ? {} : { baseline: resolvedIssueBaseline(beforeReport, before.rootDigest) }), - toolVersion: options.toolVersion ?? "0.1.2", + toolVersion: options.toolVersion ?? "0.1.3", }); } diff --git a/src/cli/index.ts b/src/cli/index.ts index a3e5e22..44c29b4 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -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."], @@ -642,7 +642,7 @@ export function createCli(io: CliIO = defaultCliIO): Command { .command("init") .option("--target ", "Template target: github or pre-commit", "github") .option("--node-version ", "Node.js version for GitHub Actions", "20") - .option("--package-version ", "Pinned published SkillSync package version", "0.1.2") + .option("--package-version ", "Pinned published SkillSync package version", "0.1.3") .option("--path ", "Project Skill paths") .option("--apply", "Write the generated file") .option("--force", "Allow replacing an existing generated file") @@ -650,7 +650,7 @@ export function createCli(io: CliIO = defaultCliIO): Command { 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, diff --git a/src/domain/artifact-contract.ts b/src/domain/artifact-contract.ts index 6782f5b..bd80648 100644 --- a/src/domain/artifact-contract.ts +++ b/src/domain/artifact-contract.ts @@ -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({ diff --git a/src/domain/artifact-references.ts b/src/domain/artifact-references.ts index ff6534a..2630968 100644 --- a/src/domain/artifact-references.ts +++ b/src/domain/artifact-references.ts @@ -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; sources?: Array<{ digest: string; row: number }> }; +type Index = { targets?: Map; sources?: Array<{ digest: string; row: number }>; referenced?: Set }; /** One invocation-local index: declared fields only, hashed IDs, fixed aggregate ceiling. */ export function createArtifactReferenceIndex(contract: ArtifactContract) { @@ -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 { @@ -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[] { @@ -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; }, diff --git a/src/reporters/sarif.ts b/src/reporters/sarif.ts index ea91726..da1afab 100644 --- a/src/reporters/sarif.ts +++ b/src/reporters/sarif.ts @@ -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()], }, }, diff --git a/templates/github/skillsync.yml b/templates/github/skillsync.yml index 9d1c146..ce15814 100644 --- a/templates/github/skillsync.yml +++ b/templates/github/skillsync.yml @@ -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: diff --git a/templates/pre-commit/skillsync.yaml b/templates/pre-commit/skillsync.yaml index f6bc684..4300337 100644 --- a/templates/pre-commit/skillsync.yaml +++ b/templates/pre-commit/skillsync.yaml @@ -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 diff --git a/tests/cli/ci.test.ts b/tests/cli/ci.test.ts index 6b52a64..c895241 100644 --- a/tests/cli/ci.test.ts +++ b/tests/cli/ci.test.ts @@ -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({ @@ -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({ diff --git a/tests/cli/help.test.ts b/tests/cli/help.test.ts index 566c376..993c489 100644 --- a/tests/cli/help.test.ts +++ b/tests/cli/help.test.ts @@ -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"]); diff --git a/tests/cli/report.test.ts b/tests/cli/report.test.ts index 829f66c..a1dd34a 100644 --- a/tests/cli/report.test.ts +++ b/tests/cli/report.test.ts @@ -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 { @@ -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_/); }); diff --git a/tests/docs/documentation.test.ts b/tests/docs/documentation.test.ts index e1a1b66..abf90b5 100644 --- a/tests/docs/documentation.test.ts +++ b/tests/docs/documentation.test.ts @@ -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([ @@ -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"); @@ -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"); diff --git a/tests/integration/artifact-references.test.ts b/tests/integration/artifact-references.test.ts index 64ee63c..c8cde62 100644 --- a/tests/integration/artifact-references.test.ts +++ b/tests/integration/artifact-references.test.ts @@ -88,3 +88,66 @@ describe("physical CSV references", () => { expect(report.findings[0].code).toBe("artifact.structure-or-snapshot"); expect(report.files).toEqual([]); }); }); + +describe("required target coverage", () => { + it("preserves the normalized contract digest of the existing citation fixture", async () => { + const root = join(import.meta.dirname, "../../fixtures/product/reference-integrity"); + const report = await runArtifacts({ contract: join(root, "contract.json"), path: join(root, "artifacts") }); + expect(report.exitCode).toBe(0); + expect(report.contract_sha256).toBe("3023d7b5d113d867f7651e8743f352ef27025cfa151e9463701e0b3748677308"); + }); + it("keeps unused targets allowed by default and by an explicit false option", async () => { + const defaults = await fixture("customer\nC1\n", "id\nC1\nC2\n"); + expect((await runArtifacts(defaults)).status).toBe("passed"); + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: false }); + expect((await runArtifacts(await fixture("customer\nC1\n", "id\nC1\nC2\n", contract))).status).toBe("passed"); + }); + it("passes complete target coverage with repeated forward references", async () => { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: true }); + const report = await runArtifacts(await fixture("customer\nC2\nC1\nC1\n", "id\nC1\nC2\n", contract)); + expect(report.status).toBe("passed"); expect(report.execution).toBe("not-run"); + }); + it("reports the uncovered target row without retaining or rendering its ID", async () => { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: true }); + const report = await runArtifacts(await fixture("customer\nC1\n", "id\nC1\nDO_NOT_DISCLOSE\n", contract)); + expect(report.findings).toEqual([{ code: "artifact.reference-unused", path: "z-customers.csv", field: "id", row: 2 }]); + expect(report.exitCode).toBe(1); + expect(renderArtifacts(report, "json") + renderArtifacts(report)).not.toContain("DO_NOT_DISCLOSE"); + }); + it("catches an empty delivery while bounding its target findings at 256", async () => { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: true }); + Object.assign(contract.files[0], { min_rows: 0 }); contract.files[1].max_rows = 300; + const targets = "id\n" + Array.from({ length: 300 }, (_, row) => `C${row + 1}\n`).join(""); + const report = await runArtifacts(await fixture("customer\n", targets, contract)); + expect(report.exitCode).toBe(1); expect(report.findings).toHaveLength(256); + expect(report.findings[0]).toEqual({ code: "artifact.reference-unused", path: "z-customers.csv", field: "id", row: 1 }); + expect(report.findings[255].row).toBe(256); + }); + it("checks each declared target table independently", async () => { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: true }); + contract.limits.max_files = 3; + contract.files.push({ ...contract.files[1], path: "other-customers.csv" }); + contract.checks.push({ ...contract.checks[0], target: { path: "other-customers.csv", field: "id" } }); + const options = await fixture("customer\nC1\n", "id\nC1\nC2\n", contract); + await writeFile(join(options.path, "other-customers.csv"), "id\nC1\n"); + const report = await runArtifacts(options); + expect(report.findings).toEqual([{ code: "artifact.reference-unused", path: "z-customers.csv", field: "id", row: 2 }]); + }); + it("keeps capacity failure ahead of target coverage and releases oversized indexes", async () => { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: true }); + contract.limits = { max_files: 2, max_file_bytes: 1_000_000, max_total_bytes: 2_000_000 }; + for (const file of contract.files) file.max_rows = 100_000; + const source = "customer\n" + "C1\n".repeat(50_000); + const target = "id\n" + Array.from({ length: 50_001 }, (_, row) => `C${row + 1}\n`).join(""); + const report = await runArtifacts(await fixture(source, target, contract)); + expect(report.findings).toEqual([{ code: "artifact.reference-capacity" }]); expect(report.exitCode).toBe(1); + }); + it("rejects non-boolean coverage options before reading artifact paths", async () => { + for (const value of ["true", 1, null, {}]) { + const contract = declaration(); Object.assign(contract.checks[0], { require_all_targets: value }); + const options = await fixture(undefined, undefined, contract); + const report = await runArtifacts({ ...options, path: join(options.root, "does-not-exist") }); + expect(report.exitCode).toBe(2); expect(report.findings[0].code).toBe("artifact.contract-invalid"); + } + }); +}); diff --git a/tests/reporters/sarif.test.ts b/tests/reporters/sarif.test.ts index db3526e..e4461f8 100644 --- a/tests/reporters/sarif.test.ts +++ b/tests/reporters/sarif.test.ts @@ -44,7 +44,7 @@ describe("renderSarif", () => { expect(sarif.version).toBe("2.1.0"); expect(sarif.runs[0]?.tool.driver.name).toBe("skillsync"); - expect(sarif.runs[0]?.tool.driver.version).toBe("0.1.2"); + expect(sarif.runs[0]?.tool.driver.version).toBe("0.1.3"); expect(sarif.runs[0]?.tool.driver.rules[0]?.id).toBe("structure.missing-reference"); expect(sarif.runs[0]?.tool.driver.rules[0]?.help?.text).toBe("Add the referenced file."); expect(sarif.runs[0]?.results[0]?.ruleId).toBe("structure.missing-reference");