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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Source string artifact fields support bounded exact `one_of` options for
classification labels, with a physical CSV/JSON material-intake example.
Unknown labels fail without value disclosure; classifications do not prove approval.
- Source `https_url` artifact fields validate bounded HTTPS addresses
against exact declared DNS hosts, with an offline knowledge-source inventory.
The checker never fetches sources, retains URL values in reports or proves truth.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ The command above verifies the included sample Skill. Replace the fixture path w

## What you get

- The local source candidate `artifacts` command independently checks actual
- The source `artifacts` command independently checks actual
CSV/JSON delivery files, Receipt/file hashes and cross-file count/integer sums.
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.
The [material intake catalog](fixtures/product/review-intake/README.md) rejects
unknown classification labels using bounded `one_of` strings; labels do not prove approval.
- `verify` reviews one local Skill for provenance, target compatibility, and changes without running its scripts.
- `scan` inventories local Skills, while `compat` checks their declared features against agent profiles.
- `diff` shows the meaningful changes between two Skill versions before you accept them.
Expand Down
15 changes: 15 additions & 0 deletions docs/artifact-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ follow redirects, assess the source text or prove that a named host is public or
trustworthy. Exact URL spelling remains unchanged for uniqueness comparisons.
Optional JSON fields may be omitted; an empty URL does not represent a source.

String fields can declare `one_of` with 1–32 unique exact options, each limited
to 256 UTF-16 code units. Options cannot contain surrounding whitespace, control
or format characters. Other scalar types cannot carry `one_of`; an `equals`
value must also be in the declared list. An empty option requires
`allow_empty: true`. Matching does not trim, normalize, fold case or coerce types;
CSV formula rejection still applies. Optional JSON fields may still be omitted.

For example, `{ "name": "currency", "type": "string", "one_of": ["CNY", "USD"] }`
checks only that the label is one of the caller's declarations. The
[material intake catalog](../fixtures/product/review-intake/README.md) demonstrates
domain and decision labels in physical CSV/JSON plus independent reference totals.
These classifications do not authenticate reviewers, authorize actions or prove
business correctness. This source extension is not in npm0.1.0; findings omit
the actual string values.

`bytes` rules check integrity and capacity without interpreting content. JSON
checks cover declared top-level fields, not full JSON Schema or arbitrary
expressions. Contract files are limited to 64 KiB. Limits must be explicit and
Expand Down
4 changes: 4 additions & 0 deletions docs/domain-adaptation.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ not vendor certification, and unknown/runtime-dependent features stay explicit.
checks actual bounded CSV/JSON, exact HTTPS host declarations and independent
reference counts. It is not in npm0.1.0 and does not verify external source truth.
- Add further versioned input-schema checks and bounded fixture resources.
- The source [material intake catalog](../fixtures/product/review-intake/README.md)
checks caller-declared domain/decision labels in real CSV/JSON with bounded
`one_of` string options. Labels are exact data classifications, not authenticated
approval or semantic review; this source extension is not in npm0.1.0.
- Add references and output-evidence requirements for additional domains.
- Validate a separately approved local execution backend using synthetic inputs
before describing it as an actual integration.
Expand Down
30 changes: 30 additions & 0 deletions fixtures/product/review-intake/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Bounded material intake labels

This synthetic source example checks actual CSV and JSON using closed string
options. It demonstrates a shared intake format for code-review material,
knowledge references and operations data; it performs no semantic review,
approval, authenticated handoff or external platform connection.

```bash
npm run build
node dist/cli/index.js artifacts \
--contract fixtures/product/review-intake/contract.json \
--path fixtures/product/review-intake/artifacts --format json
```

Three synthetic records have exact declared domains and decision labels. The
summary independently matches their row count and three reference markers. An
unknown or mistyped label fails even when numeric summaries still match. A
permitted label such as `ready` does not prove a person reviewed or approved the
material; the labels are only declared data values. Reference markers do not
prove source truth.

The contract allows two files, 16 KiB per file, 32 KiB total and 1,000 CSV rows.
`one_of` accepts 1–32 unique literal strings of at most 256 UTF-16 code units,
with no surrounding whitespace, control or format characters. Matching is exact:
no trimming, case folding, regular-expression evaluation or type coercion. An
empty option requires `allow_empty: true`. Values are not included in findings.

