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

## 0.1.6 source candidate - 2026-10-10

- Compare ordered two-to-four-column string identities as well as existing
single keys. Organization-local IDs no longer need to be flattened into a
delimiter string for integer reconciliation.
- Frame component counts and UTF-8 byte lengths; preserve exact spelling and
positional aliases. Count every component and value against the existing
100,000-cell limit. Invalid dimensions fail before artifact reads.
- Composite-key findings contain only the bounded declared column-name array;
raw identifiers, fingerprints and amounts remain internal. Existing single-key
contracts and reports retain their behavior.
- Add a reproducible synthetic multi-organization fixture. This source candidate
is not a new GitHub/npm release; GitHub0.1.4/npm0.1.0 remain unchanged.

## 0.1.5 source candidate - 2026-10-09

- Add bounded per-key integer reconciliation across physical CSV files. Split
Expand Down
9 changes: 6 additions & 3 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 · source candidate v0.1.5 · Node.js 20+**
> **Alpha · source candidate v0.1.6 · 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.

Source v0.1.5 adds per-key integer reconciliation. The published GitHub package
Source v0.1.6 adds bounded ordered composite keys to per-key integer reconciliation. The published GitHub package
is still v0.1.4 and npm is still 0.1.0; neither includes the new rule.

[![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 @@ -21,7 +21,7 @@ npm install -g @chumanic/skillsync@0.1.0
skillsync verify --path . --target codex
```

**From source (0.1.5 candidate):**
**From source (0.1.6 candidate):**

```bash
git clone https://github.com/Chumaniac/skillsync.git
Expand Down Expand Up @@ -62,6 +62,9 @@ The command above verifies the included sample Skill. Replace the fixture path w

## What you get

- Source0.1.6 supports ordered two-to-four-column identity tuples for integer
reconciliation. The [multi-organization example](fixtures/product/tenant-reconciliation/README.md)
separates repeated local IDs, without delimiter collisions or raw ID reporting.
- Source `keyed_integer_sum_equals` checks compare each record key's integer
total across CSV files, including split deliveries. They detect balanced but
wrongly allocated results that global sums cannot identify. The
Expand Down
36 changes: 35 additions & 1 deletion docs/artifact-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ authenticated evidence; the report marks provenance as not authenticated.

## Per-key integer reconciliation

Source candidate 0.1.5 supports `keyed_integer_sum_equals`. It is absent from
Source candidate 0.1.6 supports `keyed_integer_sum_equals`. It is absent from
published npm0.1.0 and the existing GitHub0.1.4 package. Each side declares a CSV
path, required nonempty string `key`, and required integer `value` column.
The two paths must be distinct; comparing a file with itself is rejected before
Expand Down Expand Up @@ -63,6 +63,40 @@ Without the rule, existing normalized contracts and report shapes are unchanged.
Reports still say `execution: not-run` and `provenance: not-authenticated`;
equality does not authenticate the expected input or approve a business action.

### Ordered composite identities in source 0.1.6

The `key` selector may also be an ordered array of two to four distinct column
names. Every component must be a declared required nonempty string. Both sides
must have the same number of components; positional column names may differ.
One-element arrays, duplicate/empty names, undeclared fields, wrong scalar types,
nullable components and mixed-width sides are rejected before artifact reads.

```json
{
"type": "keyed_integer_sum_equals",
"source": { "path": "expected.csv", "key": ["tenant_id", "order_id"], "value": "amount_cents" },
"target": { "path": "allocations.csv", "key": ["tenant_id", "order_id"], "value": "amount_cents" }
}
```

Two organizations can use the same local order ID without their totals being
merged. Component order, whitespace, Unicode spelling and numeric-looking
strings remain exact. Tuples are internally framed with component count and
UTF-8 byte lengths before hashing; joining IDs with a separator is not equivalent.
Single-string keys preserve the existing behavior.

Each unique grouping observes `key components + integer value` cells per row.
The invocation-wide limit remains 100,000, including repeated rows. Matching
group declarations reuse an index; matching selectors reuse their fingerprint
within a row. Exhaustion and unsafe sums preserve `unknown` without partial
counts. Composite missing-key findings have a `field` array of declared column
names, while ordinary findings retain a string. No tuple values are reported.

The [multi-organization example](../fixtures/product/tenant-reconciliation/README.md)
detects a balanced cross-organization shift that a single local ID accepts.
Composite identities apply to this integer reconciliation rule. The separate
`reference_exists` rule still uses a single unique string field.

## Declared requirements

The [synthetic contract](../fixtures/product/order-summary/contract.json) declares:
Expand Down
11 changes: 11 additions & 0 deletions docs/deep-optimization-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ Keep the existing 10,000-file/16 MiB-file/64 MiB-total and 256-finding ceilings.

## Domain expansion

### Continuation cycle — 2026-10-10

Source0.1.6 implements ordered two-to-four-column string keys for integer
reconciliation, with positional aliases and unambiguous UTF-8 framing.
Every component consumes the existing bounded selected-cell budget. The
multi-organization fixture demonstrates repeated local IDs and balanced shifts;
review, knowledge and operations can use workspace/file/check or source/version
dimensions deliberately. This does not add composite references, JSON relations,
signed identity or any live service. The next producer-consumer journey retains
actual synthetic deliveries from the existing restricted SkillTape executor.

| Domain | Contract example | Benefit | Evidence limit |
| --- | --- | --- | --- |
| Code review | Expected versus delivered findings/counts per changed file | Detect lost or misassigned review materials | No semantic correctness or reviewer authentication. |
Expand Down
41 changes: 41 additions & 0 deletions fixtures/product/tenant-reconciliation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Multi-organization integer reconciliation

Status: source candidate0.1.6, 2026-10-10. IDs and amounts are synthetic.
This fixture is absent from GitHub0.1.4/npm0.1.0 and runs no Skill, network,
account or payment operation. Build the current source:

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

ORG_A and ORG_B both use ORDER_1. Their expectations are 1,000 and 2,000 minor
units. Split allocation rows pass when grouped by the ordered
`["tenant_id", "order_id"]` tuple. A rule using only `order_id` merges these
organizations and can miss allocation errors.

Preserve the successful input and copy a fault into an independent directory:

```bash
workspace="$(mktemp -d)"
cp -R fixtures/product/tenant-reconciliation/artifacts "$workspace/artifacts"
cp fixtures/product/tenant-reconciliation/faults/shifted-allocations.csv \
"$workspace/artifacts/allocations.csv"
node dist/cli/index.js artifacts \
--contract fixtures/product/tenant-reconciliation/contract.json \
--path "$workspace/artifacts" --format json
```

The second command exits1 and reports two mismatched tuple totals. Global
counts and total3,000 remain consistent. Keys have two to four required exact
string components; all components and values consume the bounded cell budget.
Findings expose only declared paths/columns and counts, not organization/order
values or per-record amounts.

Reports retain `execution: not-run` and `provenance: not-authenticated`.
Requirements and their ownership must be reviewed separately. No identity,
source truth, authorization or real financial integration is established.
See [artifact rules](../../../docs/artifact-delivery.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tenant_id,order_id,amount_cents
ORG_A,ORDER_1,400
ORG_B,ORDER_1,2000
ORG_A,ORDER_1,600
3 changes: 3 additions & 0 deletions fixtures/product/tenant-reconciliation/artifacts/expected.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tenant_id,order_id,amount_cents
ORG_A,ORDER_1,1000
ORG_B,ORDER_1,2000
5 changes: 5 additions & 0 deletions fixtures/product/tenant-reconciliation/artifacts/summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"expected_rows": 2,
"allocation_rows": 3,
"total_cents": 3000
}
120 changes: 120 additions & 0 deletions fixtures/product/tenant-reconciliation/contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"schema": "skillsync.artifacts/v1",
"limits": {
"max_files": 3,
"max_file_bytes": 1048576,
"max_total_bytes": 3145728
},
"files": [
{
"path": "expected.csv",
"format": "csv",
"columns": [
{
"name": "tenant_id",
"type": "string"
},
{
"name": "order_id",
"type": "string"
},
{
"name": "amount_cents",
"type": "integer",
"min": 0
}
],
"unique_by": [
"tenant_id",
"order_id"
],
"max_rows": 1000
},
{
"path": "allocations.csv",
"format": "csv",
"columns": [
{
"name": "tenant_id",
"type": "string"
},
{
"name": "order_id",
"type": "string"
},
{
"name": "amount_cents",
"type": "integer",
"min": 0
}
],
"max_rows": 1000
},
{
"path": "summary.json",
"format": "json",
"fields": [
{
"name": "expected_rows",
"type": "integer",
"min": 0
},
{
"name": "allocation_rows",
"type": "integer",
"min": 0
},
{
"name": "total_cents",
"type": "integer",
"min": 0
}
]
}
],
"checks": [
{
"type": "csv_summary",
"csv": "expected.csv",
"json": "summary.json",
"row_count_field": "expected_rows",
"sums": [
{
"column": "amount_cents",
"field": "total_cents"
}
]
},
{
"type": "csv_summary",
"csv": "allocations.csv",
"json": "summary.json",
"row_count_field": "allocation_rows",
"sums": [
{
"column": "amount_cents",
"field": "total_cents"
}
]
},
{
"type": "keyed_integer_sum_equals",
"source": {
"path": "expected.csv",
"key": [
"tenant_id",
"order_id"
],
"value": "amount_cents"
},
"target": {
"path": "allocations.csv",
"key": [
"tenant_id",
"order_id"
],
"value": "amount_cents"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tenant_id,order_id,amount_cents
ORG_A,ORDER_1,500
ORG_B,ORDER_1,1900
ORG_A,ORDER_1,600
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.5",
"version": "0.1.6",
"private": false,
"publishConfig": {
"access": "public"
Expand Down
2 changes: 1 addition & 1 deletion src/domain/artifact-content.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ArtifactFileRule, ArtifactScalarRule } from "./artifact-contract.js";
import { parseStrictJson } from "./strict-json.js";

export type ArtifactFinding = { code: string; path?: string; field?: string; row?: number; message?: string };
export type ArtifactFinding = { code: string; path?: string; field?: string | string[]; row?: number; message?: string };
export type ArtifactFacts = { rows?: number; integers: Map<string, number> };
export type ArtifactCellObserver = (field: string, value: string, row: number) => void;
export type ArtifactRowObserver = (values: ReadonlyMap<string, string | number>) => void;
Expand Down
15 changes: 11 additions & 4 deletions src/domain/artifact-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const referenceField = z.object({ path: safePath, field: z.string().min(1).max(1
const reference = z.object({ type: z.literal("reference_exists"), source: referenceField, target: referenceField,
require_all_targets: z.boolean().optional(),
}).strict();
const keyedSide = z.object({ path: safePath, key: z.string().min(1).max(128), value: z.string().min(1).max(128) }).strict();
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"),
Expand Down Expand Up @@ -83,13 +84,19 @@ export const artifactContractSchema = z.object({ schema: z.literal("skillsync.ar
for (const check of contract.checks) {
if (check.type === "keyed_integer_sum_equals") {
if (check.source.path === check.target.path) fail();
const width = (key: string | string[]) => typeof key === "string" ? 1 : key.length;
if (width(check.source.key) !== width(check.target.key)) fail();
for (const side of [check.source, check.target]) {
const file = files.get(side.path);
if (file?.format !== "csv") { fail(); continue; }
const key = file.columns.find(column => column.name === side.key);
const keys = typeof side.key === "string" ? [side.key] : side.key;
if (new Set(keys).size !== keys.length) fail();
for (const name of keys) {
const key = file.columns.find(column => column.name === name);
if (key?.type !== "string" || key.allow_empty || !key.required) fail();
}
const value = file.columns.find(column => column.name === side.value);
if (key?.type !== "string" || key.allow_empty || !key.required ||
value?.type !== "integer" || !value.required) fail();
if (value?.type !== "integer" || !value.required) fail();
}
continue;
}
Expand Down
Loading
Loading