import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; const REPO_ROOT = path.resolve(__dirname, '..', '..', '..'); const CANONICAL_SKILLS = path.join(REPO_ROOT, '.claude', 'skills'); function readSkillFile(skill: 'gitnexus-plan' | 'gitnexus-work', relativePath: string): string { return readFileSync(path.join(CANONICAL_SKILLS, skill, relativePath), 'utf-8'); } function expectConcepts( text: string, concepts: ReadonlyArray, ): void { for (const [description, pattern] of concepts) { expect(text, description).toMatch(pattern); } } function section(text: string, startHeading: string, endHeading?: string): string { const start = text.indexOf(startHeading); if (start < 0) throw new Error(`Missing contract section: ${startHeading}`); const end = endHeading ? text.indexOf(endHeading, start + startHeading.length) : text.length; if (endHeading && end < 0) throw new Error(`Missing contract section: ${endHeading}`); return text.slice(start, end); } describe('gitnexus-plan evidence provenance contract', () => { const ledger = readSkillFile('gitnexus-plan', 'references/context-ledger.md'); const pack = readSkillFile('gitnexus-plan', 'references/context-pack.md'); const template = readSkillFile('gitnexus-plan', 'references/plan-template.md'); const serializer = readSkillFile('gitnexus-plan', 'references/evidence-provenance.md'); const ledgerProvenance = section(ledger, '## Evidence provenance', '## Reread rules'); const packIntro = section(pack, '# Implementation context pack', '## Schema'); const packSchema = section(pack, '## Schema', '## Must not contain'); const packBounds = section(pack, '## Must not contain'); const templateContract = section(template, '## Compact form'); const provenanceContract = `${ledgerProvenance}\n${packSchema}\n${templateContract}`; it('makes provenance mandatory in compact and full packs', () => { expectConcepts(packIntro, [ ['compact pack includes provenance', /Compact plans[\s\S]*evidence_provenance/i], ['provenance is mandatory in both forms', /evidence_provenance[\s\S]*mandatory[\s\S]*both/i], ]); expect(packSchema).toMatch(/implementation_context:[\s\S]*evidence_provenance:/i); }); it('records a versioned global dirty digest and sorted cited-path manifest', () => { expectConcepts(provenanceContract, [ ['versioned provenance schema', /evidence_provenance[\s\S]*schema_version/i], ['full pinned commit identity', /head_commit:[^\n]*full commit/i], ['whole-tree dirty-state digest', /global_dirty_digest/i], ['sorted cited-path manifest', /cited_path_manifest[\s\S]*sorted/i], ['filesystem object kind', /object_kind/i], ['HEAD layer digest', /head_digest/i], ['index layer digest', /index_digest/i], ['worktree layer digest', /worktree_digest/i], ['untracked layer digest', /untracked_digest/i], ['generated plan excluded from the digest', /generated plan[\s\S]*exclud/i], ]); expect(packBounds).toMatch(/digest only|only its canonical[\s\S]*global_dirty_digest/i); expect(packBounds).toMatch(/detailed entries[\s\S]*bounded to cited paths/i); }); it('binds the emitted schema to one versioned portable serializer', () => { expect(packSchema.match(/canonicalization:/g) ?? []).toHaveLength(1); expect(packSchema).toMatch( /schema_version:\s*2[\s\S]*canonicalization:\s*['"]gitnexus-evidence-provenance-v2 NUL-framed UTF-8 records['"]/, ); expect(packSchema).toMatch(/sole normative emitted[\s\S]*executable serializer/i); expect(ledger).not.toMatch(/canonicalization:/); expect(ledgerProvenance).toMatch( /context-pack\.md[\s\S]*normative emitted field schema[\s\S]*do not redefine/i, ); expectConcepts(serializer, [ ['UTF-8 and NFC path contract', /valid UTF-8[\s\S]*Unicode[\s\S]*NFC/i], ['version and schema prefix', /gitnexus-evidence-provenance[\s\S]*schema_version[\s\S]*`2`/i], ['fixed field order', /fixed-order[\s\S]*head_kind[\s\S]*untracked_digest/i], ['NUL record framing', /NUL-framed[\s\S]*extra NUL/i], ['explicit absent literal', /literal `absent`/i], ['unsigned UTF-8 sorting', /unsigned lexicographic[\s\S]*UTF-8 bytes/i], ['rename endpoint expansion', /old endpoint[\s\S]*new endpoint[\s\S]*include both/i], ['exact plan exclusion', /one exact normalized path[\s\S]*No glob/i], ]); }); it('defines descriptor-anchored plan reads and digest-bound durable Deepen writes', () => { expectConcepts(serializer, [ [ 'read receipt carries canonical path, exact bytes, and digest', /read-plan[\s\S]*generated_plan_path[\s\S]*plan_bytes_base64[\s\S]*plan_digest/i, ], ['read rejects symlink parents and leaves', /read-plan[\s\S]*symlink[\s\S]*O_NOFOLLOW/i], [ 'Deepen requires the read receipt path and digest', /--replace[\s\S]*--expected-plan-path[\s\S]*--expected-plan-digest[\s\S]*same[\s\S]*receipt/i, ], [ 'preservation moves are directory durable', /preservation move[\s\S]*fsyncs both[\s\S]*source and destination directories/i, ], [ 'HEAD and index layers use captured anchors', /HEAD objects[\s\S]*captured[\s\S]*Index layers[\s\S]*captured/i, ], [ 'absent citations are descriptor guarded twice', /absent cited path[\s\S]*descriptor[\s\S]*checked both before and after/i, ], [ 'publication is a no-replace link, not an interpreter', /spawns no interpreter[\s\S]*link\(2\)[\s\S]*fails `EEXIST`/i, ], [ 'the macOS guarantee is stated, not smoothed over', /Linux anchors, macOS verifies[\s\S]*detection rather than prevention/i, ], ]); }); it.each(['staged', 'unstaged', 'untracked', 'deleted', 'renamed', 'mixed', 'absent'])( 'represents the %s cited-path state', (state) => { expect(provenanceContract).toMatch(new RegExp(`\\b${state}\\b`, 'i')); }, ); it('preserves both rename endpoints and canonical order', () => { expectConcepts(provenanceContract, [ ['rename source endpoint', /rename_from/i], ['rename destination endpoint', /rename_to/i], ['canonical sorted records', /canonical[\s\S]*sorted|sorted[\s\S]*canonical/i], ]); }); }); describe('gitnexus-work dirty-state re-anchoring contract', () => { const work = readSkillFile('gitnexus-work', 'SKILL.md'); const phase1 = section(work, '## Phase 1', '## Phase 2'); it('recomputes both provenance layers even at the same HEAD', () => { expectConcepts(phase1, [ [ 'same-HEAD recomputation', /same HEAD[\s\S]*recompute[\s\S]*global dirty digest[\s\S]*cited-path manifest/i, ], ['legacy provenance handling', /legacy[\s\S]*re-anchor/i], ['global mismatch handling', /global dirty digest[\s\S]*mismatch[\s\S]*re-anchor/i], ]); expect(phase1).not.toMatch( /HEAD equals the pin[\s\S]*skip all re-reading[\s\S]*go straight to work/i, ); }); it.each(['staged', 'unstaged', 'untracked', 'deleted', 'renamed', 'mixed'])( 'detects %s cited-path drift', (state) => { expect(phase1).toMatch(new RegExp(`\\b${state}\\b`, 'i')); }, ); it('rereads changed citations and assesses new uncited dirty scope', () => { expectConcepts(phase1, [ ['changed cited paths are reread', /changed cited paths?[\s\S]*re-read/i], ['new uncited dirtiness is assessed', /new uncited dirty paths?[\s\S]*assess/i], ['unreadable evidence blocks work', /unreadable[\s\S]*block/i], [ 'Deepen is reserved for invalidated planning decisions', /Deepen only if[\s\S]*scope[\s\S]*requirements?[\s\S]*(key technical decision|KTD)/i, ], ]); }); it('binds provenance to the exact plan document that was loaded', () => { expectConcepts(phase1, [ [ 'loaded plan uses the descriptor-anchored helper receipt', /descriptor-anchored[\s\S]*read-plan[\s\S]*plan_bytes_base64/i, ], [ 'loaded and recorded paths must match exactly', /byte-for-byte[\s\S]*read-plan receipt[\s\S]*evidence_provenance\.generated_plan_path/i, ], ]); }); }); describe('gitnexus-work build-current/index-current contract', () => { const work = readSkillFile('gitnexus-work', 'SKILL.md'); const inputTriage = section(work, '## Input triage', '## Phase 1'); const procedure = section(work, '### Build-current/index-current procedure', '## Phase 3'); const phase3 = section(work, '## Phase 3', '## Phase 4'); const phase4 = section(work, '## Phase 4', '## Never'); it('defines one procedure and invokes it at every graph boundary', () => { expect(work.match(/^### Build-current\/index-current procedure$/gim) ?? []).toHaveLength(1); expectConcepts(`${inputTriage}\n${procedure}`, [ ['procedure applies in direct mode', /direct mode[\s\S]*Build-current\/index-current/i], ]); expectConcepts(phase3, [ [ 'procedure runs before every graph-dependent impact query', /Build-current\/index-current[\s\S]*immediately before every graph-dependent[\s\S]*impact/i, ], ]); expectConcepts(phase4, [ [ 'procedure runs before final graph verification', /before final graph verification[\s\S]*Build-current\/index-current procedure/i, ], ]); }); it('invalidates stale graph state and proves the analyzer identity', () => { expectConcepts(procedure, [ [ 'committed and uncommitted relationship changes invalidate freshness', /relationship-affecting[\s\S]*committed[\s\S]*uncommitted[\s\S]*invalidat/i, ], ['indexed commit is compared', /index\.commit[\s\S]*current HEAD/i], [ 'typed persisted runner receipt is consumed', /index\.runner_identity[\s\S]*schemaVersion:\s*4[\s\S]*invoked-artifact[\s\S]*build[\s\S]*dependency-runtime[\s\S]*digest/i, ], [ 'schemas 1 through 3 are legacy', /schema-1,\s*schema-2, and schema-3 receipts[\s\S]*legacy/i, ], ['dependency canonicalization is current', /gitnexus-analyzer-dependency-runtime-v4/i], [ 'dependency package payload and native/parser runtime state is covered', /dependency-runtime digest[\s\S]*package metadata[\s\S]*JavaScript[\s\S]*native[\s\S]*parser artifacts/i, ], [ 'semantic status comparison excludes only the diagnostic entrypoint', /status --json[\s\S]*semantic field[\s\S]*excluding[\s\S]*invokedArtifact/i, ], [ 'status must report a current runner receipt', /runnerIdentityStatus:\s*current[\s\S]*incompleteReasons:\s*\[\][\s\S]*status:\s*up-to-date/i, ], ['MCP context must be complete', /index\.incomplete_reasons:\s*\[\]/i], [ 'stale or unknown receipts trigger a rebuild', /runner receipt[\s\S]*(stale|unknown)[\s\S]*build/i, ], ['current local analyzer is built', /npm run build/i], [ 'current local analyzer performs a PDG refresh', /node\s+[^\n]*dist\/cli\/index\.js\s+analyze\s+--index-only\s+--pdg/i, ], ['timestamps are only a conservative trigger', /timestamps?[\s\S]*trigger[\s\S]*not proof/i], [ 'refresh failure blocks graph-dependent work', /failure[\s\S]*blocks?[\s\S]*graph-dependent/i, ], ['inter-step relationship edits force another refresh', /inter-step[\s\S]*refresh/i], ['older runner fallback is forbidden', /do not fall back[\s\S]*older/i], [ 'legacy or unequal receipts force an actual metadata write', /--force[\s\S]*absent[\s\S]*malformed[\s\S]*unequal/i, ], ]); }); }); describe('gitnexus-plan read-only planning boundary', () => { const skill = readSkillFile('gitnexus-plan', 'SKILL.md'); const readme = readSkillFile('gitnexus-plan', 'README.md'); const planningDocs = `${skill}\n${readme}`; const feedback = section(skill, '## Skill feedback'); it('never builds analyzer output or mutates implementation files', () => { expectConcepts(planningDocs, [ ['dist builds are expressly forbidden', /must not build[\s\S]*dist\//i], ['source mutation is forbidden', /must not mutate[\s\S]*source/i], ['test mutation is forbidden', /must not mutate[\s\S]*tests?/i], ['configuration mutation is forbidden', /must not mutate[\s\S]*config/i], ]); expect(skill).not.toMatch( /when in doubt,? rebuild|permitted state changes[\s\S]*dist\/ rebuild/i, ); }); it('treats stale analyzer provenance as a source-weighted limitation', () => { expectConcepts(planningDocs, [ ['stale analyzer provenance is disclosed', /stale analyzer[\s\S]*provenance/i], ['claims become source-weighted', /source-weighted limitation/i], ['feedback stays in chat', /feedback[\s\S]*chat-only/i], ]); expect(feedback).not.toMatch(/append one JSON line|learnings\.jsonl/i); }); });