These checks use the current source implementation. They are not in the public
npm0.1.0 package, and are physical-file evidence with execution `not-run` and
provenance `not-authenticated`.
4 changes: 4 additions & 0 deletions fixtures/product/review-intake/artifacts/materials.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
material_id,domain,decision,reference_count
SYN-MAT-001,code-review,needs-review,1
SYN-MAT-002,knowledge-reference,ready,2
SYN-MAT-003,data-ops,blocked,0
6 changes: 6 additions & 0 deletions fixtures/product/review-intake/artifacts/summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"row_count": 3,
"total_references": 3,
"queue": "manual-review"
}
31 changes: 31 additions & 0 deletions fixtures/product/review-intake/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"schema": "skillsync.artifacts/v1",
"limits": { "max_files": 2, "max_file_bytes": 16384, "max_total_bytes": 32768 },
"files": [
{
"path": "materials.csv",
"format": "csv",
"columns": [
{ "name": "material_id", "type": "string" },
{ "name": "domain", "type": "string", "one_of": ["code-review", "knowledge-reference", "data-ops"] },
{ "name": "decision", "type": "string", "one_of": ["ready", "needs-review", "blocked"] },
{ "name": "reference_count", "type": "integer", "min": 0, "max": 10 }
],
"min_rows": 1,
"max_rows": 1000,
"unique_by": ["material_id"]
},
{
"path": "summary.json",
"format": "json",
"fields": [
{ "name": "schema_version", "type": "integer", "equals": 1 },
{ "name": "row_count", "type": "integer", "min": 1, "max": 1000 },
{ "name": "total_references", "type": "integer", "min": 0, "max": 10000 },
{ "name": "queue", "type": "string", "one_of": ["manual-review", "triage"] }
]
}
],
"checks": [{ "type": "csv_summary", "csv": "materials.csv", "json": "summary.json",
"row_count_field": "row_count", "sums": [{ "column": "reference_count", "field": "total_references" }] }]
}
2 changes: 2 additions & 0 deletions src/domain/artifact-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ function scalarValid(value: unknown, rule: ArtifactScalarRule): boolean {
if (rule.type === "boolean" && typeof value !== "boolean") return false;
if (rule.type === "array" && !Array.isArray(value)) return false;
if (rule.type === "object" && (!value || typeof value !== "object" || Array.isArray(value))) return false;
// One bounded literal list; do not coerce, normalize, evaluate or retain values.
if (rule.one_of !== undefined && (typeof value !== "string" || !rule.one_of.includes(value))) return false;
if (rule.equals !== undefined && value !== rule.equals) return false;
if (typeof value === "number" && ((rule.min !== undefined && value < rule.min) || (rule.max !== undefined && value > rule.max))) return false;
return true;
Expand Down
6 changes: 6 additions & 0 deletions src/domain/artifact-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const scalar = z.object({
name: z.string().min(1).max(128),
type: z.enum(["string", "integer", "number", "boolean", "date", "https_url", "array", "object"]),
allowed_hosts: z.array(allowedHost).min(1).max(16).optional(),
one_of: z.array(z.string().max(256).refine(value => value === value.trim() &&
!/[\p{Cc}\p{Cf}]/u.test(value), "options must be exact plain strings")).min(1).max(32).optional(),
required: z.boolean().default(true),
equals: z.union([z.string(), z.number().finite(), z.boolean()]).optional(),
min: z.number().finite().optional(), max: z.number().finite().optional(),
Expand Down Expand Up @@ -56,6 +58,10 @@ export const artifactContractSchema = z.object({ schema: z.literal("skillsync.ar
const rules = file.format === "csv" ? file.columns : file.format === "json" ? file.fields : [];
if (new Set(rules.map(rule => rule.name)).size !== rules.length) fail();
for (const rule of rules) {
if (rule.one_of !== undefined && (rule.type !== "string" ||
new Set(rule.one_of).size !== rule.one_of.length ||
(!rule.allow_empty && rule.one_of.includes("")) ||
(rule.equals !== undefined && (typeof rule.equals !== "string" || !rule.one_of.includes(rule.equals))))) fail();
if (rule.type === "https_url" ? !rule.allowed_hosts ||
new Set(rule.allowed_hosts).size !== rule.allowed_hosts.length : rule.allowed_hosts !== undefined) fail();
if ((rule.min !== undefined || rule.max !== undefined) && !["integer", "number"].includes(rule.type)) fail();
Expand Down
84 changes: 84 additions & 0 deletions tests/domain/artifact-enum-values.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from "vitest";
import { parseArtifactContract } from "../../src/domain/artifact-contract";
import { inspectArtifactContent } from "../../src/domain/artifact-content";

function contract(field: Record<string, unknown> = {}) {
return { schema: "skillsync.artifacts/v1",
limits: { max_files: 1, max_file_bytes: 4096, max_total_bytes: 4096 },
files: [{ path: "review.json", format: "json", fields: [{
name: "decision", type: "string", one_of: ["ready", "needs-review", "blocked"], ...field,
}] }] };
}

function inspect(value: unknown, field: Record<string, unknown> = {}) {
return inspectArtifactContent(parseArtifactContract(contract(field)).files[0],
Buffer.from(JSON.stringify({ decision: value })));
}

describe("bounded exact scalar options", () => {
it.each(["ready", "needs-review", "blocked"])("accepts declared value %s", value => {
expect(inspect(value).findings).toEqual([]);
});

it.each(["approved", "Ready", " ready", "ready ", "re\u200Bady", "", 1, true, null, { label: "ready" }])("rejects unlisted or mistyped material without retaining it: %j", value => {
const report = inspect(value);
expect(report.findings).toEqual([{ code: "artifact.json-value", path: "review.json", field: "decision" }]);
expect(report.facts.integers.size).toBe(0);
expect(Object.keys(report.facts)).toEqual(["integers"]);
});

it("shares the same rule with actual CSV cells", () => {
const rule = parseArtifactContract({ ...contract(), files: [{ path: "review.csv", format: "csv",
columns: [{ name: "decision", type: "string", one_of: ["ready", "blocked"] }], max_rows: 10 }] }).files[0];
expect(inspectArtifactContent(rule, Buffer.from("decision\nready\nblocked\n")).findings).toEqual([]);
expect(inspectArtifactContent(rule, Buffer.from("decision\nReady\n")).findings)
.toContainEqual({ code: "artifact.csv-value", path: "review.csv", field: "decision", row: 1 });
});

it("preserves formula rejection even for an explicitly listed string", () => {
const rule = parseArtifactContract({ ...contract(), files: [{ path: "review.csv", format: "csv",
columns: [{ name: "decision", type: "string", one_of: ["+ready"] }], max_rows: 10 }] }).files[0];
expect(inspectArtifactContent(rule, Buffer.from("decision\n+ready\n")).findings)
.toContainEqual({ code: "artifact.csv-formula", path: "review.csv", field: "decision", row: 1 });
});

it("allows an empty option only when the rule explicitly allows empty material", () => {
expect(inspect("", { one_of: ["", "ready"], allow_empty: true }).findings).toEqual([]);
expect(() => parseArtifactContract(contract({ one_of: ["", "ready"] }))).toThrow("artifact contract is invalid");
});

it("does not require an optional JSON field to be present", () => {
const rule = parseArtifactContract(contract({ required: false })).files[0];
expect(inspectArtifactContent(rule, Buffer.from("{}")).findings).toEqual([]);
});

it.each([
{ one_of: [] }, { one_of: ["ready", "ready"] }, { one_of: [1] },
{ one_of: Array.from({ length: 33 }, (_, index) => `state-${index}`) },
{ one_of: ["x".repeat(257)] }, { one_of: [" ready"] },
{ one_of: ["ready\n"] }, { one_of: ["re\u200Bady"] },
{ type: "integer", one_of: ["1"] }, { type: "date", one_of: ["2026-10-08"] },
{ type: "https_url", allowed_hosts: ["docs.example.test"], one_of: ["https://docs.example.test/"] },
{ equals: "approved" },
])("rejects inconsistent or unbounded declarations: %j", field => {
expect(() => parseArtifactContract(contract(field))).toThrow("artifact contract is invalid");
});

it("accepts the inclusive 32-option and 256-character boundaries", () => {
const values = ["x".repeat(256), ...Array.from({ length: 31 }, (_, index) => `state-${index}`)];
expect(inspect(values[0], { one_of: values, equals: values[0] }).findings).toEqual([]);
});

it("keeps original string contracts unchanged when options are absent", () => {
const rule = parseArtifactContract(contract({ one_of: undefined })).files[0];
expect(JSON.stringify(rule)).not.toContain("one_of");
expect(inspectArtifactContent(rule, Buffer.from('{"decision":"any-original-string"}')).findings).toEqual([]);
});

it("uses caller-declared currency labels without interpreting monetary values", () => {
expect(inspect("CNY", { one_of: ["CNY", "USD"] }).findings).toEqual([]);
expect(inspect("cny", { one_of: ["CNY", "USD"] }).findings)
.toContainEqual({ code: "artifact.json-value", path: "review.json", field: "decision" });
expect(inspect("待审", { one_of: ["待审", "已完成"] }).findings).toEqual([]);
});
});
63 changes: 63 additions & 0 deletions tests/integration/review-intake.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { runArtifacts, renderArtifacts } from "../../src/cli/commands/artifacts";
import { createCli } from "../../src/cli/index";

const source = resolve("fixtures/product/review-intake");
async function fixture(run: (contract: string, data: string) => Promise<void>) {
const root = await mkdtemp(join(tmpdir(), "skillsync-review-intake-"));
try {
await cp(join(source, "artifacts"), join(root, "artifacts"), { recursive: true });
await cp(join(source, "contract.json"), join(root, "contract.json"));
await run(join(root, "contract.json"), join(root, "artifacts"));
} finally { await rm(root, { recursive: true, force: true }); }
}

describe("physical review intake labels", () => {
it("accepts three synthetic domain records and a matching reference summary", async () => fixture(async (contract, data) => {
const report = await runArtifacts({ contract, path: data });
expect(report.status).toBe("passed");
expect(report.evidence).toBe("physical-files");
expect(report.execution).toBe("not-run");
expect(report.provenance).toBe("not-authenticated");
expect(report.files.find(file => file.path === "materials.csv")?.rows).toBe(3);
}));

it("fails unknown CSV labels through the CLI without exposing values or absolute paths", async () => fixture(async (contract, data) => {
const path = join(data, "materials.csv"), value = "PRIVATE_SYNTHETIC_SELECTION";
await writeFile(path, (await readFile(path, "utf8")).replace("needs-review", value));
const report = await runArtifacts({ contract, path: data });
expect(report.findings).toContainEqual({ code: "artifact.csv-value", path: "materials.csv", field: "decision", row: 1 });
for (const format of ["json", "text"]) {
const output = renderArtifacts(report, format);
expect(output).not.toContain(value);
expect(output).not.toContain(data);
}
let output = "", exitCode = 0;
const cli = createCli({ writeOut: text => { output += text; }, writeErr: () => undefined, setExitCode: code => { exitCode = code; } });
await cli.parseAsync(["node", "skillsync", "artifacts", "--contract", contract, "--path", data, "--format", "json"]);
expect(exitCode).toBe(1);
expect(JSON.parse(output).status).toBe("failed");
expect(output).not.toContain(value);
}));

it("applies the same closed options to the JSON summary", async () => fixture(async (contract, data) => {
const path = join(data, "summary.json");
const summary = JSON.parse(await readFile(path, "utf8"));
summary.queue = "AUTOMATIC_APPROVAL_NOT_DECLARED";
await writeFile(path, JSON.stringify(summary));
expect((await runArtifacts({ contract, path: data })).findings)
.toContainEqual({ code: "artifact.json-value", path: "summary.json", field: "queue" });
}));

it("retains independent aggregate checks when all labels are allowed", async () => fixture(async (contract, data) => {
const path = join(data, "summary.json");
const summary = JSON.parse(await readFile(path, "utf8"));
summary.total_references = 99;
await writeFile(path, JSON.stringify(summary));
expect((await runArtifacts({ contract, path: data })).findings)
.toContainEqual({ code: "artifact.summary-sum", path: "summary.json", field: "total_references" });
}));
});
Loading