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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.3.0 - 2026-10-20

- Review all validated cross-file declarations and inspection limits in offline HTML, text and JSON reports, including CSV summaries, reference metadata and keyed integer allocations.
- Keep declarations visible when inputs are missing, retain legacy baselines without declaration metadata, and omit expected scalar values, raw identities and cell contents.
- Bound the checklist to 128 declarations and eight visible mappings per declaration; preserve complete mappings in the validated JSON report.


## 0.2.1 - 2026-10-19

- Locate citation metadata failures in both the delivered table and referenced table, including data-record positions and declared column mappings. Raw IDs and cell values remain omitted.
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

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

> **Alpha · GitHub release v0.2.1 · Node.js 20+**
> **Alpha · GitHub release v0.3.0 · 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.

GitHub v0.2.1 locates citation metadata failures by file, data record and compared columns. It checks exact digests and revisions without copying record values into reports. The offline review, correction workflow and bounded composite-key reconciliation remain available. The npm registry stays at 0.1.0; use the verified GitHub archive for current features.
GitHub v0.3.0 adds a declared cross-file checklist to HTML, text and JSON reports, including paths, field mappings and capacity limits. Declarations remain separate from actual results, and expected values are omitted. [Review the declaration list](docs/declared-checks.md). It also locates citation metadata failures by file, data record and compared columns. It checks exact digests and revisions without copying record values into reports. The offline review, correction workflow and bounded composite-key reconciliation remain available. The npm registry stays at 0.1.0; use the verified GitHub archive for current features.

