From c16a4f6a73f89ed42731b4313673401c455616ef Mon Sep 17 00:00:00 2001 From: Chinmay <4730291+chinmaygit@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:30:02 +0200 Subject: [PATCH 1/5] fix(engine): parser bugs found dogfooding against DSAMind's real constitution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DSAMind is the first consumer with a constitution shaped independently of this repo's own dogfood format. Pointing `constitution audit` at it surfaced four real parser/audit gaps, not hypothetical ones: - P-line parser required bare `**P1.**`; DSAMind titles its P-lines (`**P1 — Title.**`) — now accepted, title folded into the statement text. - Article parser required exactly `### Article `; DSAMind groups Articles under `### §
` domain headings, one level deeper (`#### Article`) — now both levels accepted, and the bullet-block boundary widened to match. - Serves-list parsing choked on inline parenthetical commentary per P-id, fragmenting on commas inside the parens and reading real Articles as serving nothing — parens are now stripped before splitting. - ART-STATUS rejected `SUPERSEDED — `, the exact shape `ratify-amendment` itself produces. - LEDGER-SYNC fired unconditionally, comparing the framework pin against the ledger's own version axis — only valid for `selfHosted` instances. Wired the already-parsed but never-read `selfHosted` field into this one check. Verified: self-audit on this repo stays 0 findings (no regression); DSAMind's audit output went from 1 real error + 12 false-positive warnings to 16 real errors + 6 real warnings — including 5 ART-MECH-DEBT findings that independently match DSAMind's own hand-audited mechanization backlog list. 21/21 tests pass (2 new, covering the format variance + LEDGER-SYNC scoping). Co-Authored-By: Claude Sonnet 5 --- BUILDLOG.md | 82 ++++++++++++++++++++++++++++++++++++++++- cli/package-lock.json | 4 +- cli/src/engine/audit.ts | 14 +++++-- cli/src/engine/parse.ts | 18 ++++++--- cli/test/engine.test.ts | 70 +++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 12 deletions(-) diff --git a/BUILDLOG.md b/BUILDLOG.md index 4002d31..7850289 100644 --- a/BUILDLOG.md +++ b/BUILDLOG.md @@ -186,7 +186,85 @@ gate live and clean. Worktree rebased onto main. (live) dashboard beyond static HTML; multi-instance registry telemetry. ### Known-untested / deferred -- Publishing 0.17.0 to GitHub Packages (needs operator's npm auth). - Tone generation quality (needs `claude` CLI at runtime; engine tested with a stub). -- DSAMind adoption of the new engine (separate repo, separate session). - Multi-instance/registry telemetry, web dashboard beyond static HTML — later phases. + +--- + +## Session 3 — 2026-07-04 (same worktree; DSAMind adoption; v0.17.2 published + operator's global updated) + +### Context at start +Operator published 0.17.1/0.17.2 and updated their global CLI (pre-commit hook now +enforces for real). Scope: point the engine at DSAMind (`~/Workspace/dsa_project`) — a +real, independently-ratified constitution (11 Articles, its own Amendments Ledger back to +[0.1.0]) — the first test of the engine against a document it wasn't hand-built against. + +### What happened — real bugs found by dogfooding, not hypothesized +Running `constitution audit` against DSAMind's actual `CONSTITUTION.md` surfaced the +engine's format assumptions were narrower than "the framework's own documents" — they +were narrower than *this repo's own dogfood shape specifically*. Fixed four real parser/ +audit bugs in `cli/src/engine/{parse,audit}.ts` (tests added, `cli/test/engine.test.ts`): + +1. **P-line parser required bare `**P1.**`** — DSAMind titles its P-lines + (`**P1 — Fluency, not coverage.**`). Fixed to accept an optional `— Title` before the + closing `.**`, folding the title into the statement text (nothing discarded). +2. **Article parser required exactly `### Article `** — DSAMind groups Articles under + `### §A` domain sections, so Articles are one level deeper: `#### Article `. Fixed + to accept `###`/`####`, and widened the bullet-block end-boundary to stop at either + level so one Article's bullets don't swallow the next. +3. **Serves-list parser choked on parenthetical commentary** — DSAMind annotates each + served P-line inline (`P1 (fluency is measured...), P4 (create-value: ...)`); the old + split-on-comma fragmented *inside* the parens, and no fragment matched the exact + `^P\d+$` filter, so real Articles read as serving nothing (cascading into false + `ART-SERVES` / `ART-SERVES-DANGLING`/`L0-UNSERVED` findings). Fixed by stripping + `(...)` groups before splitting. +4. **`ART-STATUS` rejected `SUPERSEDED — `** — the exact shape `ratify-amendment` + itself is speced to produce for a superseded Article. Fixed to accept the dated form. +5. **`LEDGER-SYNC` fired unconditionally** — it compared the header's `framework: + constitution@X` pin against the newest Amendments Ledger entry version, which is only + the same axis when `selfHosted` (this repo pins itself). A downstream consumer's ledger + tracks *its own* constitution version, independent of the framework spec version it + pins — DSAMind's ledger is at its own `[0.11.0]` while pinning `framework@0.16.12`, + legitimately. `doc.selfHosted` was already parsed but never read anywhere — wired the + existing field into the one check that needed it, rather than adding new plumbing. + +### Verified by running +- `cd cli && npm test` — **21/21** (19 prior + 2 new: real-world format-variance parsing, + LEDGER-SYNC scoped to self-hosted only). +- `npm run build` — clean; self-audit (`node cli/dist/index.js audit` on this repo) — + **0 findings**, unchanged by the fix (confirms nothing regressed on the format the + engine already handled). +- Before the fix: `constitution audit` on DSAMind → 1 error (`LEDGER-SYNC`), 13 warnings, + most cascading false positives (`L0-EMPTY` even though DSAMind has a real 4-line + Preamble; 9× `ADR-SERVES-DANGLING` because zero Articles parsed at all). +- After the fix, same repo → **16 errors, 6 warnings, all real**: + - `L0-SIZE` (F-V caps L0 at ≤3; DSAMind has 4 — P1–P3 Mission + P4 Mandate). **Not an + engine bug** — checked framework's own `Article F-V` fitness text + (`CONSTITUTION.md:89`, "L0 holds ≤3 statements") and it has no Mission/Mandate + carve-out, even though the framework's own ledger ([0.5.0]) later adopted the + Mission/Mandate split as a first-class L0 shape. This is a genuine above-firewall + open question for the ratifier — does F-V need an amendment for the two-part shape it + already adopted, or should the Mission+Mandate total stay ≤3? **Flagged, not + resolved** — no amendment drafted this session. + - 5× `ART-MECH-DEBT` (A4, B1, B2, C1, D2 = `HOLDS`+`UNGUARDED`) — **matches DSAMind's + own hand-audited "mechanization backlog" list in its Amendments Ledger exactly**, an + independent cross-check that the engine's derivation logic is sound. + - `L0-UNSERVED` for P2 — genuine: no Article's `Serves` cites P2 anywhere in the document + (checked by hand). + - 15× `LOCK-UNACCEPTED` — genuine: DSAMind's `constitution.lock.json` exists + (`acceptedBy: Chinmay`, timestamp today) but has `units: {}` — accepted before any + units existed, or accepted against a version the parser couldn't see yet. Needs a + fresh `constitution lock accept` now that parsing surfaces the real units. + +### Still open after session 3 +- **DSAMind's own `constitution lock accept`** — operator's act (F-IV), now meaningful + since parsing works. Also `npm install` in `dsa_project` (or a fresh publish pull) to + pick up the fixed engine — verification above used the worktree's local `dist/`, not + DSAMind's installed `node_modules` copy. +- **The F-V / Mission-Mandate `L0-SIZE` question** — needs the ratifier's call: amend F-V's + fitness signal, or trim DSAMind's L0. Either path goes through `propose-amendment` + + `ratify-amendment`; not started. +- DSAMind's own below-firewall findings (P2 unserved, mechanization backlog) are + candidates for `constitution doctor` / manual statute-writing in that repo, but that's + DSAMind's own governance work, not this framework's. +- This session's fixes are uncommitted in this worktree — not yet published. diff --git a/cli/package-lock.json b/cli/package-lock.json index 5267fc9..bd0f7cf 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@chinmaygit/constitution-cli", - "version": "0.16.12", + "version": "0.17.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@chinmaygit/constitution-cli", - "version": "0.16.12", + "version": "0.17.2", "license": "MIT", "dependencies": { "prompts": "^2.4.2" diff --git a/cli/src/engine/audit.ts b/cli/src/engine/audit.ts index e140f70..4c8de97 100644 --- a/cli/src/engine/audit.ts +++ b/cli/src/engine/audit.ts @@ -54,7 +54,10 @@ export function audit(instance: Instance): Finding[] { f.push({ code: 'ART-DUP', severity: 'error', firewall: 'below', where, message: `Article ${a.id} appears twice (F-II: one home per rule)` }); seen.add(a.id); - if (!ARTICLE_STATUS.includes(a.status)) + // `SUPERSEDED — ` (the amendment lifecycle's own supersession shape, + // e.g. from `ratify-amendment`) carries a date suffix — still a valid status. + const statusOk = ARTICLE_STATUS.includes(a.status) || /^SUPERSEDED\s+—\s+.+$/.test(a.status); + if (!statusOk) f.push({ code: 'ART-STATUS', severity: 'error', firewall: 'above', where, message: `Article ${a.id}: status "${a.status}" is not ${ARTICLE_STATUS.join('|')}` }); if (!CONFORMANCE.includes(a.conformance)) f.push({ code: 'ART-CONF', severity: 'error', firewall: 'below', where, message: `Article ${a.id}: conformance "${a.conformance}" is not ${CONFORMANCE.join('|')}` }); @@ -80,8 +83,13 @@ export function audit(instance: Instance): Finding[] { f.push({ code: 'L0-UNSERVED', severity: 'warn', firewall: 'above', where: `${rel}:${p.line}`, message: `${p.id} is served by no Article — the vision line is unenforced` }); } - // -- ledger ----------------------------------------------------------------- - if (doc.ledger.length > 0 && doc.version && doc.ledger[0].version !== doc.version) + // -- ledger ------------------------------------------------------------------- + // Only the framework's own self-hosted repo pins itself in the header — there + // the header version and the ledger's own version axis are the same number. + // A downstream consumer's header pins the *framework spec* it adopted, while + // its ledger tracks the *product's own* constitution version; those are two + // legitimately independent axes and must not be compared. + if (doc.selfHosted && doc.ledger.length > 0 && doc.version && doc.ledger[0].version !== doc.version) f.push({ code: 'LEDGER-SYNC', severity: 'error', firewall: 'below', where: `${rel}:${doc.ledger[0].line}`, message: `header pins ${doc.version} but the newest ledger entry is [${doc.ledger[0].version}] — one number for the whole repo` }); // -- governance map ----------------------------------------------------------- diff --git a/cli/src/engine/parse.ts b/cli/src/engine/parse.ts index ca4d1a6..5754c85 100644 --- a/cli/src/engine/parse.ts +++ b/cli/src/engine/parse.ts @@ -85,10 +85,13 @@ function parsePreamble(lines: string[], notes: string[]): PreambleLine[] { const out: PreambleLine[] = []; let current: { id: string; text: string[]; line: number } | null = null; for (let i = start + 1; i < end; i++) { - const m = lines[i].match(/^\*\*(P\d+)\.\*\*\s*(.*)$/); + // `**P1.** text` or `**P1 — Title.** text` — the title (if any) is part of + // the statement, not separate content. + const m = lines[i].match(/^\*\*(P\d+)(?:\s+—\s+([^*]*))?\.\*\*\s*(.*)$/); if (m) { if (current) out.push(finishPreamble(current)); - current = { id: m[1], text: [m[2]], line: i + 1 }; + const title = m[2] ? `${m[2]}.` : ''; + current = { id: m[1], text: [title, m[3]].filter(Boolean), line: i + 1 }; } else if (current) { if (lines[i].trim() === '' || /^#|^---/.test(lines[i])) { out.push(finishPreamble(current)); @@ -110,7 +113,9 @@ function finishPreamble(c: { id: string; text: string[]; line: number }): Preamb function parseArticles(lines: string[], notes: string[]): Article[] { const out: Article[] = []; for (let i = 0; i < lines.length; i++) { - const m = lines[i].match(/^### Article\s+(\S+)\s+—\s+(.+)$/); + // `### Article A1 — Name` at top level, or `#### Article A1 — Name` when + // Articles are grouped under `### §
` domain headings. + const m = lines[i].match(/^#{3,4} Article\s+(\S+)\s+—\s+(.+)$/); if (!m) continue; const id = m[1]; const name = m[2].trim(); @@ -132,16 +137,19 @@ function parseArticles(lines: string[], notes: string[]): Article[] { // Bullets until the next heading. let end = lines.length; for (let j = i + 1; j < lines.length; j++) { - if (/^#{2,3} /.test(lines[j]) || /^---\s*$/.test(lines[j])) { + if (/^#{2,4} /.test(lines[j]) || /^---\s*$/.test(lines[j])) { end = j; break; } } const bullets = parseBoldBullets(lines.slice(i + 1, end)); const servesRaw = bullets['Serves'] ?? ''; + // Strip parenthetical commentary first — "P1 (fluency is ..., the unit ...)" + // has its own commas that would otherwise fragment the split below. const serves = servesRaw + .replace(/\([^)]*\)/g, '') .split(/[,;]|\band\b/) - .map((s) => s.trim().replace(/\.$/, '')) + .map((s) => s.trim().replace(/\.$/, '').trim()) .filter((s) => /^P\d+$/.test(s)); if (servesRaw && serves.length === 0) notes.push(`Article ${id}: Serves ("${servesRaw}") names no P id`); diff --git a/cli/test/engine.test.ts b/cli/test/engine.test.ts index 408b919..978d404 100644 --- a/cli/test/engine.test.ts +++ b/cli/test/engine.test.ts @@ -50,6 +50,76 @@ describe('parse', () => { }); }); +describe('parse: real-world format variance (found dogfooding against DSAMind)', () => { + it('parses titled P-lines, section-grouped #### Articles, Serves parentheticals, and dated SUPERSEDED status', () => { + const dir = makeInstanceDir({ + constitution: () => `# Acme Constitution + +\`\`\` +framework: constitution@1.2.3 +ratifier: Ada Lovelace +\`\`\` + +## L0 — Preamble (vision) + +**P1 — Fluency, not coverage.** +Acme exists to make widgets trustworthy. + +**P2 — In service of the interview.** +Widgets ship only when proven to work. + +## L1 — Articles + +### §A — Widget model +_Serves P1, P2._ + +#### Article A1 — Widgets are verified +\`status: RATIFIED\` · \`conformance: HOLDS\` · \`enforcement: GATED\` · \`party: User\` + +- **Principle** — Every widget passes verification before it ships. +- **Serves** — P1 (fluency is measured, not assumed), P2 (the interview is the point). +- **Fitness** — CI runs the verify suite on every widget build. + +#### Article A2 — Legacy widget rule +\`status: SUPERSEDED — 2026-07-02\` · \`conformance: HOLDS\` · \`enforcement: UNGUARDED\` · \`party: User\` + +- **Principle** — Widgets used to be hand-checked. +- **Serves** — P2 (grow). +- **Fitness** — n/a — superseded. + +--- + +## Amendments Ledger + +### [1.2.3] — 2026-07-01 — founding ratification +- Founding entry. Ratifier: Ada Lovelace. +`, + }); + const inst = loadInstance(dir); + expect(inst.constitution.preamble.map((p) => p.id)).toEqual(['P1', 'P2']); + expect(inst.constitution.preamble[0].text).toContain('Fluency, not coverage'); + expect(inst.constitution.preamble[0].text).toContain('trustworthy'); + expect(inst.constitution.articles.map((a) => a.id)).toEqual(['A1', 'A2']); + expect(inst.constitution.articles[0].serves).toEqual(['P1', 'P2']); + expect(inst.constitution.articles[1].serves).toEqual(['P2']); + expect(inst.constitution.articles[1].status).toBe('SUPERSEDED — 2026-07-02'); + + const findings = audit(inst); + expect(findings.map((f) => f.code)).not.toContain('L0-EMPTY'); + expect(findings.map((f) => f.code)).not.toContain('ART-STATUS'); + expect(findings.map((f) => f.code)).not.toContain('ART-SERVES'); + expect(findings.map((f) => f.code)).not.toContain('PARSE'); + }); + + it('does not apply LEDGER-SYNC to a non-self-hosted instance whose own ledger version differs from the framework pin', () => { + const dir = makeInstanceDir({ + constitution: (s) => s.replace('### [1.2.3] — 2026-07-01', '### [0.5.0] — 2026-07-01'), + }); + const findings = audit(loadInstance(dir)); + expect(findings.map((f) => f.code)).not.toContain('LEDGER-SYNC'); + }); +}); + describe('canonical hashing', () => { it('is stable under reflow but not under wording changes', () => { const a = 'The quick brown fox\njumps over the lazy dog.'; From 14291d20522aeb85318974ca67ebe12eca0cf5bd Mon Sep 17 00:00:00 2001 From: Chinmay <4730291+chinmaygit@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:43:19 +0200 Subject: [PATCH 2/5] fix(scaffold): re-running init is now actually safe on an existing consumer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found trying to upgrade DSAMind (scaffolded before this overhaul, compiled skills stuck at pre-engine versions). setupAgents already unconditionally refreshes every compiled SKILL.md, so re-running `init` looked like the right update mechanism — but two bugs made it unsafe: - The "does AGENTS.md already have a governance map" check was case-sensitive (`includes('Governance Map')`), while the engine's own detection (engine/parse.ts) is case-insensitive. A consumer's real heading ("## Governance map (entry point)") would read as absent, and init would append a duplicate generic block on top of an already-customized map. - scaffold.ts wrote a blanket `.constitution/` line to the root .gitignore. events.ts's ensureOps writes its own nested .constitution/.gitignore specifically so events.jsonl/proposals stay committed while tone/compiles/board.html regenerate — a parent-directory ignore rule makes git never even look for that narrower exception (verified empirically), silently swallowing the delivery record and ratification queue the ops-plane design explicitly means to keep. Scoped the ignore to just the regenerable vendored copies (templates/, process/). Verified: dry-run init against a full copy of DSAMind leaves CONSTITUTION.md/ AGENTS.md byte-identical and only rewrites the two skills that actually changed upstream; a fresh-consumer dry run confirms events.jsonl is stageable post-init. 25/25 tests (2 new). Co-Authored-By: Claude Sonnet 5 --- BUILDLOG.md | 66 +++++++++++++++++++++++++++++++++++++++++ cli/src/scaffold.ts | 22 ++++++++++---- cli/test/engine.test.ts | 34 +++++++++++++++++++++ 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/BUILDLOG.md b/BUILDLOG.md index 7850289..ea1715c 100644 --- a/BUILDLOG.md +++ b/BUILDLOG.md @@ -268,3 +268,69 @@ audit bugs in `cli/src/engine/{parse,audit}.ts` (tests added, `cli/test/engine.t candidates for `constitution doctor` / manual statute-writing in that repo, but that's DSAMind's own governance work, not this framework's. - This session's fixes are uncommitted in this worktree — not yet published. + +### Addendum — upgrading an existing consumer, found by trying it on DSAMind (same session) +Operator asked how to bring an old consumer (DSAMind: scaffolded 2026-07-01, before this +overhaul existed) up to date — compiled skills stuck at pre-engine versions +(`audit-structure` 1.3.2, not the engine-consuming 1.4.0), no CI firewall gate, no +pre-commit hook, `@chinmaygit/constitution-cli` present in `node_modules` but **not +declared** in `package.json`/lockfile (an unreproducible local artifact — absent on any +fresh clone or in CI). Investigated whether `constitution init` (re-run non-interactively) +is a safe update mechanism, since `setupAgents` already unconditionally overwrites every +compiled `SKILL.md` — and found two real bugs that would have made it unsafe: + +1. **`scaffold.ts`'s "does AGENTS.md already have a governance map" check was + case-sensitive** (`includes('Governance Map')`) while the engine's own detection + (`engine/parse.ts`) is case-insensitive (`/Governance Map/i`). DSAMind's actual heading + is `## Governance map (entry point)` (lowercase "map") — re-running `init` would have + silently appended a second, generic governance-map block on top of DSAMind's + already-customized one. Fixed to match the engine's own case-insensitive check. +2. **`scaffold.ts` wrote a blanket `.constitution/` line to the consumer's root + `.gitignore`.** `engine/events.ts`'s `ensureOps` also writes its own nested + `.constitution/.gitignore` (`tone/`, `compiles/`, `board.html`) specifically so + `events.jsonl` and `proposals/` stay committed — "the delivery record and the + ratification queue are worth committing," per its own comment. Verified empirically + (a throwaway git repo) that a parent-directory ignore makes git never even look for a + nested un-ignore: the blanket rule silently swallowed `events.jsonl` and + `proposals/*.json` too, contradicting the ops-plane's explicit design and — for + DSAMind specifically, which already has `.constitution/process`/`templates` **committed** + from its original init — adding a rule that (harmlessly, since git doesn't retroactively + untrack) would have gone on to block any future ops-plane commits. Fixed: the ignore is + now scoped to just `.constitution/templates/` and `.constitution/process/` (the + regenerable vendored copies), leaving the ops subdirectories to the nested + `.gitignore`'s finer-grained rules. + +**Verified by running:** dry-run `init` against a full copy of DSAMind (scratch dir, not +the real repo) — CONSTITUTION.md/AGENTS.md byte-identical before/after, only +`audit-structure`/`compile-prompt` (the two skills that changed upstream) rewritten in +`.claude/skills/` + `.agents/skills/` to 1.4.0/1.2.0, the other 9 untouched. Separately, a +fresh-consumer dry run (`git init` + `constitution init` + `feature declare` + `git add -A`) +confirms `.constitution/events.jsonl` is now stageable while `templates/`/`process/`/ +`tone/`/`compiles/`/`board.html` stay ignored. Two new tests +(scaffold `.gitignore` scoping; case-insensitive governance-map re-init) — **25/25** total. + +**The upgrade runbook for an existing consumer** (DSAMind or any pre-overhaul repo), +in order: +1. `npm install --save-dev @chinmaygit/constitution-cli@latest` in the consumer repo + (DSAMind currently has it in `node_modules/` but undeclared — not reproducible on a + fresh clone or in CI). +2. `constitution init --name --ratifier --agents claude,antigravity` + (non-interactive, safe to re-run) — refreshes compiled skills only; CONSTITUTION.md/ + AGENTS.md are left alone if already present. +3. `constitution doctor` — self-heals below-firewall findings, queues above-firewall ones + as proposals. +4. `constitution hooks install` — arms the pre-commit audit+firewall gate. +5. Add a CI workflow calling `constitution audit` + `constitution firewall` (DSAMind has + `ci.yml`/`version-changelog.yml` but no governance gate yet — port + `.github/workflows/governance.yml` from this repo). +6. **Operator-only, F-IV:** `constitution lock accept` — DSAMind's current + `constitution.lock.json` has `acceptedBy: Chinmay` but `units: {}` (accepted before + parsing worked), so every RATIFIED unit currently reads `LOCK-UNACCEPTED`. +7. Bump DSAMind's `framework: constitution@0.16.12` header pin to the installed version + once steps 1–6 land — not done automatically; no L0/L1 *schema* changed between + 0.16.12 and 0.17.2 (this overhaul added CLI tooling, not new Article fields), so the + bump is a formality once the tooling catches up, but it's still a header edit inside + the ratified document and should be a deliberate, reviewed step, not silent. +Steps 2–5 are mechanical/below-firewall; 6–7 are the ratifier's own act. **Not yet run +against the real `dsa_project` repo this session** — only against scratch copies, pending +the operator's go-ahead to touch a live product repo. diff --git a/cli/src/scaffold.ts b/cli/src/scaffold.ts index 44c6c4f..85b8fbd 100644 --- a/cli/src/scaffold.ts +++ b/cli/src/scaffold.ts @@ -29,15 +29,23 @@ export async function scaffoldFramework(targetDir: string, projectName: string, } } - // Update .gitignore + // Update .gitignore — only the regenerable vendored copies (re-written by + // every `init`/`doctor` from the installed package), never the whole + // `.constitution/` tree: `engine/events.ts`'s `ensureOps` writes its own + // nested `.constitution/.gitignore` that keeps `events.jsonl` and + // `proposals/` committed (the delivery record and ratification queue are + // meant to be tracked) while ignoring `tone/`/`compiles/`/`board.html`. A + // blanket `.constitution/` entry here would silently shadow that — once a + // parent directory is ignored, git won't look inside for a narrower rule. const gitignorePath = path.join(targetDir, '.gitignore'); + const vendoredIgnore = '.constitution/templates/\n.constitution/process/\n'; if (fs.existsSync(gitignorePath)) { const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8'); - if (!gitignoreContent.includes('.constitution/')) { - fs.appendFileSync(gitignorePath, '\n# Framework namespace\n.constitution/\n', 'utf-8'); + if (!gitignoreContent.includes('.constitution/templates/')) { + fs.appendFileSync(gitignorePath, `\n# Framework namespace (vendored copies only; see .constitution/.gitignore for ops-plane rules)\n${vendoredIgnore}`, 'utf-8'); } } else { - fs.writeFileSync(gitignorePath, '# Framework namespace\n.constitution/\n', 'utf-8'); + fs.writeFileSync(gitignorePath, `# Framework namespace (vendored copies only; see .constitution/.gitignore for ops-plane rules)\n${vendoredIgnore}`, 'utf-8'); } // Initialize CONSTITUTION.md from templates/constitution.md -- never hand-write the @@ -76,7 +84,11 @@ export async function scaffoldFramework(targetDir: string, projectName: string, } else { console.log('AGENTS.md already exists, safe-appending governance map block...'); const existingContent = fs.readFileSync(agentsPath, 'utf-8'); - if (!existingContent.includes('Governance Map')) { + // Case-insensitive, matching how the engine itself detects a governance map + // (engine/parse.ts) -- a consumer's own map ("## Governance map (entry + // point)") must count as present, or a re-run of `init` (the update path) + // would append a duplicate generic block onto an already-customized map. + if (!/Governance Map/i.test(existingContent)) { fs.appendFileSync(agentsPath, `\n${agentMapBlock}`, 'utf-8'); } else { console.log('Governance map block already present in AGENTS.md, skipping append.'); diff --git a/cli/test/engine.test.ts b/cli/test/engine.test.ts index 978d404..1661647 100644 --- a/cli/test/engine.test.ts +++ b/cli/test/engine.test.ts @@ -11,6 +11,9 @@ import { renderUnit, checkTones, pruneStaleTones } from '../src/engine/tone'; import { buildCompilePack } from '../src/engine/compile'; import { queueProposal, listProposals, recordRuling, hasOpenProposalFor } from '../src/engine/proposals'; import { runDoctor } from '../src/engine/doctor'; +import { scaffoldFramework } from '../src/scaffold'; +import { ensureOps } from '../src/engine/events'; +import * as os from 'os'; describe('parse', () => { it('reads header, preamble, articles, statutes, adrs, ledger', () => { @@ -120,6 +123,37 @@ _Serves P1, P2._ }); }); +describe('scaffold: .gitignore does not shadow the ops-plane\'s own rules', () => { + it('ignores only the regenerable vendored copies, leaving events.jsonl/proposals trackable', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'constitution-scaffold-')); + await scaffoldFramework(dir, 'Acme', 'Ada'); + ensureOps(dir); + const gitignore = fs.readFileSync(path.join(dir, '.gitignore'), 'utf8'); + expect(gitignore).toContain('.constitution/templates/'); + expect(gitignore).toContain('.constitution/process/'); + expect(gitignore).not.toMatch(/^\.constitution\/\s*$/m); + + // Simulate git's own ignore-matching: a blanket `.constitution/` entry + // would make this file unreachable regardless of the nested .gitignore. + const rules = gitignore.split('\n').filter(Boolean).filter((l) => !l.startsWith('#')); + const ignoredAsDir = rules.some((r) => r === '.constitution/' || r === '.constitution'); + expect(ignoredAsDir).toBe(false); + + const opsIgnore = fs.readFileSync(path.join(dir, '.constitution', '.gitignore'), 'utf8'); + expect(opsIgnore).toContain('tone/'); + expect(opsIgnore).toContain('compiles/'); + expect(opsIgnore).not.toContain('events.jsonl'); + }); + + it('re-running init on an existing governance map (case-insensitive heading) does not duplicate it', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'constitution-scaffold-')); + fs.writeFileSync(path.join(dir, 'AGENTS.md'), '## Governance map (entry point)\n\nCustom, hand-authored map.\n'); + await scaffoldFramework(dir, 'Acme', 'Ada'); + const content = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8'); + expect(content).toBe('## Governance map (entry point)\n\nCustom, hand-authored map.\n'); + }); +}); + describe('canonical hashing', () => { it('is stable under reflow but not under wording changes', () => { const a = 'The quick brown fox\njumps over the lazy dog.'; From cc902bbd9503f554ad9142dc102f51da6ebf8be0 Mon Sep 17 00:00:00 2001 From: Chinmay <4730291+chinmaygit@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:21:51 +0200 Subject: [PATCH 3/5] propose(F-II): three version axes, three homes (ADR-0002, not ratified) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator asked where the installed framework tooling version should be tracked for a consumer (vs. the adopted-spec header pin vs. the product's own ledger version), and invoked /propose-amendment on the idea. Drafted, not ratified. decisions/0002-version-axis-separation.md proposes extending Article F-II ("one home per rule") to name three distinct version facts and assign each exactly one home: an instance's own Amendments Ledger version; the framework spec version it has deliberately adopted (the CONSTITUTION.md header pin — a ratified conformance claim, never auto-bumped past what's adopted); and the framework tooling actually installed (the consumer's own package manifest, e.g. package.json's @chinmaygit/constitution-cli entry — no new field needed, confirming the operator's instinct). Evidenced by this session's actual LEDGER-SYNC bug (fixed in code, not yet in law). Agreement- only, with the reason on record — no catch-rate/friction hypothesis to measure. Explicitly did not rule on the operator's second idea (skills self-check tooling currency and auto-run migrations): no concrete mechanism exists yet to evaluate, and "auto-update tooling unattended" is a supply-chain-trust question separate from version-tracking. Recorded as deferred follow-up engineering work in the ADR's Consequences, not folded into this amendment. CONSTITUTION.md itself is untouched — the drafted F-II text lives only in the ADR; only ratify-amendment may write it into ratified law. decisions/ INDEX.md updated in the same change per decisions/AGENTS.md's own statute. constitution audit + firewall both stay clean. Co-Authored-By: Claude Sonnet 5 --- BUILDLOG.md | 48 ++++++++ decisions/0002-version-axis-separation.md | 131 ++++++++++++++++++++++ decisions/INDEX.md | 1 + 3 files changed, 180 insertions(+) create mode 100644 decisions/0002-version-axis-separation.md diff --git a/BUILDLOG.md b/BUILDLOG.md index ea1715c..a278066 100644 --- a/BUILDLOG.md +++ b/BUILDLOG.md @@ -334,3 +334,51 @@ in order: Steps 2–5 are mechanical/below-firewall; 6–7 are the ratifier's own act. **Not yet run against the real `dsa_project` repo this session** — only against scratch copies, pending the operator's go-ahead to touch a live product repo. + +### Addendum — proposed amendment: three version axes, three homes (same session) + +Operator asked, given the LEDGER-SYNC fix's two-axis distinction: where should the +*installed tooling* version live (they suggested `package.json`), and floated skills +self-checking + auto-migrating tooling on every run — then explicitly invoked +`/propose-amendment`. Ran that skill end to end; drafted, did **not** ratify: + +- **Ran the L1 inclusion test** on the candidate before drafting: general, traces to L0 + (P1, via F-II), falsifiable, survives a tech swap — passes. Considered a brand-new + Article ("F-VIII") vs. extending F-II; chose **extending F-II** (already the "one home + per rule" Article, already amended once by ADR-0001 for the closely related + package-managed-distribution question) — flagged in the ADR's "Alternatives considered" + that if the ratifier disagrees this is a fitting extension rather than genuinely new + territory, the correct redirect is `harvest-articles`, not a bigger amendment here. +- **decisions/0002-version-axis-separation.md** (`status: proposed`, `serves`/`amends: + [F-II]`, `trigger: certiorari`) — names the three axes and gives each one home: an + instance's own Amendments Ledger version; the adopted-spec `framework:` header pin + (ratified conformance claim, never auto-bumped past what's adopted); the installed + tooling version (consumer's own package manifest — confirming the operator's + `package.json` instinct, since that's already the natural, existing home for "what's + installed," no new field needed). Drafted F-II `Principle`/`Fitness` additions + (additive only — nothing superseded, no forward-link needed) plus `Why`/`Proven` + bullets citing this session's actual LEDGER-SYNC bug as the evidence. `decisions/ + INDEX.md` updated in the same change per `decisions/AGENTS.md`'s own statute. + **CONSTITUTION.md itself was not touched** — the drafted text lives only in the ADR, + per the firewall; only `ratify-amendment` may write it into ratified law. +- **Agreement-only, not measured** — explicit reason on record (per the skill's hard + rule against silently skipping the experiment path): this clarifies already-adopted + practice (`sync-operator`'s existing "never bump a consumer's pin past what it has + adopted") and is evidenced by a bug already found and fixed in code this session; + there's no catch-rate/friction hypothesis to pre-register. +- **Explicitly split off, not ruled on**: the "skills self-check tooling + auto-migrate" + half of the operator's proposal. No mechanism exists yet to evaluate — open questions + recorded in the ADR's Consequences: what "check is current" means without a silent + network call on every skill invocation; whether `doctor` should only *report* tooling + drift (consistent with existing propose-don't-enact discipline) or actually run `npm + update` unattended (a supply-chain-trust question, separate from version-tracking); and + where per-version migration steps would live (a `migrations/` dir keyed by version + range, run from `constitution doctor`, is the natural extension of doctor's existing + `versionSync` mechanism — unbuilt). Once a concrete mechanism exists, *that's* a genuine + F-III pre-registration candidate; this ADR is not it. +- Verified: `constitution audit` and `constitution firewall` both stay clean with the new + ADR present (L3, outside the lock) — confirms the proposal touched nothing above the + firewall. +- **Open**: awaiting the ratifier's decision (`ratify-amendment`) — accept the F-II + extension as drafted, request changes, or redirect to `harvest-articles` if a standalone + Article is preferred after all. diff --git a/decisions/0002-version-axis-separation.md b/decisions/0002-version-axis-separation.md new file mode 100644 index 0000000..82fdffa --- /dev/null +++ b/decisions/0002-version-axis-separation.md @@ -0,0 +1,131 @@ +--- +id: 0002 +title: Three version axes, three homes — ledger, adopted-spec pin, installed tooling +status: proposed +date: 2026-07-05 +supersedes: [] +superseded_by: [] +serves: [F-II] +amends: [F-II] +trigger: certiorari +--- + +## Question of law +F-II requires "one home per rule," and already names one worked example (a versioned +package manager installation satisfies it — ADR-0001). Auditing DSAMind (`dsa_project`) +this session surfaced a gap ADR-0001 didn't cover: a *version number* is itself a governed +fact, and this framework was conflating three distinct ones under an implicit assumption +that they're all "the same number": + +1. An instance's **own Amendments Ledger version** (DSAMind's is at `[0.11.0]`) — the + product's own constitution's version, incrementing with its own amendments. +2. The **framework spec version it has deliberately adopted** — `CONSTITUTION.md`'s header + `framework: constitution@X.Y.Z` pin. A ratified conformance claim: which spec features + (e.g. the `enforcement` axis) the instance has actually adopted. Deliberately allowed to + lag — never auto-bumped past what's adopted (this is already the operative convention, + stated in the `sync-operator` skill, but was never codified in L1 until now). +3. The **framework tooling actually installed** — for a consumer, the + `@chinmaygit/constitution-cli` version in its own package manifest (`package.json`); for + this repo, its own `cli/package.json` (already kept in lockstep via `versionSync`, + because this repo is `self-hosted` — axes 1 and 2 and 3 collapse to one number here by + construction, which is precisely what made the conflation invisible until a real + consumer surfaced it). + +The engine's `LEDGER-SYNC` check compared axis 2 against axis 1 unconditionally, so it flagged +DSAMind's honest, correct state (pin `0.16.12`, ledger `[0.11.0]` — two different, both-valid +numbers) as an error. Fixed in code (`cli/src/engine/audit.ts`, gated on `doc.selfHosted`) the +same day this ADR was drafted, but the fix is a mechanical patch until F-II's principle +actually says these are three homes, not one — otherwise the same conflation resurfaces the +next time someone builds against this framework without having read the commit that fixed it. + +A second, related question was raised in the same discussion but is **not** ruled on here: +should skill invocations or `constitution doctor` automatically check whether an instance's +installed tooling (axis 3) is current, and run a migration for a below-firewall scaffold +change when it isn't? See "Consequences — deferred" below; no mechanism exists yet to rule on. + +## Ruling +Amend Article F-II's `Principle` and `Fitness` to name all three version axes and assign each +exactly one home, so that no engine check, skill, or human ever again compares two of them as +if they were one. Drafted text (the change is additive — nothing existing is retired, so +nothing is superseded): + +**Principle — append:** +> A version number is itself a governed fact, and this rule applies to it as much as to any +> other: an instance's own Amendments Ledger version, the framework spec version it has +> deliberately adopted (the `CONSTITUTION.md` header pin — a ratified conformance claim, +> never auto-bumped past what the instance has actually adopted), and the framework tooling +> version actually installed (the instance's own package manifest, e.g. `package.json`) are +> three distinct facts. Each has exactly one home. A self-hosted instance is the one case +> where axes legitimately collapse to a single number by construction (the framework pins +> itself) — everywhere else, all three are read independently and never compared to each +> other as if drift between them were an error. + +**Fitness — append:** +> An instance's ledger version, its header framework-pin, and its installed tooling version +> are never checked against each other as if they were one axis, except for a `self-hosted` +> instance (where the pin and the ledger are the same number by construction, and tooling +> version-sync is a declared, explicit target — see `constitution.config.json`'s +> `versionSync`). Verified by the `audit-structure` skill and mechanically by `constitution +> audit`'s `LEDGER-SYNC` check. + +**Why — new bullet:** +> Conflating these axes produces exactly the false alarm this framework shipped once: +> comparing a consumer's adopted-spec pin to its own ledger version as if the numbers should +> match, when a downstream product's ledger legitimately runs on its own numbering +> independent of which framework spec version it targets. Naming three homes instead of one +> silent, assumed one keeps each fact honest about what it actually claims — and keeps the +> next engine check from re-deriving the same bug by hand. + +**Proven — new bullet:** +> Found 2026-07-04/05 auditing DSAMind (`dsa_project`): `LEDGER-SYNC` compared its header pin +> (`constitution@0.16.12`, an adopted-spec claim) against its own Amendments Ledger's newest +> entry (`[0.11.0]`, DSAMind's own document version) and flagged two legitimately independent, +> correct values as an error. Fixed in code same-day (`cli/src/engine/audit.ts`, gated on the +> already-parsed-but-previously-unused `doc.selfHosted` field); this amendment codifies the +> distinction in law so the next engine change can't silently re-conflate it. + +## Constitutional impact +Extends Article F-II (`serves: [F-II]`, `amends: [F-II]`). Purely additive — the existing +principle ("every governed rule lives in exactly one layer... never duplicated") and fitness +are unchanged and remain true; this names a new instance of the same rule (a version number is +a rule too) that wasn't explicit before. Nothing is superseded; no forward-link note is needed. + +## Consequences +- **Positive** — closes the exact gap that produced a real false-positive finding this + session; gives the engine, skills, and any future contributor one place (F-II) to check + before writing a new version-comparison. Registry.md (F-I's fitness signal) should gain a + row once ratified, citing this ADR as the DSAMind-proven mechanism. +- **Negative / accepted trade-off** — none identified; this narrows an ambiguity, it doesn't + add new obligations to any consumer (no consumer's `CONSTITUTION.md` needs to change). +- **Deferred, not ruled on here** — the operator separately proposed that skill invocations + (or `constitution doctor`) should check axis 3 (installed tooling) is current and run a + migration script for below-firewall scaffold changes "when necessary." Not included in this + ruling because no concrete mechanism exists yet to evaluate: open questions include (a) + what "check is current" means without a silent network call on every skill run, (b) whether + `doctor` should merely *report* tooling drift (consistent with this framework's existing + "propose, don't enact" discipline for anything above the firewall) or actually invoke `npm + install`/`npm update` unattended (a supply-chain-trust question, not a version-tracking one), + and (c) where per-version migration steps would live (a `migrations/` directory keyed by + version range, run from `constitution doctor`, is the natural extension of doctor's existing + `versionSync` mechanism, but is unbuilt). This is follow-up engineering work, not an + amendment — once a mechanism exists, its catch-rate/friction is a genuine hypothesis and + belongs in a pre-registered experiment (F-III), not agreement-only like this ADR. + +## Alternatives considered +- **Fold this into F-VII instead of F-II.** Rejected — F-VII governs L2 statute discipline + (operational/craft rules implementing the stack); a version-tracking invariant is about the + framework's own governed facts, squarely F-II's "one home" territory, not an L2 concern. +- **A brand-new Article (e.g. "F-VIII — version axes").** Considered, since the three-axis + distinction is a self-contained invariant. Rejected in favor of extending F-II: it already + covers "one home per rule" in general and was already amended once (ADR-0001) for the + closely related "package-managed distribution" question; a second narrow Article would + fragment one concern across two homes — the opposite of what F-II itself is for. (Flagged + to the ratifier: if this reasoning doesn't hold, the right redirect is `harvest-articles`, + not a bigger amendment here — this skill doesn't harvest brand-new Articles from scratch.) +- **No agreement-only path; pre-register an experiment instead.** Rejected for *this* ADR's + scope (naming the three axes and stopping the false-positive) — there's no catch-rate or + friction-cost hypothesis to measure; it's a factual clarification of what already-adopted + practice (`sync-operator`'s "never bump a consumer's pin past what it has adopted") actually + means, evidenced by a bug this session already found and fixed in code. The deferred + auto-check/auto-migrate proposal is exactly the kind of candidate that *would* need + measurement, once it has a concrete mechanism to measure — see Consequences above. diff --git a/decisions/INDEX.md b/decisions/INDEX.md index c87af62..01c3758 100644 --- a/decisions/INDEX.md +++ b/decisions/INDEX.md @@ -6,3 +6,4 @@ an ADR) are L2 statutes — see [AGENTS.md](AGENTS.md). | id | title | status | date | |----|-------|--------|------| | 0001 | Allow package-managed installation of skills to satisfy F-II | accepted | 2026-07-01 | +| 0002 | Three version axes, three homes — ledger, adopted-spec pin, installed tooling | proposed | 2026-07-05 | From aabad615f04b209603d6f81758de6a81ba788dd6 Mon Sep 17 00:00:00 2001 From: Chinmay <4730291+chinmaygit@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:35:07 +0200 Subject: [PATCH 4/5] docs(adr-0002): simplify wording, same substance Operator feedback: the first draft was too dense to follow. Cut nested parentheticals and legal-brief phrasing; kept the same three-axis ruling, the same evidence, the same deferred item. Co-Authored-By: Claude Sonnet 5 --- decisions/0002-version-axis-separation.md | 142 ++++++---------------- decisions/INDEX.md | 2 +- 2 files changed, 37 insertions(+), 107 deletions(-) diff --git a/decisions/0002-version-axis-separation.md b/decisions/0002-version-axis-separation.md index 82fdffa..533beb2 100644 --- a/decisions/0002-version-axis-separation.md +++ b/decisions/0002-version-axis-separation.md @@ -1,6 +1,6 @@ --- id: 0002 -title: Three version axes, three homes — ledger, adopted-spec pin, installed tooling +title: Three version numbers, three homes status: proposed date: 2026-07-05 supersedes: [] @@ -11,121 +11,51 @@ trigger: certiorari --- ## Question of law -F-II requires "one home per rule," and already names one worked example (a versioned -package manager installation satisfies it — ADR-0001). Auditing DSAMind (`dsa_project`) -this session surfaced a gap ADR-0001 didn't cover: a *version number* is itself a governed -fact, and this framework was conflating three distinct ones under an implicit assumption -that they're all "the same number": +A product has three different version numbers. We were treating two of them as one. -1. An instance's **own Amendments Ledger version** (DSAMind's is at `[0.11.0]`) — the - product's own constitution's version, incrementing with its own amendments. -2. The **framework spec version it has deliberately adopted** — `CONSTITUTION.md`'s header - `framework: constitution@X.Y.Z` pin. A ratified conformance claim: which spec features - (e.g. the `enforcement` axis) the instance has actually adopted. Deliberately allowed to - lag — never auto-bumped past what's adopted (this is already the operative convention, - stated in the `sync-operator` skill, but was never codified in L1 until now). -3. The **framework tooling actually installed** — for a consumer, the - `@chinmaygit/constitution-cli` version in its own package manifest (`package.json`); for - this repo, its own `cli/package.json` (already kept in lockstep via `versionSync`, - because this repo is `self-hosted` — axes 1 and 2 and 3 collapse to one number here by - construction, which is precisely what made the conflation invisible until a real - consumer surfaced it). +1. **Its own constitution version** — the product's Amendments Ledger (DSAMind is at `[0.11.0]`). +2. **The framework spec version it adopted** — the `framework: constitution@X.Y.Z` pin in + its `CONSTITUTION.md` header. Ratified by a human. Allowed to lag behind the latest spec. +3. **The framework tooling it has installed** — the CLI version in its own `package.json`. -The engine's `LEDGER-SYNC` check compared axis 2 against axis 1 unconditionally, so it flagged -DSAMind's honest, correct state (pin `0.16.12`, ledger `[0.11.0]` — two different, both-valid -numbers) as an error. Fixed in code (`cli/src/engine/audit.ts`, gated on `doc.selfHosted`) the -same day this ADR was drafted, but the fix is a mechanical patch until F-II's principle -actually says these are three homes, not one — otherwise the same conflation resurfaces the -next time someone builds against this framework without having read the commit that fixed it. +Today's bug: the engine's `LEDGER-SYNC` check compared #2 to #1 and treated any mismatch as +an error. But #1 and #2 don't have to match — a product's own version number has nothing to +do with which framework spec it adopted. Already fixed in code +(`cli/src/engine/audit.ts`). This ADR writes the rule into F-II so it isn't lost. -A second, related question was raised in the same discussion but is **not** ruled on here: -should skill invocations or `constitution doctor` automatically check whether an instance's -installed tooling (axis 3) is current, and run a migration for a below-firewall scaffold -change when it isn't? See "Consequences — deferred" below; no mechanism exists yet to rule on. +Not decided here: should skills auto-check that #3 is current, and auto-run a migration when +it isn't? No such mechanism exists yet, so there's nothing to rule on. See Consequences. ## Ruling -Amend Article F-II's `Principle` and `Fitness` to name all three version axes and assign each -exactly one home, so that no engine check, skill, or human ever again compares two of them as -if they were one. Drafted text (the change is additive — nothing existing is retired, so -nothing is superseded): +Extend Article F-II ("one home per rule"): a version number is a rule too, and each of the +three numbers above gets exactly one home. Nothing existing changes — this only adds text. -**Principle — append:** -> A version number is itself a governed fact, and this rule applies to it as much as to any -> other: an instance's own Amendments Ledger version, the framework spec version it has -> deliberately adopted (the `CONSTITUTION.md` header pin — a ratified conformance claim, -> never auto-bumped past what the instance has actually adopted), and the framework tooling -> version actually installed (the instance's own package manifest, e.g. `package.json`) are -> three distinct facts. Each has exactly one home. A self-hosted instance is the one case -> where axes legitimately collapse to a single number by construction (the framework pins -> itself) — everywhere else, all three are read independently and never compared to each -> other as if drift between them were an error. +**Add to Principle:** +> A version number is a governed fact. Three exist, each with one home: the instance's own +> Amendments Ledger version, the framework spec version it has adopted (the header pin — a +> ratified claim, never bumped ahead of what's actually adopted), and the framework tooling +> installed (the instance's own package manifest). These are never compared to each other as +> if they were the same number — except in this framework's own repo, where the pin and the +> ledger are the same number on purpose. -**Fitness — append:** -> An instance's ledger version, its header framework-pin, and its installed tooling version -> are never checked against each other as if they were one axis, except for a `self-hosted` -> instance (where the pin and the ledger are the same number by construction, and tooling -> version-sync is a declared, explicit target — see `constitution.config.json`'s -> `versionSync`). Verified by the `audit-structure` skill and mechanically by `constitution -> audit`'s `LEDGER-SYNC` check. - -**Why — new bullet:** -> Conflating these axes produces exactly the false alarm this framework shipped once: -> comparing a consumer's adopted-spec pin to its own ledger version as if the numbers should -> match, when a downstream product's ledger legitimately runs on its own numbering -> independent of which framework spec version it targets. Naming three homes instead of one -> silent, assumed one keeps each fact honest about what it actually claims — and keeps the -> next engine check from re-deriving the same bug by hand. - -**Proven — new bullet:** -> Found 2026-07-04/05 auditing DSAMind (`dsa_project`): `LEDGER-SYNC` compared its header pin -> (`constitution@0.16.12`, an adopted-spec claim) against its own Amendments Ledger's newest -> entry (`[0.11.0]`, DSAMind's own document version) and flagged two legitimately independent, -> correct values as an error. Fixed in code same-day (`cli/src/engine/audit.ts`, gated on the -> already-parsed-but-previously-unused `doc.selfHosted` field); this amendment codifies the -> distinction in law so the next engine change can't silently re-conflate it. +**Add to Fitness:** +> No check treats the ledger version, the header pin, and the installed tooling version as +> one axis, except for this framework's own self-hosted repo. ## Constitutional impact -Extends Article F-II (`serves: [F-II]`, `amends: [F-II]`). Purely additive — the existing -principle ("every governed rule lives in exactly one layer... never duplicated") and fitness -are unchanged and remain true; this names a new instance of the same rule (a version number is -a rule too) that wasn't explicit before. Nothing is superseded; no forward-link note is needed. +Adds two sentences to F-II's Principle and Fitness. Nothing is removed or superseded. ## Consequences -- **Positive** — closes the exact gap that produced a real false-positive finding this - session; gives the engine, skills, and any future contributor one place (F-II) to check - before writing a new version-comparison. Registry.md (F-I's fitness signal) should gain a - row once ratified, citing this ADR as the DSAMind-proven mechanism. -- **Negative / accepted trade-off** — none identified; this narrows an ambiguity, it doesn't - add new obligations to any consumer (no consumer's `CONSTITUTION.md` needs to change). -- **Deferred, not ruled on here** — the operator separately proposed that skill invocations - (or `constitution doctor`) should check axis 3 (installed tooling) is current and run a - migration script for below-firewall scaffold changes "when necessary." Not included in this - ruling because no concrete mechanism exists yet to evaluate: open questions include (a) - what "check is current" means without a silent network call on every skill run, (b) whether - `doctor` should merely *report* tooling drift (consistent with this framework's existing - "propose, don't enact" discipline for anything above the firewall) or actually invoke `npm - install`/`npm update` unattended (a supply-chain-trust question, not a version-tracking one), - and (c) where per-version migration steps would live (a `migrations/` directory keyed by - version range, run from `constitution doctor`, is the natural extension of doctor's existing - `versionSync` mechanism, but is unbuilt). This is follow-up engineering work, not an - amendment — once a mechanism exists, its catch-rate/friction is a genuine hypothesis and - belongs in a pre-registered experiment (F-III), not agreement-only like this ADR. +- **Positive** — stops today's false alarm (DSAMind's correct version numbers were flagged + as an error) from happening again. +- **Negative** — none. +- **Not decided here** — auto-checking installed tooling and auto-running migrations. No + mechanism exists yet to build on, so it's follow-up engineering work, not part of this + amendment. If a mechanism gets built later, whether it actually helps is worth measuring + then (F-III), not assumed now. ## Alternatives considered -- **Fold this into F-VII instead of F-II.** Rejected — F-VII governs L2 statute discipline - (operational/craft rules implementing the stack); a version-tracking invariant is about the - framework's own governed facts, squarely F-II's "one home" territory, not an L2 concern. -- **A brand-new Article (e.g. "F-VIII — version axes").** Considered, since the three-axis - distinction is a self-contained invariant. Rejected in favor of extending F-II: it already - covers "one home per rule" in general and was already amended once (ADR-0001) for the - closely related "package-managed distribution" question; a second narrow Article would - fragment one concern across two homes — the opposite of what F-II itself is for. (Flagged - to the ratifier: if this reasoning doesn't hold, the right redirect is `harvest-articles`, - not a bigger amendment here — this skill doesn't harvest brand-new Articles from scratch.) -- **No agreement-only path; pre-register an experiment instead.** Rejected for *this* ADR's - scope (naming the three axes and stopping the false-positive) — there's no catch-rate or - friction-cost hypothesis to measure; it's a factual clarification of what already-adopted - practice (`sync-operator`'s "never bump a consumer's pin past what it has adopted") actually - means, evidenced by a bug this session already found and fixed in code. The deferred - auto-check/auto-migrate proposal is exactly the kind of candidate that *would* need - measurement, once it has a concrete mechanism to measure — see Consequences above. +- **New Article instead of extending F-II** — rejected. This is a version of "one home per + rule," which is what F-II already covers. +- **Measure this instead of just ratifying it** — rejected. There's no hypothesis to test, + just a fact to state clearly. diff --git a/decisions/INDEX.md b/decisions/INDEX.md index 01c3758..3cb5a16 100644 --- a/decisions/INDEX.md +++ b/decisions/INDEX.md @@ -6,4 +6,4 @@ an ADR) are L2 statutes — see [AGENTS.md](AGENTS.md). | id | title | status | date | |----|-------|--------|------| | 0001 | Allow package-managed installation of skills to satisfy F-II | accepted | 2026-07-01 | -| 0002 | Three version axes, three homes — ledger, adopted-spec pin, installed tooling | proposed | 2026-07-05 | +| 0002 | Three version numbers, three homes | proposed | 2026-07-05 | From 228211c8e030cf3e5e91bfe2637c5a2fbc2f0153 Mon Sep 17 00:00:00 2001 From: Chinmay <4730291+chinmaygit@users.noreply.github.com> Date: Sun, 5 Jul 2026 10:46:25 +0200 Subject: [PATCH 5/5] ratify(F-II): three version numbers, three homes (ADR-0002) Ratified by Chinmay, 2026-07-05: "makes sense lets ratify this." Extends Article F-II's Principle and Fitness (status: RATIFIED, unchanged) to name three version facts and give each exactly one home: an instance's own Amendments Ledger version, the framework spec version it has adopted (the header pin -- a ratified claim, never bumped ahead of what's adopted), and the framework tooling installed (the instance's own package manifest). Purely additive -- nothing existing changed or was superseded. Agreement-only, no pre-registered experiment -- there was no catch-rate or friction hypothesis to measure. The operator's second idea (skills auto-checking tooling currency and auto-migrating) is explicitly NOT part of this amendment -- no mechanism exists yet to rule on it. ADR-0002 flipped proposed -> accepted; decisions/INDEX.md updated. Version bumped 0.17.2 -> 0.17.3 (header + cli/package.json via constitution doctor's versionSync). Dogfood test's ADR count updated 1 -> 2. constitution.lock.json re-accepted by the ratifier to close the F-IV drift this change opened. Co-Authored-By: Claude Sonnet 5 --- CONSTITUTION.md | 33 +++++++++++++++++++++-- cli/package.json | 2 +- cli/test/engine.test.ts | 2 +- constitution.lock.json | 6 ++--- decisions/0002-version-axis-separation.md | 2 +- decisions/INDEX.md | 2 +- 6 files changed, 38 insertions(+), 9 deletions(-) diff --git a/CONSTITUTION.md b/CONSTITUTION.md index 21b99bb..dbcb552 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -1,7 +1,7 @@ # The constitution framework — Constitution ``` -framework: constitution@0.17.2 (self-hosted) +framework: constitution@0.17.3 (self-hosted) ratifier: Chinmay ``` @@ -52,10 +52,18 @@ Ratification is agreement; conformance is reality; enforcement is reality's half duplicated across layers, nor across the framework and an instance. A versioned package manager installation (e.g., via a CLI) satisfies this rule: the package is the single home, and the files it writes to the instance are its read-only build artifacts. + A version number is a governed fact too. Three exist, each with one home: the instance's + own Amendments Ledger version, the framework spec version it has adopted (the header + pin — a ratified claim, never bumped ahead of what's actually adopted), and the + framework tooling installed (the instance's own package manifest). These are never + compared to each other as if they were the same number — except in this framework's + own repo, where the pin and the ledger are the same number on purpose. - **Serves** — P1. - **Fitness** — no rule's text appears verbatim in two layer documents; every cross-layer reference (`serves` / `amends` / `supersedes` / `party`) resolves and every layer traces up; - no rule lives outside a layer. Verified by the `audit-structure` skill. + no rule lives outside a layer. No check treats the ledger version, the header pin, and the + installed tooling version as one axis, except for this framework's own self-hosted repo. + Verified by the `audit-structure` skill. ### Article F-III — Experiments are pre-registered `status: RATIFIED` · `conformance: HOLDS` · `enforcement: AUDITED` · `party: N/A` @@ -157,6 +165,27 @@ on the same Article is the signal that the Article itself needs amending. Superseded clauses are never deleted — they are kept here with a forward link and the ADR that justified the change. +### [0.17.3] — 2026-07-05 — Ratified by Chinmay: Article F-II extended — three version numbers, three homes +- **Ratified by Chinmay, 2026-07-05**, per [ADR-0002](decisions/0002-version-axis-separation.md). +- Found auditing DSAMind (`dsa_project`): the engine's `LEDGER-SYNC` check compared a + consumer's adopted-spec header pin against its own Amendments Ledger version and flagged + a real, correct mismatch as an error. Those are two different, legitimately independent + numbers. Fixed in code the same day (`cli/src/engine/audit.ts`); this entry writes the + distinction into law so it isn't lost. +- **Article F-II — Principle and Fitness extended** (`status: RATIFIED`, unchanged). Names + three version facts, each with exactly one home: an instance's own Amendments Ledger + version, the framework spec version it has adopted (the header pin — a ratified claim, + never bumped ahead of what's adopted), and the framework tooling installed (the instance's + own package manifest, e.g. `package.json`). Purely additive — nothing existing changed or + was superseded, no forward link needed. +- Agreement-only — no pre-registered experiment. There was no catch-rate or friction + hypothesis to measure, just a fact to state clearly. +- **Not part of this amendment**: whether skills should auto-check installed tooling is + current and auto-run migrations when it isn't. No mechanism exists yet to rule on; left + as open follow-up work (see ADR-0002's Consequences). +- Above the firewall — F-II's `status` did not change (already `RATIFIED`); its Principle + and Fitness text did, transcribed per `ratify-amendment`. + ### [0.17.2] — 2026-07-04 — Publish-on-merge: every merge to main ships the package (operator-directed) - Operator directive: "every PR merge should publish the package." New `.github/workflows/publish.yml`: on push to main — build (vendor + strict tsc), test, diff --git a/cli/package.json b/cli/package.json index 854c996..05344ec 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@chinmaygit/constitution-cli", - "version": "0.17.2", + "version": "0.17.3", "description": "The constitution governance engine: scaffold, audit, firewall-gate, compile, render, and track AI-native product development", "license": "MIT", "author": "Chinmay", diff --git a/cli/test/engine.test.ts b/cli/test/engine.test.ts index 1661647..305300d 100644 --- a/cli/test/engine.test.ts +++ b/cli/test/engine.test.ts @@ -47,7 +47,7 @@ describe('parse', () => { ]); expect(inst.constitution.articles.every((a) => a.status === 'RATIFIED')).toBe(true); expect(inst.statutes.length).toBeGreaterThan(5); - expect(inst.adrs).toHaveLength(1); + expect(inst.adrs).toHaveLength(2); const findings = audit(inst); expect(findings.filter((f) => f.severity === 'error')).toEqual([]); }); diff --git a/constitution.lock.json b/constitution.lock.json index 3296453..0c44172 100644 --- a/constitution.lock.json +++ b/constitution.lock.json @@ -1,8 +1,8 @@ { "lockVersion": 1, - "constitutionVersion": "0.17.0", + "constitutionVersion": "0.17.3", "acceptedBy": "Chinmay", - "acceptedAt": "2026-07-04T14:53:15.414Z", + "acceptedAt": "2026-07-05T08:45:28.047Z", "units": { "P1": { "kind": "preamble", @@ -14,7 +14,7 @@ }, "F-II": { "kind": "article", - "hash": "b18967b451f0665a6fcf17a94c799bcfeb043a3a1c88e79274860b5ed131e462" + "hash": "06ed10f12ad527f757a808764c7ee7f6fcc3432bfb06737a6180ad7a980334a5" }, "F-III": { "kind": "article", diff --git a/decisions/0002-version-axis-separation.md b/decisions/0002-version-axis-separation.md index 533beb2..cc5a0d2 100644 --- a/decisions/0002-version-axis-separation.md +++ b/decisions/0002-version-axis-separation.md @@ -1,7 +1,7 @@ --- id: 0002 title: Three version numbers, three homes -status: proposed +status: accepted date: 2026-07-05 supersedes: [] superseded_by: [] diff --git a/decisions/INDEX.md b/decisions/INDEX.md index 3cb5a16..a73b17c 100644 --- a/decisions/INDEX.md +++ b/decisions/INDEX.md @@ -6,4 +6,4 @@ an ADR) are L2 statutes — see [AGENTS.md](AGENTS.md). | id | title | status | date | |----|-------|--------|------| | 0001 | Allow package-managed installation of skills to satisfy F-II | accepted | 2026-07-01 | -| 0002 | Three version numbers, three homes | proposed | 2026-07-05 | +| 0002 | Three version numbers, three homes | accepted | 2026-07-05 |