[![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 @@ -15,7 +15,7 @@ GitHub v0.2.1 locates citation metadata failures by file, data record and compar

Tagged builds now publish a CLI tarball, checksums, a validated CycloneDX SBOM
and a source manifest on [GitHub Releases](https://github.com/Chumaniac/skillsync/releases).
[GitHub v0.2.1](https://github.com/Chumaniac/skillsync/releases/tag/v0.2.1)
[GitHub v0.3.0](https://github.com/Chumaniac/skillsync/releases/tag/v0.3.0)
is distributed as a versioned CLI tarball with checksums, SBOM, source manifest
and GitHub provenance. Its release workflow requires the exact immutable tag
and an already successful main check before packaging. Earlier v0.1.7 assets
Expand All @@ -24,12 +24,12 @@ Installation uses Node.js 20+ and fetches the package's public dependencies;
it needs no npm login.

```bash
release_base="https://github.com/Chumaniac/skillsync/releases/download/v0.2.1"
for asset in chumanic-skillsync-0.2.1.tgz checksums.txt sbom.cyclonedx.json release-manifest.json; do
release_base="https://github.com/Chumaniac/skillsync/releases/download/v0.3.0"
for asset in chumanic-skillsync-0.3.0.tgz checksums.txt sbom.cyclonedx.json release-manifest.json; do
curl --fail --location --output "$asset" "$release_base/$asset"
done
shasum -a 256 -c checksums.txt
npm install --prefix ./skillsync-tools ./chumanic-skillsync-0.2.1.tgz
npm install --prefix ./skillsync-tools ./chumanic-skillsync-0.3.0.tgz
./skillsync-tools/node_modules/.bin/skillsync --version
```

Expand All @@ -47,7 +47,7 @@ npm install -g @chumanic/skillsync@0.1.0
skillsync verify --path . --target codex
```

**From source (0.2.1):**
**From source (0.3.0):**

```bash
git clone https://github.com/Chumaniac/skillsync.git
Expand Down
17 changes: 17 additions & 0 deletions docs/declared-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Review declared cross-file checks

Artifact text, JSON and standalone HTML reports now include the validated cross-file declarations and file/byte limits. The three supported kinds are CSV summaries, reference existence/metadata bindings, and integer reconciliation by identity. This lets a reviewer see the selected paths, fields, identity columns and mappings without reopening the contract.

Declarations describe expectations; they are separate from completed observations and findings. They remain visible when a required file is missing. Invalid contracts have no inferred declaration list, and legacy JSON baselines without this optional metadata remain accepted. Per-file type, digest and scalar-value rules remain in the original contract: expected values, enum choices, raw IDs and cell values are not copied into the list.

The contract/parser limit is 128 cross-file checks. HTML shows all 128 and up to 8 mappings per check, clips metadata at 160 characters and has no scripts or external requests. JSON retains complete declared mappings; report input remains bounded and validated when used as a baseline.

Method reference: [CLI-Anything's structured and verifiable output](https://github.com/HKUDS/CLI-Anything/blob/34f519533bc175d2fe287ab8316b0dd99bb9cc43/README.md), Apache-2.0, reviewed from the 2026-10-19 Trending snapshot. Only the output-review method is borrowed; no upstream executable or plugin is installed.

From a source checkout, try the original synthetic fixture:

```sh
node dist/cli/index.js artifacts --contract fixtures/product/declared-checks/contract.json --path fixtures/product/declared-checks/artifacts --format html > declared-checks.html
```

The packaged CLI includes the same fixture under its package directory. Open the resulting HTML and select **Declared checks** to review all three kinds before inspecting outcomes.
2 changes: 2 additions & 0 deletions fixtures/product/declared-checks/artifacts/actual.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,cents,revision
synthetic-order-a,100,fixture-v1
2 changes: 2 additions & 0 deletions fixtures/product/declared-checks/artifacts/expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,cents,revision
synthetic-order-a,100,fixture-v1
1 change: 1 addition & 0 deletions fixtures/product/declared-checks/artifacts/summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"rows":1,"cents":100}
112 changes: 112 additions & 0 deletions fixtures/product/declared-checks/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"schema": "skillsync.artifacts/v1",
"limits": {
"max_files": 3,
"max_file_bytes": 4096,
"max_total_bytes": 8192
},
"files": [
{
"path": "expected.csv",
"format": "csv",
"columns": [
{
"name": "id",
"type": "string"
},
{
"name": "cents",
"type": "integer"
},
{
"name": "revision",
"type": "string"
}
],
"unique_by": [
"id"
],
"max_rows": 10
},
{
"path": "actual.csv",
"format": "csv",
"columns": [
{
"name": "id",
"type": "string"
},
{
"name": "cents",
"type": "integer"
},
{
"name": "revision",
"type": "string"
}
],
"unique_by": [
"id"
],
"max_rows": 10
},
{
"path": "summary.json",
"format": "json",
"fields": [
{
"name": "rows",
"type": "integer"
},
{
"name": "cents",
"type": "integer"
}
]
}
],
"checks": [
{
"type": "csv_summary",
"csv": "actual.csv",
"json": "summary.json",
"row_count_field": "rows",
"sums": [
{
"column": "cents",
"field": "cents"
}
]
},
{
"type": "reference_exists",
"source": {
"path": "actual.csv",
"field": "id"
},
"target": {
"path": "expected.csv",
"field": "id"
},
"matches": [
{
"source": "revision",
"target": "revision"
}
]
},
{
"type": "keyed_integer_sum_equals",
"source": {
"path": "expected.csv",
"key": "id",
"value": "cents"
},
"target": {
"path": "actual.csv",
"key": "id",
"value": "cents"
}
}
]
}
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.2.1",
"version": "0.3.0",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "node:path";
import { parse as parseYaml } from "yaml";
import { z } from "zod";

import { parseArtifactContract } from "../../domain/artifact-contract.js";
import { parseArtifactContract, type ArtifactDeclarations } from "../../domain/artifact-contract.js";
import { parseStrictJson } from "../../domain/strict-json.js";
import { inspectArtifactContent, type ArtifactFacts, type ArtifactFinding, type ArtifactRowObserver } from "../../domain/artifact-content.js";
import { createArtifactReferenceIndex } from "../../domain/artifact-references.js";
Expand All @@ -30,6 +30,7 @@ export type ArtifactReport = {
files: Array<{ path: string; bytes: number; sha256: string; rows?: number }>;
findings: ArtifactFinding[];
reconciliations?: ReconciliationSummary[];
declarations?: ArtifactDeclarations;
comparison?: ArtifactComparison;
};

Expand Down Expand Up @@ -70,6 +71,8 @@ async function inspectArtifacts(options: ArtifactsOptions): Promise<ArtifactRepo
const bytes = await readWorkspaceFile(options.contract, 64 * 1024);
contract = parseArtifactContract(parseYaml(new TextDecoder("utf-8", { fatal: true }).decode(bytes), { maxAliasCount: 100, prettyErrors: false, logLevel: "silent" }));
report.contract_sha256 = hash(JSON.stringify(contract));
// Only validated cross-file metadata is included; scalar expected values and raw cells are omitted.
report.declarations = { limits: { ...contract.limits }, checks: contract.checks.map((check, index) => ({ ordinal: index + 1, check })) };
} catch { issue("artifact.contract-invalid"); report.exitCode = 2; return report; }
let root = options.path ?? join(options.delivery!, "artifacts");
try {
Expand Down Expand Up @@ -175,6 +178,7 @@ export function renderArtifacts(report: ArtifactReport, format = "text"): string
...(safe.comparison ? [`Baseline comparison: ${safe.comparison.status}${safe.comparison.reason ? `; ${safe.comparison.reason}` : ""}`,
...(safe.comparison.findings ? [`Finding categories: ${safe.comparison.findings.added} new; ${safe.comparison.findings.ongoing} ongoing; ${safe.comparison.findings.resolved} resolved`] : [])] : []),
...(safe.comparison?.checks ? [`Record check changes: ${safe.comparison.checks.filter(check => check.change === "improved").length} improved; ${safe.comparison.checks.filter(check => check.change === "regressed").length} regressed; ${safe.comparison.checks.filter(check => check.change === "changed").length} mixed changes; ${safe.comparison.checks.filter(check => check.change === "unchanged").length} unchanged`] : []),
...(safe.declarations ? [`Declared cross-file checks: ${safe.declarations.checks.length}`, `Inspection limits: ${JSON.stringify(safe.declarations.limits)}`, ...safe.declarations.checks.map(item => `Declaration ${item.ordinal}: ${JSON.stringify(item.check)}`)] : ["Declaration metadata: unavailable in this snapshot"]),
`Files: ${safe.files.length}`, ...(safe.reconciliations ?? []).map(item => `Record reconciliation: ${item.status}${item.status !== "unknown" ? `; source keys=${item.sourceKeys}; target keys=${item.targetKeys}; missing=${item.missingKeys}; unexpected=${item.unexpectedKeys}; mismatched values=${item.valueMismatches}` : ""}`),
...safe.findings.map(finding => `${finding.code}${finding.path ? ` file=${JSON.stringify(finding.path)}` : ""}${finding.field ? ` field=${JSON.stringify(finding.field)}` : ""}${finding.row !== undefined ? ` row=${finding.row}` : ""}${finding.reference ? ` reference=${JSON.stringify(finding.reference)}` : ""}${finding.message ? ` ${finding.message}` : ""}`)].join("\n") + "\n";
}
19 changes: 15 additions & 4 deletions src/domain/artifact-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,25 @@ const keyColumn = z.string().min(1).max(128);
const keyedSide = z.object({ path: safePath, key: z.union([keyColumn, z.array(keyColumn).min(2).max(4)]), value: z.string().min(1).max(128) }).strict();
const reconciliation = z.object({ type: z.literal("keyed_integer_sum_equals"), source: keyedSide, target: keyedSide }).strict();

export const artifactContractSchema = z.object({ schema: z.literal("skillsync.artifacts/v1"),
limits: z.object({
const checkSchema = z.discriminatedUnion("type", [comparison, reference, reconciliation]);
const limitsSchema = z.object({
max_files: z.number().int().positive().max(WORKSPACE_TREE_LIMITS.maxFiles),
max_file_bytes: z.number().int().positive().max(WORKSPACE_TREE_LIMITS.maxFileBytes),
max_total_bytes: z.number().int().positive().max(WORKSPACE_TREE_LIMITS.maxTotalBytes),
}).strict(),
}).strict();

export const artifactDeclarationsSchema = z.object({
limits: limitsSchema,
checks: z.array(z.object({ ordinal: z.number().int().min(1).max(128), check: checkSchema }).strict()).max(128),
}).strict().superRefine((value, context) => {
if (value.checks.some((item, index) => item.ordinal !== index + 1)) context.addIssue({ code: z.ZodIssueCode.custom, message: "declaration ordinals must be consecutive" });
});
export type ArtifactDeclarations = z.infer<typeof artifactDeclarationsSchema>;

export const artifactContractSchema = z.object({ schema: z.literal("skillsync.artifacts/v1"),
limits: limitsSchema,
files: z.array(z.discriminatedUnion("format", [bytes, csv, json])).max(WORKSPACE_TREE_LIMITS.maxFiles),
checks: z.array(z.discriminatedUnion("type", [comparison, reference, reconciliation])).max(128).default([]),
checks: z.array(checkSchema).max(128).default([]),
}).strict().superRefine((contract, context) => {
const fail = () => context.addIssue({ code: z.ZodIssueCode.custom, message: "inconsistent artifact declarations" });
const files = new Map(contract.files.map(file => [file.path, file]));
Expand Down
2 changes: 2 additions & 0 deletions src/domain/artifact-report-comparison.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { z } from "zod";
import { artifactDeclarationsSchema } from "./artifact-contract.js";
import type { ArtifactReport } from "../cli/commands/artifacts.js";

const sha = z.string().regex(/^[0-9a-f]{64}$/);
Expand Down Expand Up @@ -54,6 +55,7 @@ const reportSchema = z.object({ schema: z.literal("skillsync.artifacts-report/v1
}).strict().optional(),
}).strict()).max(256), reconciliations: z.array(summary).max(128).optional(),
comparison: artifactComparisonSchema.optional(),
declarations: artifactDeclarationsSchema.optional(),
}).strict().superRefine((report, context) => {
if (new Set(report.files.map(file => file.path)).size !== report.files.length ||
report.files.reduce((total, file) => total + file.bytes, 0) > 64 * 1024 * 1024 ||
Expand Down
Loading
Loading