From 9c987d21c25c73eda6b490253c12f938fa2fb2a1 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 11 Sep 2026 13:42:00 +0000 Subject: [PATCH 1/4] Squashed 'repos/constitution/' content from commit d311127e1f7 git-subtree-dir: repos/constitution git-subtree-split: d311127e1f7974aa923346b682d1da515d053500 --- .gitignore | 1 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + AGENTS.md | 121 ++++ CLAUDE.md | 1 + CONCEPTS.md | 21 + CONSTITUTION.md | 415 +++++++++++++ ENFORCEMENT.md | 38 ++ LICENSE | 201 +++++++ README.md | 99 ++++ commitlint.config.cjs | 3 + deno.json | 12 + deno.lock | 546 ++++++++++++++++++ ...-refactor-article-iii-verification-plan.md | 432 ++++++++++++++ ...-feat-omp-constitution-ttsr-plugin-plan.md | 49 ++ ...33-retire-the-ttsr-plugin-entirely-plan.md | 99 ++++ ...e-maker-never-holds-the-instrument-plan.md | 131 +++++ .../the-vacuous-pass-gate-input-sets.md | 195 +++++++ ...x-rules-flag-translation-backreferences.md | 40 ++ ...freeze-observations-about-a-moving-base.md | 34 ++ scripts/validate-constitution.ts | 282 +++++++++ 21 files changed, 2722 insertions(+) create mode 100644 .gitignore create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CONCEPTS.md create mode 100644 CONSTITUTION.md create mode 100644 ENFORCEMENT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 commitlint.config.cjs create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md create mode 100644 docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md create mode 100644 docs/plans/2026-09-01-0533-retire-the-ttsr-plugin-entirely-plan.md create mode 100644 docs/plans/2026-09-07-1721-the-maker-never-holds-the-instrument-plan.md create mode 100644 docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md create mode 100644 docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md create mode 100644 docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md create mode 100755 scripts/validate-constitution.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..74f6a8c --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +deno run --allow-read --allow-env --allow-run --allow-sys npm:@commitlint/cli@21 --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..9b938ee --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +deno task test diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..381cf84 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md — Constitution Repository + +Single source of truth for the supreme design law of [System F Software](https://systemfsoftware.com). Consumer repos vendor via `git subtree` + symlink. This repo has no production code, no test suite, and no build step — it is two documents plus governance tooling (corpus validation, commit validation): `CONSTITUTION.md`, the maker's law, resident in every agent's context, all articles, always; and `ENFORCEMENT.md`, the instrument owner's doctrine, never loaded into the maker's context. + +@CONSTITUTION.md + +## Startup Workflow + +Before making changes: + +1. **Read this file** completely. +2. **Confirm the active task** with the user or the agent's task list. +3. **Review recent commits** with `git log --oneline -5`. +4. **Ensure current branch is not `main`** — feature branches only. If on main, create one. + +## Working Rules + +- **One task at a time.** Finish before starting the next. +- **Conventional commits required.** The commit-msg hook enforces `type(scope): description`. Run `git commit` through the hook — do not bypass with `--no-verify`. +- **Verification required.** Run the verification commands before claiming done. +- **Stay in scope.** Don't modify files unrelated to the task. Scope reduction requires explicit user approval. +- **Leave clean state.** The next session must run verification immediately. + +## Amending the Constitution + +### Corpus + +- **`CONSTITUTION.md` (Resident):** the entire maker corpus — the Application section plus Articles I–V — loaded in every session. There is no retrieved half and no on-demand trigger: law that is not in the window is not law. Residency delivers obligation, not enforcement; the machinery of judgment lives outside the maker's reach. +- **`ENFORCEMENT.md` (Non-resident):** the instrument owner's doctrine — gate design, instrument-change discipline, enrollment law. Read by whoever builds or changes a gate, never `@`-imported into a maker's context. Consumer repos route to it from the leaves that own enforcement surfaces (lint plugins, guard scripts, CI workflows, rules directories, advisor rosters): one line — "You are editing an enforcement instrument. Read the vendored `ENFORCEMENT.md` before this lands." A routing line added anywhere else is the defect: the maker must not meet instrument doctrine while doing graded work. + +### Writing a Rule + +Rules are fenced YAML blocks with: `id`, `title`, `gate`, `do`, `dont`, `harm`, `check` (and optional `example`, `scope`, `layers`). + +### Minting an ID + +ID format: `CONST-`. Pick the next free number in the family (never renumber to close gaps). + +| Letter | Family | Purpose | +|---|---|---| +| `G` | Governance | Invoking constitution or resolving priority | +| `E` | Enforcement | The maker's conduct under judgment (evidence, the instrument boundary); instrument design lives in `ENFORCEMENT.md` | +| `P` | Purity | Decision functions and side-effect isolation | +| `D` | Domain modelling | Domain types and constraints | +| `B` | Boundary | Core/shell boundaries, effects, adapters | +| `T` | Testing | Testing strategy, mutation, properties | +| `N` | Naming & structure | Module organization and naming | +| `W` | Work discipline | Task scope, bypass declarations, reviews | +| `S` | Subtraction | Code deletion and structural simplification | + +### Changing a Rule + +- Same obligation reworded / moved: **keep ID**. +- Obligation narrowed, widened, split, or deleted: **retire old ID forever** and mint new one. +## Surface Classes + +| Surface | Files | Rule | +|---|---|---| +| **Locked** | `AGENTS.md`, `.husky/_/`, verification scripts | Read and propose changes; do not edit to make verification pass. | +| **Editable** | `deno.json`, `deno.lock`, `commitlint.config.cjs`, `.gitignore`, `.husky/` (hooks only, not `_/`) | Edit freely within the active task. | +| **Human-controlled** | `CONSTITUTION.md`, `README.md`, merging to `main`, pushing, destructive ops | Propose changes; ask the user before acting. | + +## Definition of Done + +A task is done only when ALL of the following are true: + +- [ ] Target changes are applied. +- [ ] Verification commands ran and passed. +- [ ] Commit uses conventional format (`type(scope): description`). +- [ ] Evidence recorded via the runtime memory system and task list. +- [ ] No dirty files left in the working tree. + +## Verification Commands + +```bash +deno task test # one file: schema, coverage, ids, families, dangling citations +deno run --allow-read --allow-env --allow-run npm:@commitlint/cli@21 --from HEAD~1 +``` + +After a commit that deletes, splits, merges, or re-scopes a rule — not after every edit — also run the reassignment check against the revision before it: + +```bash +deno task test --against +``` + +### Anti-Bypass Rules + +- Run the **full command**, not parts in isolation. +- Evidence must be from the **current run**, not a prior session. +- **Any failure blocks done.** Do not bypass with `--no-verify`. +- Do not suppress, skip, or disable checks to make verification pass. + +### Hallucination Prevention + +- **Read before edit:** before editing a file, read it in the current session. Do not edit from memory. +- **Verify before claim:** before saying "done," the verification command must have run and its output recorded. +- **Search before write:** before writing code that calls a library API, read the actual API surface. Do not generate from training memory. + +## Multi-Agent Ownership + +When multiple agents work in the same repo: + +- Each agent owns a disjoint file/module set. +- An agent must claim a file before editing it. +- Agents may not recursively delegate to each other. +- The one-shot verification must pass before any agent claims done. + +## End of Session + +Before ending a session: + +1. Record current state, blockers, and next steps via the runtime memory system and task list. +2. Commit with a conventional-format message once work is in a safe state. +3. Leave the repo clean — `git status` should show nothing unexpected. + +## Escalation + +- **Constitution conflict**: `CONSTITUTION.md` is already in context — reread it there, not from disk. +- **Unclear requirements**: Ask the user. +- **Verification failure**: Record via memory, flag for review, do not bypass. +- **Scope ambiguity**: Re-read this file and the Definition of Done. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONCEPTS.md b/CONCEPTS.md new file mode 100644 index 0000000..e920c23 --- /dev/null +++ b/CONCEPTS.md @@ -0,0 +1,21 @@ +# Concepts + +Shared domain vocabulary for this project — entities, named processes, and status concepts with project-specific meaning. Seeded with core domain vocabulary, then accretes as ce-compound and ce-compound-refresh process learnings; direct edits are fine. Glossary only, not a spec or catch-all. + +## Constitution corpus + +### Constitution corpus +The complete set of law documents the repository ships, treated as one unit. The corpus is what the gate certifies and what agents are bound by — not any single file's contents. Shrinking the measured portion of the corpus is a defect, not an optimization. + +### Rule +One fenced YAML block inside the constitution carrying an id of the form `CONST-` — the family letter is one of G, E, P, D, B, T, N, W, S (governance, enforcement, purity, domain modelling, boundary, testing, naming & structure, work discipline, subtraction). A rule is the atomic unit of law: it is minted with a fresh number, amended in place, or vacated. Minting takes the next free number in the family — a minting law enforced by review, not by the gate. A vacated number is never reused for new law. + + +### Vacuous pass +A gate exiting green because it measured less than it claims — a missing input, an input that contributes nothing, a comparison arm that reads only part of the history. The output shape is identical to a healthy run, so the green actively certifies the absence of defects it did not look for. The doctrine this repo's gate is built on: assert the corpus, not only the contents; a gate that cannot fail is a certificate, not enforcement. + +### Residency +The delivery contract for the maker's law: whatever must bind the agent's choices is always present in its context window, with no retrieval step. The counter-design — a thin resident pointer plus retrieved full text — forces presence at fetch time but never conformance at use time. Residency delivers obligation, not enforcement: the window tells the maker what is required, and whether the requirement held is decided afterward by instruments the maker cannot edit (ENFORCEMENT.md). What cannot fit residency is reference material and instrument doctrine, not law. + +### Corpus gate +The single check that certifies the constitution corpus — schema of every rule block, id and family registry, citation integrity across the corpus, and cross-revision comparison on demand. It runs as the repository's test command and again at every commit through the pre-commit hook, which makes any corpus-shape change also a gate change: the gate certifies the very input set a restructure edits. diff --git a/CONSTITUTION.md b/CONSTITUTION.md new file mode 100644 index 0000000..2ae0905 --- /dev/null +++ b/CONSTITUTION.md @@ -0,0 +1,415 @@ +# Constitution + +## Application + +```yaml +rules: + - id: CONST-G4 + title: By Purpose, Not Quotation + gate: review + do: invoke a principle by showing the harm is present; where letter and purpose diverge, purpose governs — ask "does this harm occur here" + dont: invoke a principle by quoting a clause or asking which clause to cite + harm: the letter applied where no harm exists defeats the purpose + check: review — an invocation names the harm, not the clause + - id: CONST-E9 + title: Never Edit What Grades Your Work + gate: review + do: + - treat every judgment surface as read-only — anything whose job is to check, measure, gate, or grade, including lint configs, rule lists, thresholds, baselines, CI checks, validator scripts, rubrics, coverage floors, and review criteria + - when a rule could fail a command and no command does, say the work is ungated and hand the gap to whoever owns the instrument — your work waits for it + dont: + - edit, add to, weaken, re-tune, or delete any judgment surface in service of your work — you cannot enumerate your graders, because some fire later (review, CI, the next enrollment wave), so the ban attaches to the surface, never to whether you believe it grades this task + - build the gate your work will be graded by, or shape one so the current work passes; the same hands in a separate commit is the same cheat with better hygiene + harm: whoever edits the instrument that grades their own work reports the score they chose; the green then certifies nothing and hides the broken invariant the gate existed to catch + check: review — the change being graded and any judgment-surface change name different owners; a needed gate is a proposal to the owner and a wait, never a build by the graded + - id: CONST-E7 + title: Evidence Before Done + gate: review + do: treat "done" as a gate passed or a test shown + dont: accept a reported score or a claimed "it works" as done + harm: compliance claimed without evidence + check: review — done names the gate that passed or the test that ran; a report of either is not either + - id: CONST-G5 + title: Supreme + gate: review + do: + - govern AGENTS.md, lint, and the ADRs on what a rule should say, where they conflict + - defer to the gate on whether a rule held — where this document and a gate disagree about a specific change, the gate is the final word; the disagreement is escalated to the channel that owns the instrument, never resolved by the maker editing the gate or overriding it in place + - place contestable choices — suffixes, patterns, module shape — in ADRs + dont: pin contestable choices in this document + harm: contestable choices frozen as supreme law; conflicts left unresolved + check: review — a conflict resolved by this document, a contestable choice resolved by an ADR +``` + +--- + +## Article I — The Pure Core + +```yaml +rules: + - id: CONST-P1 + title: Purity + gate: lint + do: each domain decision is a pure function — data in, a value or typed error out + dont: + - in a decision, do I/O, throw, read a clock, or use randomness + - return an effect handle from a decision — if it needs the runtime, move the boundary, not the purity + harm: logic untestable over all inputs, untrustworthy + check: lint — decisions import no I/O or effect-runtime; mutation + - id: CONST-D1 + title: Types Before Logic + gate: type-checker + do: define types before behavior; make illegal states unrepresentable so bad data fails to compile + dont: start from functions and add types after + harm: invalid data reaches runtime; tests multiply to cover what a type could forbid + check: type-checker rejects the illegal state; review + - id: CONST-D2 + title: Each Error Its Own Variant + gate: lint + do: give every distinct failure its own tagged variant + dont: distinguish failures by a boolean or string field + harm: callers can't branch on the real failure; distinct errors collapse into one case + check: lint; review — callers branch on the variant tag, never on a field value + - id: CONST-D3 + title: No Primitive Obsession + gate: lint + do: brand every domain-meaningful value (ids, amounts, codes) as its own type + dont: pass bare text or number in a domain-significant position + harm: values transposed or misused; the type says nothing about what they are + check: lint — no bare primitives in domain signatures + - id: CONST-D4 + title: Null Is Not a State — but absence is fine for optional data + gate: lint + do: + - model mutually-exclusive states as a tagged union — one variant per state, each carrying only its valid fields + - use a plain nullable for a value absent identically in every state + dont: + - encode a state by which fields are present + - wrap such a field in Option/Maybe to "fix" it — the wrapper renames the hole, not closes it + harm: a state machine hidden in a record; the compiler can't reject invalid field combinations — the question is never "null or Option" but "a value that may not exist, or a state in disguise" + check: lint — flags an optional that correlates with the discriminant, not plain optionals; review + example: + wrong: Order { status, shippedAt?, trackingId? } — state by presence; an Option wrapper is the same defect + right: Order = Pending { placedAt } | Shipped { placedAt, shippedAt, trackingId } + fine: Customer { name, middleName? } — genuinely optional; plain nullable, no wrapper + - id: CONST-P2 + title: The Pure Core Has One Path (Cyclomatic Complexity 1) + gate: lint + do: write each core decision as a single path — choice as exhaustive dispatch over a closed type (match a tagged union), iteration as map/fold; the core is an expression, not a procedure + dont: + - in the core, use if/else, switch, ?:, or &&/|| for control + - in the core, use for/while — repetition moves into map/fold + scope: binds the pure core (decision and workflow files); the ban is on the control-flow form, not branching — a core function reads as one path yet still decides and iterates; the shell sequences steps and carries no decisions, its only structure is the sandwich (CONST-B3); the gate runs on core files, not the shell + harm: every branch is an untested path where state silently diverges — the mutator reaches it, the suite does not + check: lint — cyclomatic complexity = 1 on core files (match, map, fold are calls, not control flow, so they hold at 1; if/switch/loops raise it) + example: + wrong: if (o.kind === "Shipped") ship(o) else hold(o) — two paths in a decision + right: match(o) { Shipped -> ship, Pending -> hold } — one exhaustive dispatch over a closed type + wrong_iteration: fold over the data with a for-loop in the core — iterating the core as a procedure + right_iteration: fold(xs, 0, add) — iterate as one expression (a shell loop is fine — that's the shell) +``` + +--- + +## Article II — The Boundary + +```yaml +rules: + - id: CONST-B1 + title: Functional Core, Imperative Shell + gate: review + do: split every module into a pure core (decisions) and a thin shell (I/O); pass plain serializable data across the seam + dont: let a boundary object (handler, adapter, middleware) make a decision — it only translates external ↔ domain + harm: decisions tangled with I/O can't be tested without mocks; bugs hide in the boundary + check: review — a boundary object that needs its own test suite has logic in it; move it to the core + - id: CONST-B2 + title: Effects Are Values + gate: lint + do: return effects as lazy values (descriptions), interpreted once at the edge; attach logging, metrics, tracing as decorators on the value + dont: + - put an eager async result (promise, future, task) on the public surface + - embed a cross-cutting concern in a decision + harm: an already-started result can't be held, retried, or swapped; embedded concerns can't be turned off or composed + check: lint — no eager async result on the public surface + example: + wrong: "getUser : UserId -> " + right: "getUser : UserId -> Effect — a lazy value, interpreted once at the edge" + - id: CONST-B3 + title: The I/O Sandwich + gate: review + do: shape every outside interaction as read (impure) → transform (pure) → write (impure); the shell calls the core directly + dont: insert a layer that only passes work through without a read, transform, or write + harm: side effects leak into business logic; pass-through layers add coupling for nothing + check: review — pass-through delegation is the violation; the shell doing the read/transform/write, or sitting between transport and core, is not + example: + flow: | + read → decode → decide → shape → write + impure bread (read, write) around a thick pure filling (decode, decide, shape), no I/O between the pure steps. + read pull raw inputs — store, gateway, network, clock (impure) + decode validate raw → branded domain types (fail as data) (pure) + decide one decision over typed data → Decision | Error (pure) + shape build outputs and events from the Decision (pure) + write persist · emit · respond (impure) + wrong: read → decide → read → decide — I/O interleaved; the filling turns impure + edge: a later read that depends on an earlier decision — pre-fetch it, split into two sandwiches, or keep it openly in the shell; never fake a "pure core" around it + - id: CONST-B6 + title: The Sandwich Order Is Carried by Types + gate: type-checker + do: express an outside interaction as one phase chain — each phase's return type carries the required member the next phase's parameter demands — so the order is a consequence of the types and the compiler decides it + dont: + - hand-sequence the phases and state their order beside them; an order asserted in prose is decided by nothing + - give the phases a hierarchy — where a later phase's type is assignable to an earlier phase's parameter, an inversion still compiles + harm: an order nothing decides permits every permutation while reading as a guarantee, so the interleaved read that turns the filling impure — the defect CONST-B3 names — reaches production with the rule green + check: type-checker — composing the phases in the wrong order omits the required member, so the compiler names the phase that must come first; the sentence survives into the published declaration as that member's own name, which is what carries it into a consumer's compiler + example: + wrong: "write(decide(read(raw))) — hand-sequenced; every permutation type-checks, so the order is a comment" + right: "read : Raw -> ReadDone, decode : ReadDone -> DecodeDone, decide : DecodeDone -> DecideDone — decode cannot receive what read has not produced" + - id: CONST-B4 + title: Dependencies Point Inward + gate: lint + do: let the shell import the core; wire all implementations at one composition root + dont: let the core import the shell, the database, or the framework + harm: a decision layer chained to infrastructure can't be tested or replaced + check: import-graph lint + - id: CONST-B5 + title: Decode, Never Cast + gate: lint + do: turn outside data (bytes, serialized text, a foreign type) into a domain type via a decode returning a typed result + dont: + - assert type with an unchecked cast (`as`, `as unknown as`, `as any`) + - assert type with a suppression comment + harm: a shape nothing verified; everything downstream trusts a check that never ran + check: lint — no unchecked casts or suppression comments on outside data + example: + wrong: config := value as Config + right: "config := decode(value) : Result" + - id: CONST-P3 + title: Purity Is Per Function, Not Per Folder + gate: review + do: judge pure-versus-effectful by return type alone + dont: infer it from a folder, package, or "library versus application" + harm: a database-driver mislabeled "pure," a parser "impure," because of where it lives + check: review — return type decides; the lint behind CONST-P1 + example: + pure: "decide : Command -> Result" + effectful: "load : OrderId -> Effect — owns effects" +``` + +--- + +## Article III — Verification + +```yaml +rules: + - id: CONST-T15 + title: The Testing Trophy + gate: review + do: invest in order — static analysis first, then properties on the core where CONST-T14 grants them, then composition through the sandwich (CONST-B3), then contracts at the published edge; the order is the doctrine, a count of tests per layer is not + dont: + - gate anything on a numeric layer width or a layer-naming table — the widths were refuted instrumentation, not law + - build a Test Pyramid — a wide base of helper unit tests buries logic in I/O and leaves the untested middle + harm: investment spent where the compiler and the mutator already decide; the untested middle stays untested while the suite looks broad + check: review — each authored test names its observer and the step of the order it earns, never a width + - id: CONST-T8 + title: Test Public Functions Directly, Pure Logic with Mutation + gate: review + do: test the public API with real inputs and outputs; test internal calculation and branching logic with mutation tests (and properties only when CONST-T14 requires them); never write dedicated unit tests for code that only forwards calls between components + dont: + - write unit tests for intermediate helper functions that only pass data to other functions + - mix I/O code or adapters into the same mutation test run as pure calculation logic + - mock a dependency when only one real implementation exists + harm: unit-testing intermediate layers locks in private implementation details without catching real bugs; mocking real code gives false confidence; business rules stay tangled with I/O + check: review — every test calls either a public export or a pure decision function, never a private forwarding helper + - id: CONST-T14 + title: Properties Where the Surface Cannot Reach + gate: review + do: prove a pure decision with a property when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; the type is the generator + dont: + - cover the core with hand-picked example unit tests + - write a property for a decision already fully pinned from above just because the decision is important + harm: a green suite that tests only the cases you imagined; or a property farm that restates the public contract and dies with it + check: review — each authored property names the universal the public surface cannot reach + - id: CONST-T3 + title: Mutation Is the Measure + gate: mutation + do: gate a named, change-relevant mutated set at a perfect kill score; the set names the behavior it covers, and its scope is a cost decision, never a fault-majority claim; kill a survivor with a sharper property or by deleting the dead branch it exploits + dont: + - reach the number by a suppression comment + - reach the number by narrowing the mutated set after the fact + - reach the number by lowering the gate + - let an empty mutated set pass + - treat a raw mutation percentage as comparable across changes or codebases + harm: a score certifying tests that notice nothing; an empty or author-shrunk set passing vacuously + check: mutation gate (break = 100) on the declared mutated set; lint banning suppression, scope-narrowing, and an empty set + - id: CONST-T13 + title: Mutation Also Grades the Tests + gate: mutation + do: fail a run whose mutants all died if an authored property file defends nothing the rest of the suite does not; opt out in the mutation config, never by deleting the file the gate named + dont: + - treat a perfect mutant score as proof every test pulled its weight + - accuse a file that covered an unattributed kill + harm: toothless properties accumulate; deleting them to silence the gate removes the only named contract + check: mutation — the test-set verdict is part of the same run as the score + - id: CONST-T4 + title: Behavior Lives Where the Mutator Sees It + gate: lint + do: put any code that can be wrong (transform, check, branch) in a file the mutator covers + dont: place behavior in a declaration file (types, schemas, constant data), excluded from mutation + harm: a bug hidden behind a perfect score, in a file nothing mutates + check: lint — declaration files contain no behavior + - id: CONST-T9 + title: Pin the Published Contract Before You Delete a Path + gate: review + do: before removing or replacing a published operation, pin its observables (value, error variant, serialized document, process result) with examples or properties whose expected side is not the implementation under change; if the old operation still runs, compare old and new on the same published inputs until they agree, then delete old + dont: + - pin private functions + - derive expected values by running the implementation under change + - treat a mutation or property score as proof a deleted published capability still exists — those are blind to absence + - leave a persisted gold after the old path is gone unless the gold is externally authored, independently gated, and cheap to re-bless + harm: a rebuild silently drops a capability; same-session gold blesses the bug; a clean score after a delete is a silent regression + check: review — pins call only published names; each expected value names an independent source (spec clause, prior published major, second implementation, or a hand-written oracle next to the constructor) + - id: CONST-T10 + title: The Oracle Is Not the System Under Test + gate: review + do: every assertion has an oracle the SUT did not produce — a spec literal, a fixture not generated by importing the module, a law relating two views of the same value, or a second implementation; generated round-trip laws on a type cover what the type accepts and nothing it should reject, so a hand-written refusal survives beside them at any specifiable refusal boundary + dont: + - compute expected by calling the SUT + - assert collaborator call graphs + - treat generated accept-laws as full coverage of a refinement + harm: a green suite that cannot fail when the behavior is wrong; widening a refinement leaves generated laws green + check: review — plus sabotage (after green, break one core law and one published field; at least one test must go red) + - id: CONST-T11 + title: Snapshots and Differentials Are Published-Surface Oracles + gate: lint + do: snapshot only canonicalized published output; compare two implementations only of the same published operation (or a prior published major against current) + dont: + - snapshot or compare private helpers, mappers, or unexported modules + - snapshot a value small enough to be a property or a named example + harm: tests that fail on refactors callers cannot see and pass on contract breaks they can + check: lint — snapshot and differential fixtures are produced only through the package's published export map + - id: CONST-T12 + title: What a Test Does Comes from What It Calls, Not Its Filename + gate: lint + do: classify what a test is by what it imports and calls — public exports or pure logic under mutation — never by its folder, filename, or file extension + dont: decide which testing rules apply to a file based on its name or suffix + harm: renaming a test file secretly stops its rules from running while the test suite still looks complete + check: lint — no linter or test runner rules that pick tests by filename suffix +``` + +--- + +## Article IV — Organization + +```yaml +rules: + - id: CONST-N1 + title: Organized by What It Does + gate: review + do: organize by workflow and capability; keep code that changes together, together + dont: organize by what the system has (entities, technical layers) + harm: one change scattered across the tree + check: review — one change touches one capability subtree + - id: CONST-N2 + title: Names Scream the Domain + gate: lint + do: name files and folders for the job they do — a name must answer "of what?" + dont: + - use layer names (`core`, `shell`) + - use junk drawers (`util`, `service`, `manager`) + - use a suffix no rule keys on + harm: files no one can locate; meaningless buckets + check: filename lint — allowed suffixes; banned layer and junk-drawer names + - id: CONST-N3 + title: Fits in the Head + gate: review + do: give a module one responsibility; split it when a test needs elaborate setup (the signal it has several) + dont: accumulate unrelated concerns in one module + harm: modules no one can fully reason about; brittle, sprawling tests + check: review — fixture difficulty is the decomposition signal +``` + +--- + +## Article V — Conduct + +```yaml +rules: + - id: CONST-G3 + title: Constitutional Violations Are Automatic P0 Failures + gate: review + do: treat every undeclared violation of this constitution during review as an automatic, non-negotiable P0 failure; reject the change unconditionally with zero appeals and no severity downgrades unless explicitly declared under CONST-W3 + dont: + - downgrade an undeclared constitutional breach to an advisory, P1, P2, or non-blocking finding + - treat a CONST-W3 declaration as optional prose — an explicit declaration in the change itself is the only legal waiver, and it must name the rule and the case + - accept a promise of follow-up repair or expedience plea to bypass an active rule without a CONST-W3 declaration + harm: constitutional rules decay into optional suggestions; agents negotiate away core architecture to ship faster; unblocked violations calcify into precedent + check: review — every undeclared constitutional violation is graded P0 and blocks approval unconditionally; any review that waives or downgrades an undeclared violation is rejected + - id: CONST-S1 + title: Depth Over Expedience + gate: review + do: fix the root cause; restructure when the design is wrong + dont: patch the symptom or bypass a boundary to ship faster + harm: the bug returns + check: review — the change names the root cause it fixes + - id: CONST-W1 + title: Scope Discipline + gate: review + do: execute accepted scope in full + dont: reduce scope mid-task because it grew complex, without the author's consent + harm: half-finished work; wasted effort second-guessing intent + check: review — delivered scope matches accepted scope + - id: CONST-S2 + title: First Principles Over Precedent + gate: review + do: justify a pattern by these principles — surrounding code is evidence of what exists, never of what is correct + dont: + - justify by "that is how it's done elsewhere" + - copy a neighbouring file as a template — code age grants no immunity + harm: unexamined defaults calcify into rules; one slop pattern seeds the next by imitation, and the average drifts down + check: review — a choice defended by precedent, or by the file next to it, is rejected + - id: CONST-S3 + title: API-First Discovery + gate: review + do: define the outside contract first, then derive use cases, decisions, and machinery beneath it; model only what a known requirement needs + dont: build a domain abstraction for a hypothetical future + harm: speculative structure that never pays off and constrains what comes after + check: review — every abstraction traces to a known requirement + - id: CONST-W2 + title: Challenge Before You Commit + gate: review + do: subject a large or irreversible choice to a deliberate challenge (another agent, a person, or rigorous self-examination), record it with the decision, judge it by the harm it names + dont: + - appeal to a tribunal or standing authority + - let a challenge become a clause quoted against a choice + harm: a costly, hard-to-reverse direction taken with no one trying to break it first + check: review — the challenge is recorded with the decision + - id: CONST-W3 + title: No Silent Bypass + gate: review + do: when you break a rule here — knowingly, or because it was wrong for this case — say so, in the open, in the change itself + dont: conceal a bypass + harm: two failures — the breach and the hiding of it; the next reader trusts a rule quietly broken + check: review — every rule breach is declared in the change that contains it + - id: CONST-S4 + title: Subtract Before You Add + gate: review + do: + - treat every line as a liability — removal is the default response to slop at every scale; adding is the exception you justify + - small — unify duplicates, make bad states unrepresentable, delete a branch instead of guarding it + - structural — when the root violates this document and breeds a bug class, rebuild the core (published contract pinned first (CONST-T9); decomposed into shippable milestones) rather than prune leaves off a rotten trunk + - distrust existing structure — assume rotten until it proves it conforms + dont: + - extend a copy-paste cluster with copy N+1 + - add a helper when removing or unifying one does the job + - patch around a rotten core to keep it alive + - treat code as sound because it compiles, is large, or is old + - mistake taste ("I'd write it differently") for rot + harm: the codebase only grows; rot survives every patch and regrows; each copied pattern seeds the next, and the average drifts down + check: review reads the net line delta — a refactor/improvement/chore that adds net lines states why and names what it deleted (features and their tests are exempt); a fix that leaves a named root violation standing is rejected; "rotten" names the invariant the core breaks; a structural rebuild ships a CONST-T9 pin on every published path it deletes + example: + wrong: add formatPhone() beside the three formatters already there + right: delete the three, keep one parameterised formatter + wrong_state: add a guard for the impossible state the record permits + right_state: delete the record; a tagged union makes the state unconstructable + +``` diff --git a/ENFORCEMENT.md b/ENFORCEMENT.md new file mode 100644 index 0000000..b1d7911 --- /dev/null +++ b/ENFORCEMENT.md @@ -0,0 +1,38 @@ +# Enforcement + +Rules for whoever builds or changes an instrument that grades the maker — lint gates, stream rules, CI checks, thresholds, rubrics, advisors. Never `@`-import this file into a maker's context. + +## Mechanism + +- Bind each obligation through the strongest mechanism that can carry it, strongest first: + 1. the type system — the illegal state does not construct; + 2. a command that fails — lint, threshold, boundary audit; + 3. a pre-execution refusal — the write or tool call does not happen; + 4. resident prose — the floor; it orients, it does not enforce. +- A post-hoc reminder is prose, not a refusal. If the violating call already executed, the mechanism is rung 4. + +## Gate design + +- Forbid an outcome; never command a ritual. *Do not ship an export whose dist file is missing*, not *declare a category and write a reason*. +- Key every verdict on a recomputation — source bytes, compiler verdict, rehash — never on a field the graded work's author supplied. When a gate reads a field, recompute that field in the same run. +- Resolve every uncertainty against the permissive outcome: unparseable output degrades to revise, never to pass; an absent grant means required; an unreadable input is surfaced, never skipped; a dead run never reports itself running. +- Assert the input set, not only its contents: an empty corpus, an empty mutated set, or a missing input goes red. A gate that cannot fail is a certificate, not enforcement. + +## Changing an instrument + +- An instrument change lands alone, in its own commit, observed failing before and passing after, for the reason it states — by hands that do not hold the work the instrument judges. +- Never loosen a threshold, budget, glob, or baseline to make in-flight work pass. The same hands in a second commit is the same cheat. +- When the maker escalates an ungated principle (CONST-E9), you build the instrument and land it through your own channel; only then does the maker's work proceed. + +## Enrollment + +- Enroll over a clean tree only: migrate every live violation first, then enroll at error. A baseline or allowlist of existing violations is suppression with a date on it. +- A rule registered only in its own test harness is not enrolled. Prove carriage by driving the published artifact the way a consumer resolves it. +- `warn` is forbidden: error with a clean tree, or off with the reason named. +- Delete the retired shape from every owner at enrollment. Two conventions standing is the next agent imitating the wrong one. + +## Gate economy + +- Every gate names the mistake it prevents. A gate that cannot name one does not land. +- Judge gate work by net line delta; subtraction must leave an artifact. +- The enforcement surface is read-only to the agents it governs. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b24e080 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Constitution + +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE) +[![System F Software](https://img.shields.io/badge/systemfsoftware.com-constitution-black?style=flat-square)](https://systemfsoftware.com/constitution) +[![Rules: 38](https://img.shields.io/badge/rules-38%20in%20corpus-blue?style=flat-square)](CONSTITUTION.md) + +Shared engineering laws for repositories at [System F Software](https://systemfsoftware.com). + +It defines baseline architecture and code quality standards: a pure functional core behind a thin imperative shell, domain types before logic, mutation testing for decision paths, and subtracting code before adding more. Stack-neutral and enforced across all projects. + +```mermaid +flowchart LR + S["systemfsoftware/constitution
Upstream Repository"] -->|git subtree| A[Consumer Repo A] + S -->|git subtree| B[Consumer Repo B] + S -->|git subtree| C[Consumer Repo C] + A -.symlink.-> S + B -.symlink.-> S + C -.symlink.-> S +``` + +--- + +## Quick Start + +Vendor the repository using `git subtree` and symlink `CONSTITUTION.md` to the project root: + +```bash +# 1. Fetch the remote into a local ref +git fetch https://github.com/systemfsoftware/constitution.git main:refs/remotes/vendor/constitution + +# 2. Add as a squashed subtree +git subtree add --prefix=vendor/constitution refs/remotes/vendor/constitution --squash \ + -m "chore: vendor shared constitution" + +# 3. Symlink the constitution to the repo root +ln -s vendor/constitution/CONSTITUTION.md CONSTITUTION.md +``` + +Include `@CONSTITUTION.md` in your agent harness (`AGENTS.md` or `CLAUDE.md`) so all 38 rules remain always-on in the context window. Never include `ENFORCEMENT.md` there — it is doctrine for whoever builds the gates, not for the agent the gates grade. Instead, wire it at the surfaces that own instruments: in every `AGENTS.md` leaf governing a lint plugin, guard script, CI workflow, rules directory, or advisor roster, add one routing line — "You are editing an enforcement instrument. Read the vendored `ENFORCEMENT.md` before this lands." + +--- + +## The Articles + +The 38 rules are structured across six sections in [`CONSTITUTION.md`](CONSTITUTION.md): + +| Section | Key Invariants | +| :--- | :--- | +| **Application** | Invoke rules by harm rather than clause; build failures decide; never edit what grades your work; evidence before done. | +| **Article I: Pure Core** | Pure decisions, explicit types, tagged error variants, no `null` states. | +| **Article II: Boundaries** | Functional core / imperative shell, values for effects, decode inputs rather than casting. | +| **Article III: Verification** | Testing Trophy investment order, properties by narrow grant, mutation as the measure, independent oracles. | +| **Article IV: Organization** | Organize by domain responsibility, clear naming, keep modules small. | +| **Article V: Conduct** | Zero-appeal P0 review enforcement, fix root causes, challenge decisions before committing, subtract before adding. | + +--- + +## Machine Validation + +Rules are defined as structured YAML blocks in `CONSTITUTION.md`: + +```yaml +- id: CONST-S4 + title: Subtract Before You Add + gate: review + do: treat every line as a liability — removal is the default response to slop + dont: extend a copy-paste cluster; patch around a rotten core + harm: the codebase only grows; rot survives every patch and regrows + check: review reads the net line delta; fixes that leave root violations are rejected +``` + +Run the validator to check rule IDs, schema compliance, and citation integrity across the corpus: + +```bash +deno task test +``` + +To verify that rule identifiers have not been reassigned against a previous git revision: + +```bash +deno task test --against +``` + +--- + +## Pulling Updates + +Pull upstream changes into the subtree without touching existing symlinks: + +```bash +git subtree pull --prefix=vendor/constitution https://github.com/systemfsoftware/constitution.git main --squash \ + -m "chore: update shared constitution" +``` + +--- + +## License + +[Apache-2.0](LICENSE) © 2026 Ryan Lee. diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..84dcb12 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..efabd36 --- /dev/null +++ b/deno.json @@ -0,0 +1,12 @@ +{ + "version": "0.1.0", + "nodeModulesDir": "auto", + "tasks": { + "test": "deno run --allow-read=CONSTITUTION.md --allow-run=git scripts/validate-constitution.ts", + "prepare": "git config core.hooksPath .husky" + }, + "imports": { + "@std/yaml": "jsr:@std/yaml@^1.2.0", + "@commitlint/config-conventional": "npm:@commitlint/config-conventional@^21.2.0" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..bad6466 --- /dev/null +++ b/deno.lock @@ -0,0 +1,546 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/yaml@^1.2.0": "1.2.0", + "npm:@commitlint/cli@21": "21.2.2_@types+node@26.4.0_typescript@7.0.2", + "npm:@commitlint/config-conventional@^21.2.0": "21.2.2" + }, + "jsr": { + "@std/yaml@1.2.0": { + "integrity": "20beb41e4983ba3437dbefac62b14061ab058e8a187596f19d28ff9035f6e6cf" + } + }, + "npm": { + "@babel/code-frame@7.29.7": { + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/helper-validator-identifier@7.29.7": { + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + }, + "@commitlint/cli@21.2.2_@types+node@26.4.0_typescript@7.0.2": { + "integrity": "sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==", + "dependencies": [ + "@commitlint/config-conventional", + "@commitlint/format", + "@commitlint/lint", + "@commitlint/load", + "@commitlint/read", + "@commitlint/types", + "tinyexec", + "yargs" + ], + "bin": true + }, + "@commitlint/config-conventional@21.2.2": { + "integrity": "sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==", + "dependencies": [ + "@commitlint/types", + "conventional-changelog-conventionalcommits" + ] + }, + "@commitlint/config-validator@21.2.0": { + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", + "dependencies": [ + "@commitlint/types", + "ajv" + ] + }, + "@commitlint/ensure@21.2.0": { + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", + "dependencies": [ + "@commitlint/types", + "es-toolkit" + ] + }, + "@commitlint/execute-rule@21.0.1": { + "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==" + }, + "@commitlint/format@21.2.2": { + "integrity": "sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==", + "dependencies": [ + "@commitlint/types", + "picocolors" + ] + }, + "@commitlint/is-ignored@21.2.2": { + "integrity": "sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==", + "dependencies": [ + "@commitlint/types", + "semver" + ] + }, + "@commitlint/lint@21.2.2": { + "integrity": "sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==", + "dependencies": [ + "@commitlint/is-ignored", + "@commitlint/parse", + "@commitlint/rules", + "@commitlint/types" + ] + }, + "@commitlint/load@21.2.2_@types+node@26.4.0_typescript@7.0.2": { + "integrity": "sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==", + "dependencies": [ + "@commitlint/config-validator", + "@commitlint/execute-rule", + "@commitlint/resolve-extends", + "@commitlint/types", + "cosmiconfig", + "cosmiconfig-typescript-loader", + "es-toolkit", + "is-plain-obj", + "picocolors" + ] + }, + "@commitlint/message@21.2.0": { + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==" + }, + "@commitlint/parse@21.2.2": { + "integrity": "sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==", + "dependencies": [ + "@commitlint/types", + "conventional-changelog-angular", + "conventional-commits-parser" + ] + }, + "@commitlint/read@21.2.1": { + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", + "dependencies": [ + "@commitlint/top-level", + "@commitlint/types", + "@conventional-changelog/git-client", + "tinyexec" + ] + }, + "@commitlint/resolve-extends@21.2.2": { + "integrity": "sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==", + "dependencies": [ + "@commitlint/config-validator", + "@commitlint/types", + "es-toolkit", + "global-directory", + "resolve-from@5.0.0" + ] + }, + "@commitlint/rules@21.2.2": { + "integrity": "sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==", + "dependencies": [ + "@commitlint/ensure", + "@commitlint/message", + "@commitlint/to-lines", + "@commitlint/types" + ] + }, + "@commitlint/to-lines@21.0.1": { + "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==" + }, + "@commitlint/top-level@21.2.0": { + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", + "dependencies": [ + "escalade" + ] + }, + "@commitlint/types@21.2.0": { + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", + "dependencies": [ + "conventional-commits-parser", + "picocolors" + ] + }, + "@conventional-changelog/git-client@3.1.2": { + "integrity": "sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==", + "dependencies": [ + "@simple-libs/child-process-utils", + "@simple-libs/stream-utils", + "semver" + ] + }, + "@conventional-changelog/template@1.4.0": { + "integrity": "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==" + }, + "@simple-libs/child-process-utils@2.0.0": { + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", + "dependencies": [ + "@simple-libs/stream-utils" + ] + }, + "@simple-libs/stream-utils@2.0.0": { + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==" + }, + "@types/node@26.4.0": { + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "dependencies": [ + "undici-types" + ] + }, + "@typescript/typescript-aix-ppc64@7.0.2": { + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-darwin-arm64@7.0.2": { + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@typescript/typescript-darwin-x64@7.0.2": { + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@typescript/typescript-freebsd-arm64@7.0.2": { + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-freebsd-x64@7.0.2": { + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-linux-arm64@7.0.2": { + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@typescript/typescript-linux-arm@7.0.2": { + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@typescript/typescript-linux-loong64@7.0.2": { + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@typescript/typescript-linux-mips64el@7.0.2": { + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@typescript/typescript-linux-ppc64@7.0.2": { + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-linux-riscv64@7.0.2": { + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@typescript/typescript-linux-s390x@7.0.2": { + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@typescript/typescript-linux-x64@7.0.2": { + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@typescript/typescript-netbsd-arm64@7.0.2": { + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-netbsd-x64@7.0.2": { + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-openbsd-arm64@7.0.2": { + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-openbsd-x64@7.0.2": { + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-sunos-x64@7.0.2": { + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@typescript/typescript-win32-arm64@7.0.2": { + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@typescript/typescript-win32-x64@7.0.2": { + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "ajv@8.20.0": { + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": [ + "fast-deep-equal", + "fast-uri", + "json-schema-traverse", + "require-from-string" + ] + }, + "ansi-regex@6.3.0": { + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==" + }, + "ansi-styles@6.2.3": { + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==" + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "argue-cli@3.1.0": { + "integrity": "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==" + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "cliui@9.0.1": { + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dependencies": [ + "string-width@7.2.0", + "strip-ansi", + "wrap-ansi" + ] + }, + "conventional-changelog-angular@9.4.0": { + "integrity": "sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==", + "dependencies": [ + "@conventional-changelog/template" + ] + }, + "conventional-changelog-conventionalcommits@10.4.0": { + "integrity": "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==", + "dependencies": [ + "@conventional-changelog/template" + ] + }, + "conventional-commits-parser@7.1.2": { + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", + "dependencies": [ + "@simple-libs/stream-utils", + "argue-cli" + ], + "bin": true + }, + "cosmiconfig-typescript-loader@6.3.0_@types+node@26.4.0_cosmiconfig@9.0.2__typescript@7.0.2_typescript@7.0.2": { + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "dependencies": [ + "@types/node", + "cosmiconfig", + "jiti", + "typescript" + ] + }, + "cosmiconfig@9.0.2_typescript@7.0.2": { + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dependencies": [ + "env-paths", + "import-fresh", + "js-yaml", + "parse-json", + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "emoji-regex@10.6.0": { + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "env-paths@2.2.1": { + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "error-ex@1.3.4": { + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": [ + "is-arrayish" + ] + }, + "es-toolkit@1.52.0": { + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==" + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-uri@3.1.6": { + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==" + }, + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-east-asian-width@1.6.0": { + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==" + }, + "global-directory@5.0.0": { + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", + "dependencies": [ + "ini" + ] + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from@4.0.0" + ] + }, + "ini@6.0.0": { + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==" + }, + "is-arrayish@0.2.1": { + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-plain-obj@4.1.0": { + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==" + }, + "jiti@2.6.1": { + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "bin": true + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml@4.3.2": { + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "json-parse-even-better-errors@2.3.1": { + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema-traverse@1.0.0": { + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "parse-json@5.2.0": { + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": [ + "@babel/code-frame", + "error-ex", + "json-parse-even-better-errors", + "lines-and-columns" + ] + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "require-from-string@2.0.2": { + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-from@5.0.0": { + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "semver@7.8.5": { + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": true + }, + "string-width@7.2.0": { + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": [ + "emoji-regex", + "get-east-asian-width", + "strip-ansi" + ] + }, + "string-width@8.2.2": { + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dependencies": [ + "get-east-asian-width", + "strip-ansi" + ] + }, + "strip-ansi@7.2.0": { + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dependencies": [ + "ansi-regex" + ] + }, + "tinyexec@1.3.0": { + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==" + }, + "typescript@7.0.2": { + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "optionalDependencies": [ + "@typescript/typescript-aix-ppc64", + "@typescript/typescript-darwin-arm64", + "@typescript/typescript-darwin-x64", + "@typescript/typescript-freebsd-arm64", + "@typescript/typescript-freebsd-x64", + "@typescript/typescript-linux-arm", + "@typescript/typescript-linux-arm64", + "@typescript/typescript-linux-loong64", + "@typescript/typescript-linux-mips64el", + "@typescript/typescript-linux-ppc64", + "@typescript/typescript-linux-riscv64", + "@typescript/typescript-linux-s390x", + "@typescript/typescript-linux-x64", + "@typescript/typescript-netbsd-arm64", + "@typescript/typescript-netbsd-x64", + "@typescript/typescript-openbsd-arm64", + "@typescript/typescript-openbsd-x64", + "@typescript/typescript-sunos-x64", + "@typescript/typescript-win32-arm64", + "@typescript/typescript-win32-x64" + ], + "bin": true + }, + "undici-types@8.3.0": { + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + }, + "wrap-ansi@9.0.2": { + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dependencies": [ + "ansi-styles", + "string-width@7.2.0", + "strip-ansi" + ] + }, + "y18n@5.0.8": { + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs-parser@22.0.0": { + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==" + }, + "yargs@18.1.0": { + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dependencies": [ + "cliui", + "escalade", + "get-caller-file", + "string-width@8.2.2", + "y18n", + "yargs-parser" + ] + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/yaml@^1.2.0", + "npm:@commitlint/config-conventional@^21.2.0" + ] + } +} diff --git a/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md b/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md new file mode 100644 index 0000000..c6acc20 --- /dev/null +++ b/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md @@ -0,0 +1,432 @@ +--- +title: Article III Verification Doctrine Rehaul - Plan +type: refactor +date: 2026-08-29 +supersedes: docs/plans/2026-08-29-2210-refactor-article-iii-verification-plan.md +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-plan-bootstrap +execution: code +--- + +# Article III Verification Doctrine Rehaul - Plan + +## Goal Capsule + +- **Objective:** The constitution's verification law binds only what measured evidence supports — oracle independence, observer-fit test placement, change-relevant mutation over a named non-empty set, published-contract pinning before deletion, and recomputed gate keys — and no longer prescribes the Testing Trophy, mandatory mocked composition, blanket property coverage of the core, or characterization as a standing duty. The corpus gate is Deno. A reader outside this repo can confirm it: `pnpm test` passes with 33 rules across both corpus files, and `pnpm test --against ` run after the corpus commit names exactly CONST-T1, CONST-T2, and CONST-T5 as vacated with no id reassigned. +- **Means:** The corpus gate is rewritten as a Deno script that implements the behavior the repo's own documentation specifies (vacated-id and uncompared-file reporting); Article III is replaced (vacate T1/T2/T5, reword T3, keep T4, mint T8–T14 plus CONST-E5); CONST-S4 is repointed to CONST-T9; AGENTS.md and README.md prose is reconciled (KTD1–KTD5; normative rule text in Appendix A). +- **Authority:** The H1–H12 verdict dossier and the org-mechanism specification (both delivered in the invoking conversation) govern doctrine conflicts. The corpus's own id-surgery table in AGENTS.md governs id handling. Ecosystem tooling law: Deno only for code (software-wiki harness law). The current corpus at `origin/main` (28 rules: 7 resident Article V + 21 retrieved) is the base being amended, not evidence of correctness (CONST-S2). +- **Execution profile:** One feature branch; four conventional commits in order — plan checkpoint, validator, corpus, prose — pushed together in one PR; no consumer-repo changes. +- **Stop conditions:** A gate failure not explained by the amendment's own edits stops the run. An edit whose find-pattern misses (grep still finds the old text after the edit) stops the run for repair. Implementation-time evidence contradicting a settled decision stops the run. +- **Tail ownership:** Branch shipping, PR, and CI watch belong to the calling pipeline. The org tree's watchdog and mutation-config updates belong to that repo after its next vendor pull. + +--- + +## Product Contract + +### Summary + +Replace the Python corpus gate with a Deno script implementing the full documented behavior — schema, coverage, families, gates, dangling citations, cross-revision reassignment, and vacated-id/uncompared-file reporting. Rewrite Article III of `CONSTITUTION-ARTICLES.md` so the verification doctrine matches the evidence: observer-fit placement instead of trophy widths, a narrow grant for property tests instead of a blanket mandate, mutation gated on a named change-relevant set whose run also grades the test files, published-contract pinning instead of characterization, an independent-oracle requirement with a refusal complement, and one enforcement rule (CONST-E5) binding every gate's key to a recomputation. Repoint CONST-S4's rebuild pin from the vacated CONST-T5 to the new CONST-T9, and reconcile AGENTS.md and README.md so no prose teaches the vacated doctrine or a false rule count. + +### Problem Frame + +The org's enforcing tree already implements observer-fit placement, toothless-property detection, mutation of named sets, a contract lane driven from outside the process, and import-keyed test classification. The constitution still describes a Testing Trophy with mocked composition as the widest band and mandatory characterization before every rebuild. Agents obey `check:` lines, so the stale doctrine is not inert prose — it actively instructs behavior the enforcing tree refuted. The dossier (H1–H12, 73 primary sources) adjudicates the doctrine: same-session test generation is tautology-prone without an independent oracle (H1), the two-surface count is underdetermined (H2), decision-only mutation over-claims observer-fit (H5), a decision-layer fault-majority is false as a census (H6), and gates bind through accountability while advisory surfacing is inert (H12). The constitution must state principles at that altitude and leave mechanism binding to consumer trees, because consumer repos in several languages vendor this file. + +The amendment prunes refuted doctrine and replaces it with a larger instruction surface: Article III grows from 5 to 9 rules and the corpus from 28 to 33. That growth is deliberate — each minted rule names an observer, oracle, or gate discipline the old five could not express, and the refuted doctrine is gone from every line the nine now occupy. The no-retirement id rule means each mint is permanent; that cost is accepted per rule, not in aggregate. + +### Key Decisions + +- **Vacate CONST-T1 and CONST-T5 rather than reword them.** The trophy and the characterization duty are refuted doctrine, not rewordable ones; their numbers stay vacant forever per the corpus's own id-surgery rule. (session-settled: user-directed — chosen over keeping trophy and characterization as reworded doctrine: the org tree's measured mechanisms and the dossier refuted both.) Governs R2, R3, R20. +- **The dossier adjudicates doctrine conflicts.** Where the org tree's current mechanisms and the evidence disagree, the evidence wins: mutation scope is change-relevant, not decision-only (H5); the organizing principle is oracle independence, not a surface count (H2). (session-settled: user-directed — chosen over encoding the org repo's mechanisms verbatim: H5 refuted decision-only scoping as observer-fit and H2 refuted the exhaustive two-surface claim.) Governs R5, R8. +- **CONST-E5 ships in this amendment.** The unforgeable-gate rule is in scope now, not deferred. (session-settled: user-directed — chosen over deferring gate-verification doctrine to a later amendment: user directed inclusion when the deferral was presented as default.) Governs R14. +- **Principles only — no tool names, no consumer names, no filename suffixes in rule YAML.** Mutation is an instrument class; how a tree scopes its mutant set is that tree's config. (session-settled: user-directed — chosen over naming the enforcing stack as exemplar: consumer repos in other languages vendor this file, and a named first consumer turns a house rule into law.) Governs R19. + +### Requirements + +**Corpus gate (Deno)** + +- R1. `scripts/validate-constitution.py` is deleted; a Deno script at `scripts/validate-constitution.ts` implements the same gate — the two-file corpus contract, fenced-YAML coverage accounting, the rule schema (required, optional, and gate fields), the family registry, duplicate-id detection, dangling-citation resolution across both files, and the `--against` cross-revision reassignment check — plus the vacated-id and uncompared-file reporting on the success line that AGENTS.md and `docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md` already specify. Exit codes: 0 clean, 1 with named defects, 3 unmeasurable. `pnpm test` invokes the script directly; the shebang carries the permission flags (read limited to the two corpus files, run limited to `git`); the `@std/yaml` dependency is declared in `deno.json` imports. + +**Article III surgery** + +- R2. CONST-T1 is removed from `CONSTITUTION-ARTICLES.md`; the number is never reused; no corpus text prescribes trophy layers, widths, or mocked composition as doctrine. +- R3. CONST-T5 is removed; "characterization" appears nowhere in the corpus as a standing duty. +- R4. CONST-T2 is removed; its blanket property mandate does not survive in any reworded rule (see KTD1 for why the replacement takes a fresh id and a fresh title). +- R5. CONST-T3 keeps its id and title "Mutation Is the Measure" with a reworded body: the gate covers a named, non-empty, change-relevant mutated set; the `dont` list covers suppression, after-the-fact narrowing, gate-lowering, an empty set, and raw cross-project percentage comparison. +- R6. CONST-T4's text stands byte-identical. +- R7. CONST-T14 "Properties Where the Surface Cannot Reach" is minted: a property is earned when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; a `dont` forbids properties that restate a decision already pinned from above. +- R8. CONST-T8 "The Observer Must See the Fault Class" is minted: each behavior is enrolled in the observer that can see its failure class; `dont` entries forbid orchestrator suites, mixed mutant populations, and substitutes whose expected value derives from the single implementation they replace. +- R9. CONST-T13 "Mutation Also Grades the Tests" is minted: a run whose mutants all died fails when an authored property file defends nothing the rest of the suite does not; opting out happens in the mutation config, never by deleting the named file. +- R10. CONST-T9 "Pin the Published Contract Before You Delete a Path" is minted: observables are pinned with an expected side independent of the implementation under change; a persisted gold survives a rewrite only when externally authored, independently gated, and cheap to re-bless. +- R11. CONST-T10 "The Oracle Is Not the System Under Test" is minted: every assertion draws on an oracle the SUT did not produce, and a hand-written refusal survives beside generated accept-laws at any specifiable refusal boundary. +- R12. CONST-T11 "Snapshots and Differentials Are Published-Surface Oracles" is minted: snapshot and differential fixtures are produced only through the package's published export map. +- R13. CONST-T12 "Altitude Is What the Test Calls" is minted: a test's observer is decided by what it imports and invokes, never by filename, folder, or suffix; the check is lint. +- R14. CONST-E5 "A Gate's Key Is Recomputed, Never Reported" is minted at the end of Article III's rules block (retrieved placement — see KTD3): gates key on recomputation from source bytes, a compiler verdict, or a rehash; a gate whose verdict the gated agent can produce or observe is unverified until an independent channel confirms it. + +**Resident law** + +- R15. CONST-S4's structural-rebuild bullet cites CONST-T9 instead of CONST-T5, and its `check` clause requires a CONST-T9 pin on every published path a rebuild deletes. No other S4 text changes. + +**Prose reconciliation** + +- R16. AGENTS.md's file-choice paragraph cites CONST-T14 as its review-gated artifact-announced testing example instead of CONST-T2; the "28 of 34 rules" count sentence is recomputed against the amended corpus (33 rules) and restated with measured numbers; its vacated-id reporting claim is now backed by the gate's real behavior. +- R17. README.md's Article III row describes the new doctrine without naming the Testing Trophy, and the "Rules: 34" badge is recomputed to 33. + +**Refusals** + +- R19. No rule YAML names a tool, a consumer repository, a language stack, or a filename suffix. +- R20. No Testing Trophy diagram, width metaphor, or layer table survives anywhere in the corpus. + +### Success Criteria + +- `pnpm test` exits 0 and its success line reports the post-amendment corpus (33 rules across both files). +- After the corpus commit, `pnpm test --against ` exits 0, prints no reassignment, and names exactly CONST-T1, CONST-T2, and CONST-T5 as vacated. +- Against a pre-#9 revision, the same gate names exactly CONST-E1, CONST-E2, CONST-E3, CONST-E4, CONST-G1, and CONST-G2 as vacated. +- A probe that adds a citation to a vacated id makes the gate fail naming the dangling citation; the probe is reverted after the demonstration. +- A grep sweep over the corpus, AGENTS.md, README.md, and `scripts/` finds no reference to CONST-T1, CONST-T2, CONST-T5, "Testing Trophy", or characterization-as-duty. + +### Scope Boundaries + +In scope: `CONSTITUTION-ARTICLES.md`, `CONSTITUTION.md`, `scripts/validate-constitution.py` (deleted), `scripts/validate-constitution.ts` (new), `deno.json` (new), `package.json` (test script), `AGENTS.md`, `README.md`, and this plan. + +### Deferred to Follow-Up Work + +- The org tree's `WATCHDOG.md` police-checks "T1 = trophy, T5 = pin with characterization" (measured in the invoking conversation); it enforces vacated obligations after this amendment and must be updated in that repo after its next vendor pull. Other consumer repos are unmeasured; vacancy failure at their pulls is loud by design. A tracker ticket for this follow-up is filed by the shipping pipeline. +- Restructuring AGENTS.md's measured-count prose so future amendments drift less is a separate docs decision. + +Outside this amendment's identity: validator schema, family registry, or gate registration changes (none needed — E and T are registered, `review`/`lint`/`mutation` are registered gates); renumbering to close id gaps (forbidden by the corpus's own rule); restoring the Application section or the Preamble that commits #9/#10 removed. + +--- + +## Planning Contract + +### Key Technical Decisions + +- KTD1. **CONST-T2 is vacated; its replacement is minted as CONST-T14 with a distinct title.** The narrowed grant forbids what the old `do` required — blanket property coverage of the core violates the new `dont` — so the obligation narrowed, and the corpus's id-surgery table assigns a narrowed obligation a new id. The `--against` reassignment check compares titles, and only for ids present at both revisions, so it is structurally blind to a body-narrowing under a kept id; id discipline for body-level obligation change is therefore review-only, which is exactly why the narrowed grant takes a fresh id and a fresh title ("Properties Where the Surface Cannot Reach") — no reader can mistake the new rule for a light edit of the old. AGENTS.md's prose citation of CONST-T2 re-points to CONST-T14 (R16), which is likewise review-gated and artifact-announced. +- KTD2. **CONST-T3 keeps its id and title.** The reword adds only loophole-closers (empty set, after-the-fact narrowing, raw-percentage comparison) and renames the gated set from "the core" to a named change-relevant set; nothing the old text required is now forbidden, so an agent compliant with the old text still complies. Same obligation, sharper instrument hygiene. +- KTD3. **CONST-E5 is retrieved law at the end of Article III, not resident law.** Commit ebd9b10 (#9) removed the resident Application section — CONST-G1/E1–E4/G2 — with the rationale that gate accumulation had produced cargo-culted checks; recreating a resident enforcement block for one rule reopens exactly that. The resident file is now conduct-only by direction of #9, and CONST-E5 is not conduct. It belongs where the work announces it: an agent authoring or judging a gate holds a verification artifact, which is Article III's retrieval trigger. Family follows about-ness, not placement — the corpus's own precedent is CONST-P3 (purity) living in Article II. E5 is the next free E number because E1–E4, though deleted in #9, are permanently vacant. Grounding: this repo's vacuous-pass learning ("never key a gate on a value its own author supplies") and dossier H12 (enforced gates bind, advisory surfacing is inert; an instrument correlated with the work can manufacture a false verdict). The rule stops at accountability — a recomputed key plus an independent confirmation channel — because binding by a machine gate an agent merely observes is unmeasured (dossier open question Q6). +- KTD4. **Numbering follows the settled mint set; the bundle is one doctrine revision.** CONST-T8 through CONST-T13 per the specification; CONST-T14 for the property grant; CONST-E5 in the E family. T6 and T7 remain vacant alongside T1, T2, and T5; gaps are free and a citation into a gap fails loudly. The eight mints ship as one amendment deliberately: the rules are mutually dependent — T9's pinning assumes T10's oracle independence, T11's snapshots and T12's altitude both key on T8's observer placement, and CONST-E5 disciplines the gates the T-family names — so splitting them would mint rules that cite rules that do not exist yet. +- KTD5. **Four commits, pushed together as one PR: plan checkpoint, validator, corpus, prose.** The Deno gate lands before the corpus commit so the amendment it judges is verified by a gate that already reports vacancies; the corpus commit carries both constitution files; prose lands last. The intermediate states exist only on the local branch for minutes — CI and consumer repos see only the PR head. The corpus/prose divergence window never publishes, and each layer stays auditable on its own. +- KTD6. **The gate is Deno, not Python.** Ecosystem tooling law is Deno-only for code, and the user directed it for this run. The rewrite is a port, not a redesign: every check the Python gate made is preserved, and the one addition is the vacated-id/uncompared-file reporting the repo's documentation already specifies (AGENTS.md's gate paragraph; the vacuous-pass learning's success-line shape). The `@std/yaml` parser is declared in `deno.json` and imported by bare name; the shebang carries the least-privilege flags so callers invoke the script directly. + +### Id surgery map + +| Action | Id | Title | +|---|---|---| +| Vacate | CONST-T1 | The Testing Trophy | +| Vacate | CONST-T2 | Properties Over Examples (blanket mandate) | +| Vacate | CONST-T5 | Pin Behavior Before You Rebuild | +| Keep id, reword body | CONST-T3 | Mutation Is the Measure | +| Keep, byte-identical | CONST-T4 | Behavior Lives Where the Mutator Sees It | +| Mint | CONST-T8 | The Observer Must See the Fault Class | +| Mint | CONST-T9 | Pin the Published Contract Before You Delete a Path | +| Mint | CONST-T10 | The Oracle Is Not the System Under Test | +| Mint | CONST-T11 | Snapshots and Differentials Are Published-Surface Oracles | +| Mint | CONST-T12 | Altitude Is What the Test Calls | +| Mint | CONST-T13 | Mutation Also Grades the Tests | +| Mint | CONST-T14 | Properties Where the Surface Cannot Reach | +| Mint | CONST-E5 | A Gate's Key Is Recomputed, Never Reported | + +Base: 28 rules (CONSTITUTION.md 7 + CONSTITUTION-ARTICLES.md 21). Post-amendment: 28 − 3 + 8 = 33. + +### Assumptions + +- The org tree's enforcing mechanisms are taken as specified in the invoking conversation; they were measured in that repo and are not re-verified from here. +- The dossier's adjudications are taken as the doctrine authority as delivered; primary-source re-derivation is out of scope for this amendment. +- CONST-T2's vacancy (KTD1) is resolved by the corpus's own id-surgery table after the question was declined in dialogue; the user redirected nothing when authorizing autonomous shipping. +- The two inference-grade `harm` fields (the empty-set guard's catching behavior, machine-gate binding on an observing agent) are stated as rationale, not measured findings, and the rules that carry them are framed as guards and accountability requirements. +- The locked-surface guidance for verification scripts is overridden for the gate file itself by direct user direction (Python out, Deno in); the override is declared in the validator commit body. + +### Risks & Dependencies + +- Consumer repos citing CONST-T1, CONST-T2, or CONST-T5 fail loudly at their next vendor pull. That is the intended vacancy behavior — a citation into a gap resolves to nothing, which the gate surfaces by name. One consumer is known to cite them (the org tree's `WATCHDOG.md`, per the invoking conversation); its repair is the deferred follow-up, not a shim here. +- AGENTS.md's measured-count prose will drift again on the next amendment. Accepted; the amendment checklist already requires recomputation on delete/split/merge/re-scope commits. +- The first `pnpm test` run after the Deno rewrite fetches `@std/yaml` from JSR (as the old command fetched PyPI); subsequent runs are cached. + +### System-Wide Impact + +Every consumer repo's agents receive the amended doctrine at their next vendor pull; the constitution repo's own harness (AGENTS.md guidance, README's doctrine table and badge) changes with it. The gate moves from Python to Deno — consumer repos that invoke the vendored validator directly inherit a Deno dependency; those using `pnpm test` inherit it transparently. + +--- + +## Implementation Units + +### U5. Deno corpus gate + +**Goal:** The corpus gate is a Deno script implementing the full documented behavior, including vacated-id and uncompared-file reporting; the Python file is gone. + +**Requirements:** R1 + +**Dependencies:** none (executes first) + +**Files:** `scripts/validate-constitution.ts` (new), `scripts/validate-constitution.py` (deleted), `deno.json` (new), `package.json` (test script) + +**Approach:** + +1. Read the Python gate in full and port every check: two-file coverage, fenced-block extraction and per-block parse errors, declared-vs-parsed id accounting, schema fields, gate values, family registry, duplicate ids, `do`/`dont` string-or-string-list shape, `example` map-of-strings shape, cross-file dangling citations, `--against` title-keyed reassignment. +2. Add the success-line reporting: ids vacated since the revision, and corpus paths absent at it — neither fails the run; both are named. +3. Declare `@std/yaml` in `deno.json` imports; point `package.json`'s `test` script at the script path; carry permissions in the shebang (`--allow-read` limited to the two corpus files, `--allow-run=git`); mark the file executable. + +**Patterns to follow:** the reporting shape in the vacuous-pass learning ("what cannot be failed must be reported"); the Python gate's error-message voice. + +**Test scenarios:** + +- Red-first: before the rewrite, `pnpm test --against ebd9b10~1` (captured) prints only `no id reassigned` — six vacated ids unreported. +- After the rewrite, against `ebd9b10~1`: names exactly CONST-E1, CONST-E2, CONST-E3, CONST-E4, CONST-G1, CONST-G2 as vacated; exit 0. +- Against the pre-split revision `28c79e6~1`: names `CONSTITUTION-ARTICLES.md` as not compared; exit 0. +- Against the current HEAD: names nothing vacated; exit 0. +- Without `--against`: output shape unchanged from the Python gate's, exit 0. +- Dangling-citation fixture: a temporary citation to a vacated id fails the run naming it. + +**Verification:** the six scenarios pass; `deno lint` on the new script is clean. + +### U1. Article III surgery + +**Goal:** Article III states observer-fit, oracle-independence, mutation, pinning, and gate-key doctrine; the trophy and the characterization duty are gone. + +**Requirements:** R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, R13, R14, R19, R20 + +**Dependencies:** none on U5 (independent content; ordering matters only for U4's evidence) + +**Files:** `CONSTITUTION-ARTICLES.md` + +**Approach:** + +1. Read the current Article III block before editing. +2. Replace the rules block with the Appendix A block verbatim: CONST-T8, CONST-T14, CONST-T3, CONST-T13, CONST-T4, CONST-T9, CONST-T10, CONST-T11, CONST-T12, then CONST-E5 as the terminal rule. CONST-T4's entry stays byte-identical to the current text. +3. Article III's heading and the surrounding article structure do not change. + +**Patterns to follow:** the existing rule YAML schema (`id`, `title`, `gate`, `do`, `dont`, `harm`, `check`); gate values limited to the registered set. + +**Test scenarios:** (all run at the corpus-commit boundary, after U2's S4 repoint — the dangling T5 citation in CONSTITUTION.md keeps the gate red until then) + +- Validator schema: `pnpm test` exits 0; the success line reports 33 rules. +- Dangling-citation probe: temporarily add a citation to CONST-T5 in a corpus file; the gate fails naming the dangling citation; revert the probe and confirm green. +- Stale-doctrine sweep: a grep over the corpus for `CONST-T1`, `CONST-T2`, `CONST-T5`, `Testing Trophy`, and `characterization` returns nothing. + +**Verification:** the three scenarios pass at the corpus-commit boundary. + +### U2. S4 repoint + +**Goal:** CONST-S4 pins structural rebuilds with CONST-T9 instead of the vacated CONST-T5. + +**Requirements:** R15 + +**Dependencies:** U1 (the citation target must exist) + +**Files:** `CONSTITUTION.md` + +**Approach:** + +1. Read the current CONST-S4 block before editing and confirm both find-patterns are present verbatim. +2. In the structural-rebuild bullet, replace "behaviour pinned with characterization tests first, CONST-T5" with "published contract pinned first (CONST-T9)". +3. In the `check` clause, replace "a structural rebuild ships its characterization tests" with "a structural rebuild ships a CONST-T9 pin on every published path it deletes". +4. If either find-pattern misses, stop for amendment repair — do not approximate the edit. + +**Patterns to follow:** the resident Article V rule shape; no other S4 field changes. + +**Test scenarios:** + +- A grep of `CONSTITUTION.md` for CONST-T5 returns nothing; a grep for CONST-T9 returns the S4 citation. +- `pnpm test` exits 0 at the corpus-commit boundary (with U1). + +**Verification:** both scenarios pass. + +### U3. Harness prose reconciliation + +**Goal:** The harness and front-page prose teach the amended doctrine, cite only live rules, and carry measured numbers. + +**Requirements:** R16, R17 + +**Dependencies:** U1, U2, U5 (prose must match final rule text, ids, and gate behavior) + +**Files:** `AGENTS.md`, `README.md` + +**Approach:** + +1. In AGENTS.md's file-choice paragraph, replace the CONST-T2 example citation with CONST-T14. +2. In AGENTS.md's gate-explanation paragraph, recompute the inbound-citation count and the corpus total (33) against the amended files and restate the sentence with the measured numbers; its vacated-reporting claim now names real behavior. +3. In README.md's doctrine table, rewrite the Article III row to describe observer-fit placement, properties by narrow grant, and mutation as the measure; recompute the "Rules" badge from 34 to 33. + +**Patterns to follow:** AGENTS.md's existing measured-claim style; README.md's table-row and badge shapes. + +**Test scenarios:** + +- Repo-wide grep sweep (excluding `.git`, lock files, and this plan) for CONST-T1, CONST-T2, CONST-T5, "Testing Trophy", and characterization-as-duty returns nothing. +- The recomputed AGENTS.md and README numbers match fresh counts run at implementation time, not numbers carried from this plan. + +**Verification:** both scenarios pass. + +### U4. Verification battery and commits + +**Goal:** Evidence is recorded and the work lands as four conventional commits. + +**Requirements:** all + +**Dependencies:** U5, U1, U2, U3 + +**Files:** none new + +**Approach:** + +1. Run the full Verification Contract in order. +2. Commit the plan checkpoint (`docs(plans):`) first; then the Deno gate (`feat(scripts)!:`) with the locked-surface override declared in the body; then the corpus change (U1 + U2, `refactor(constitution)!:`); then the prose (U3, `docs:`). +3. Run commitlint over all four commits. + +**Test scenarios:** + +- All four commits pass `pnpm exec commitlint --from HEAD~4`. +- The working tree is clean after the commits. + +**Verification:** the Verification Contract's exit criteria all hold. + +--- + +## Verification Contract + +| Check | Command / method | Exit criterion | +|---|---|---| +| Corpus gate | `pnpm test` | Exit 0; success line reports 33 rules across both corpus files | +| Vacancy gate | `pnpm test --against ` | Exit 0; no reassignment; names exactly CONST-T1, CONST-T2, CONST-T5 vacated | +| Backfill fixture | `pnpm test --against ebd9b10~1` | Exit 0; names exactly the six #9-removed ids vacated | +| Uncompared fixture | `pnpm test --against 28c79e6~1` | Exit 0; names `CONSTITUTION-ARTICLES.md` not compared | +| Dangling-citation probe | temporary citation to a vacated id, then `pnpm test` | Fails naming the citation; probe reverted; gate green again | +| Stale-doctrine sweep | grep corpus, AGENTS.md, README.md, `scripts/` for vacated ids and retired doctrine names | Zero matches | +| Commit format | `pnpm exec commitlint --from HEAD~4` | Exit 0 for all four commits | +| Clean state | `git status --porcelain` | Empty after commits | + +--- + +## Definition of Done + +- All requirements R1–R20 hold in the tree. +- The Verification Contract passes in full, in the current session. +- The four commits (plan, validator, corpus, prose) are separate, conventional, and pushed together in one PR. +- No consumer-repo file is touched; the org-tree watchdog follow-up remains deferred with its tracker ticket filed. +- Working tree is clean. + +--- + +## Appendix A — Article III replacement block + +Normative payload for U1. YAML block content, replacing the current Article III `rules:` sequence: + +```yaml +rules: + - id: CONST-T8 + title: The Observer Must See the Fault Class + gate: review + do: enroll each behavior in the observer that can see its failure class — a pure decision is read by properties and by mutation of that decision; a published operation is read by tests that call only exported names; a shell that only translates is read by those exported-name tests, not by a suite of its own + dont: + - give the orchestrator its own test suite — extract the decision or live with the exported-name test + - enroll adapters, codecs, or wiring in the same mutant set as decisions + - treat a substitute whose expected value derives from the single implementation it replaces as an observer + harm: the wrong instrument reports coverage and measures nothing; mocked composition certifies wiring; a fat middle grows where decisions and I/O stay tangled + check: review — every new test names the surface it binds (decision or published export) and does not import a non-exported symbol to assert it + - id: CONST-T14 + title: Properties Where the Surface Cannot Reach + gate: review + do: prove a pure decision with a property when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; the type is the generator + dont: + - cover the core with hand-picked example unit tests + - write a property for a decision already fully pinned from above just because the decision is important + harm: a green suite that tests only the cases you imagined; or a property farm that restates the public contract and dies with it + check: review — each authored property names the universal the public surface cannot reach + - id: CONST-T3 + title: Mutation Is the Measure + gate: mutation + do: gate a named, change-relevant mutated set at a perfect kill score; the set names the behavior it covers, and its scope is a cost decision, never a fault-majority claim; kill a survivor with a sharper property or by deleting the dead branch it exploits + dont: + - reach the number by a suppression comment + - reach the number by narrowing the mutated set after the fact + - reach the number by lowering the gate + - let an empty mutated set pass + - treat a raw mutation percentage as comparable across changes or codebases + harm: a score certifying tests that notice nothing; an empty or author-shrunk set passing vacuously + check: mutation gate (break = 100) on the declared mutated set; lint banning suppression, scope-narrowing, and an empty set + - id: CONST-T13 + title: Mutation Also Grades the Tests + gate: mutation + do: fail a run whose mutants all died if an authored property file defends nothing the rest of the suite does not; opt out in the mutation config, never by deleting the file the gate named + dont: + - treat a perfect mutant score as proof every test pulled its weight + - accuse a file that covered an unattributed kill + harm: toothless properties accumulate; deleting them to silence the gate removes the only named contract + check: mutation — the test-set verdict is part of the same run as the score + - id: CONST-T4 + title: Behavior Lives Where the Mutator Sees It + gate: lint + do: put any code that can be wrong (transform, check, branch) in a file the mutator covers + dont: place behavior in a declaration file (types, schemas, constant data), excluded from mutation + harm: a bug hidden behind a perfect score, in a file nothing mutates + check: lint — declaration files contain no behavior + - id: CONST-T9 + title: Pin the Published Contract Before You Delete a Path + gate: review + do: before removing or replacing a published operation, pin its observables (value, error variant, serialized document, process result) with examples or properties whose expected side is not the implementation under change; if the old operation still runs, compare old and new on the same published inputs until they agree, then delete old + dont: + - pin private functions + - derive expected values by running the implementation under change + - treat a mutation or property score as proof a deleted published capability still exists — those are blind to absence + - leave a persisted gold after the old path is gone unless the gold is externally authored, independently gated, and cheap to re-bless + harm: a rebuild silently drops a capability; same-session gold blesses the bug; a clean score after a delete is a silent regression + check: review — pins call only published names; each expected value names an independent source (spec clause, prior published major, second implementation, or a hand-written oracle next to the constructor) + - id: CONST-T10 + title: The Oracle Is Not the System Under Test + gate: review + do: every assertion has an oracle the SUT did not produce — a spec literal, a fixture not generated by importing the module, a law relating two views of the same value, or a second implementation; generated round-trip laws on a type cover what the type accepts and nothing it should reject, so a hand-written refusal survives beside them at any specifiable refusal boundary + dont: + - compute expected by calling the SUT + - assert collaborator call graphs + - treat generated accept-laws as full coverage of a refinement + harm: a green suite that cannot fail when the behavior is wrong; widening a refinement leaves generated laws green + check: review — plus sabotage: after green, break one core law and one published field; at least one test must go red + - id: CONST-T11 + title: Snapshots and Differentials Are Published-Surface Oracles + gate: lint + do: snapshot only canonicalized published output; compare two implementations only of the same published operation (or a prior published major against current) + dont: + - snapshot or compare private helpers, mappers, or unexported modules + - snapshot a value small enough to be a property or a named example + harm: tests that fail on refactors callers cannot see and pass on contract breaks they can + check: lint — snapshot and differential fixtures are produced only through the package's published export map + - id: CONST-T12 + title: Altitude Is What the Test Calls + gate: lint + do: decide a test's observer from what it imports and invokes — published names or a decision under mutation — never from a filename, folder, or suffix + dont: key which doctrine applies to a test on a label the author can rename + harm: a rename silently un-enrolls the file from its observer; the absence reads as coverage + check: lint — no rule that selects tests by filename suffix + - id: CONST-E5 + title: A Gate's Key Is Recomputed, Never Reported + gate: review + do: + - key every gate on a recomputation from source bytes, a compiler verdict, or a rehash — never on a field the gated work's author supplied; when a gate reads a field, recompute that field in the same run + - treat a gate whose verdict the gated agent can produce or observe as unverified until an independent channel confirms it — an instrument the agent does not control, or review by someone who is not the gated agent + dont: + - accept a self-reported field, a presence flag, a metadata suffix, or a comment as evidence a property holds + - treat a mechanical gate's green as self-certifying + harm: a check keyed on author-supplied values passes everything and catches nothing, and the green then masks the broken invariant the gate exists to catch; an instrument correlated with the work under test can manufacture a verdict no single observer catches + check: review — each gate names the recomputation it runs and the independent channel that confirms its verdict +``` + +--- + +## Appendix B — CONST-S4 edits + +Normative payload for U2, verified verbatim against the current corpus. Two edits, complete and final: + +- Structural-rebuild bullet: "behaviour pinned with characterization tests first, CONST-T5" becomes "published contract pinned first (CONST-T9)". +- Check clause: "a structural rebuild ships its characterization tests" becomes "a structural rebuild ships a CONST-T9 pin on every published path it deletes". + +--- + +## Sources & Research + +- H1–H12 verdict dossier (delivered in the invoking conversation): doctrine authority. Load-bearing verdicts: H1 (same-session contamination), H2 (oracle independence over surface count), H5 (change-relevant mutation; decision-only refuted; empty-set guard load-bearing), H7 (refusal complement), H8 (seam authority), H11 (external-gated gold), H12 (gates bind; accountability reading). Independently spot-confirmed against primary literature (arXiv:2607.05139, arXiv:2410.21136, EMSE'22 commit-relevant mutants). +- Org-mechanism specification (delivered in the invoking conversation): the enforcing tree's mechanisms, the mint set CONST-T8 through CONST-T13, and the measured consumer citation (org tree WATCHDOG.md). +- Python gate `scripts/validate-constitution.py`, read in full: the port source — schema fields, family registry, cross-file citation resolution, title-keyed `--against` reassignment over ids present at both revisions. +- AGENTS.md: id-surgery table (obligation identity), family registry, file-choice rule, the gate-explanation paragraph whose counts R16 recomputes and whose vacated-reporting claim the Deno gate makes true. +- `docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md`: grounding for CONST-E5 and the `--against` reporting shape — a gate keyed on author-supplied values certifies nothing; what cannot be failed must be reported. +- Tooling: Deno-only ecosystem law (software-wiki harness convention); `@std/yaml` (`jsr:@std/yaml`, single-document `parse`, safe for untrusted input — docs.deno.com/runtime/reference/std/yaml). +- Corpus base: `origin/main` at dd5722a (28 rules; commits ebd9b10/#9 and dd5722a/#10 removed the Application section and the Preamble). diff --git a/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md b/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md new file mode 100644 index 0000000..287e44d --- /dev/null +++ b/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md @@ -0,0 +1,49 @@ +--- +title: "feat(plugin): create OMP constitution TTSR rules plugin" +date: 2026-08-30 +category: feature +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +execution: code +product_contract_source: ce-plan +--- + +# Goal Capsule + +Add a standard OMP plugin package (`@systemfsoftware/omp-plugin-constitution` under `packages/omp-plugin/` or `plugins/constitution/`) that defines Time-Traveling Stream Rules (TTSR) in markdown (`rules/*.md`). The rules trigger on edits/writes to source code and tests, enforcing strict adherence to the Supreme Engineering Constitution (`CONSTITUTION.md` and `CONSTITUTION-ARTICLES.md` Articles I–V). Any violation in review or source generation is flagged immediately with automatic P0 severity and actionable remediation. + +# Product Contract + +## 1. Scope and Objective +- Package an OMP plugin adhering to Claude/OMP plugin discovery specifications (`.omp-plugin/plugin.json`, `rules/`, `package.json`, `README.md`). +- Define targeted TTSR stream rules (`rules/*.md`) covering: + 1. `constitution-pure-core.md`: Enforces Article I (purity, tagged error variants, branded types, cyclomatic complexity 1, no primitive obsession, no boolean/status string errors, no null states). + 2. `constitution-boundary.md`: Enforces Article II (functional core / imperative shell, effects as lazy values, I/O sandwich order, inward dependencies, decode never unchecked cast `as any`/`as unknown`). + 3. `constitution-verification.md`: Enforces Article III (public export API testing, mutation testing on decisions, narrow property grants, independent oracles, no mocked intermediate single-implementation glue code, no tautological/characterization tests). + 4. `constitution-conduct-and-review.md`: Enforces Article V & Governance (automatic P0 on constitutional breaches with zero appeals unless declared under CONST-W3, root cause over expedient patch, subtract before add, scope discipline). +- Each rule carries frontmatter with `condition`, `scope` (`tool:edit(...)`, `tool:write(...)`, `tool:ast_edit(...)`), and `interruptMode: tool-only` or `always`. +- Update project manifest (`package.json`) and root test validation to ensure all rules are valid YAML frontmatter and tested against `omp ttsr test` (or regex validator). + +## 2. Requirements & Invariants +- **R1 (Plugin Discovery)**: Plugin manifest `.omp-plugin/plugin.json` and `.claude-plugin/plugin.json` properly identify the plugin name, version, and author metadata. +- **R2 (TTSR Rule Format)**: Every rule file in `rules/` has valid YAML frontmatter with `description`, `condition`, `scope`, and markdown instructions quoting the exact rule IDs (`CONST-P1`, `CONST-D1`, `CONST-B1`, `CONST-T8`, `CONST-G3`, etc.). +- **R3 (Zero Hallucination / Grounded)**: Rule text cites exact IDs and principles from the repository's `CONSTITUTION.md` and `CONSTITUTION-ARTICLES.md`. +- **R4 (Verification)**: `pnpm test` (and any new plugin test scripts) must execute cleanly, validating all YAML blocks across the repository. + +# Implementation Units + +- [ ] **U1: Scaffold OMP Plugin Manifest & Directory Structure** + - **Goal**: Create the plugin layout under `plugins/constitution/` or root plugin metadata with `.omp-plugin/plugin.json`, `.claude-plugin/plugin.json`, and `package.json`. + - **Files**: `plugins/constitution/.omp-plugin/plugin.json`, `plugins/constitution/.claude-plugin/plugin.json`, `plugins/constitution/package.json`, `plugins/constitution/README.md`. + +- [ ] **U2: Implement Article I & II TTSR Rules (Core & Boundary)** + - **Goal**: Author `rules/constitution-pure-core.md` and `rules/constitution-boundary.md` with precise regex and ast conditions for unchecked casts (`as any`, `as unknown`), eager async promises on domain signatures, mutable control flow in pure core, and missing error tagging. + - **Files**: `plugins/constitution/rules/constitution-pure-core.md`, `plugins/constitution/rules/constitution-boundary.md`. + +- [ ] **U3: Implement Article III & V TTSR Rules (Verification & Review P0)** + - **Goal**: Author `rules/constitution-verification.md` and `rules/constitution-conduct-review.md` covering mocked single implementations, tautological assertions, private helper unit tests, and the mandatory P0 automatic review rejection. + - **Files**: `plugins/constitution/rules/constitution-verification.md`, `plugins/constitution/rules/constitution-conduct-review.md`. + +- [ ] **U4: Wire Validation, Test Battery, and Documentation** + - **Goal**: Update root `scripts/validate-constitution.ts` (or add plugin test scripts) to validate the plugin rules, update `README.md` and `AGENTS.md` to document the OMP plugin installation and usage. + - **Files**: `scripts/validate-constitution.ts`, `README.md`, `AGENTS.md`, `package.json`. diff --git a/docs/plans/2026-09-01-0533-retire-the-ttsr-plugin-entirely-plan.md b/docs/plans/2026-09-01-0533-retire-the-ttsr-plugin-entirely-plan.md new file mode 100644 index 0000000..525e4dc --- /dev/null +++ b/docs/plans/2026-09-01-0533-retire-the-ttsr-plugin-entirely-plan.md @@ -0,0 +1,99 @@ +# Plan — Retire the TTSR plugin entirely (supersedes 2026-09-01-0454) + +```yaml +plan: + id: unified-ce-unified-plan/v1 + status: implementation-ready + created: 2026-09-01-05:33 + supersedes: docs/plans/2026-09-01-0454-refactor-restore-single-constitution-plan.md + delivery: one branch (restore-single-document), one PR (#21), own commits +``` + +## Problem + +The 0454 plan kept the plugin alive in reduced form: three rule files as +"residency statements" and `constitution-conduct-review.md` as the one active +interrupt. The user rejected that on first principles: **the constitution is +always on, so every plugin rule is useless.** The plugin's entire function was +to carry law into the agent's context on a trigger; residency (`@CONSTITUTION.md`) +already delivers all of it, always. The conduct-review interrupt is the clearest +case — its body restates `CONST-G3`, `CONST-W3`, `CONST-S1`, `CONST-S4`, +`CONST-S2`, every one of them resident in `CONSTITUTION.md` the moment the +trigger fires. The interrupt injects text the agent already has. + +This is CONST-S4 turned on the restore itself: the statement files were a +helper added where removal does the job. + +## Requirements + +- **R1 — The plugin is gone.** `plugins/` (constitution README, all four rule + files, `.omp-plugin/`, `.claude-plugin/`) is deleted. No manifest, no + statement file, no tombstone README survives (DEL1: remove means remove). +- **R2 — The repo teaches one delivery path.** `README.md` quick start drops + the marketplace/install step; `@CONSTITUTION.md` is the only wiring. + `AGENTS.md` drops "agent harness" from the tooling description. +- **R3 — The ttsr learning is marked historical.** Its subject corpus (the + constitution plugin's condition regexes) no longer exists; the two + transferable traps (PCRE flag translation, backreference tautology matching) + stay for any future TTSR plugin. +- **R4 — Clean cutover.** No live surface references the plugin or any rule + file name: `git grep -nI -e 'plugins/constitution' -e 'constitution-conduct-review' + -e 'constitution-pure-core' -e 'constitution-boundary' + -e 'constitution-verification' -- . ':!docs/plans'` prints zero lines. + (Plan documents are the named exception; this one included.) + +## Implementation units + +- **U1 — Plan supersession** (this document): write, delete 0454, + commit `docs(plans)`. +- **U2 — Plugin removal**: `git rm -r plugins/`; README step 2 removed; + AGENTS.md tooling line corrected. One commit. +- **U3 — Learning annotation**: ttsr doc Context paragraph noting the plugin's + removal and why, lesson kept as transferable. Rides U2's commit — same + cutover, one reviewable unit. +- **U4 — Verification battery**: `deno task test` (gate reads only + `CONSTITUTION.md`; unaffected, must stay green); `deno lint`; the R4 sweep; + `omp plugin doctor` diffed against the pre-deletion baseline (local plugin + state lives under `/root/.omp/plugins` — repo deletion must not change the + doctor verdict shape; stale local links are machine state, out of repo scope). + +## Verification Contract + +| # | Check | Command | Expected | +|---|---|---|---| +| 1 | corpus gate | `deno task test` | `valid: 40 rules across 6 yaml blocks in 1 files, 9 families`, exit 0 | +| 2 | lint | `deno lint` | clean | +| 3 | DEL1 sweep | git grep per R4 | zero lines, read stdout | +| 4 | loader state | `omp plugin doctor` | same summary shape as baseline (5 ok / 1 warn / 1 pre-existing error); no new error introduced by the repo deletion | +| 5 | clean tree | `git status --porcelain` | empty | + +## Key technical decisions + +```yaml +- id: KTD1 + decision: Delete the plugin entirely rather than keep a zero-rule shell. + why: A plugin directory with no rules and a README that says "this plugin + does nothing, residency delivers the law" is a standing tombstone — DEL1's + named anti-pattern. The git history carries the artifact. +- id: KTD2 + decision: The ttsr learning is annotated, not deleted. + why: Solutions docs are durable learnings; the flag-translation trap is + generic TTSR knowledge independent of this plugin's life. Its Context now + names the plugin as removed so no reader treats the corpus as live. +- id: KTD3 + decision: conduct-review's law needs no replacement interrupt. + why: Every rule it restates is resident (CONST-G3, W3, S1, S4, S2). A regex + trigger that injects already-present text is not enforcement, it is noise — + and its 200-char proximity window misfired on benign severity mentions + (flagged in review). +``` + +## Non-goals + +- Consumer-repo follow-ups (subtree pulls, local `omp plugin uninstall`): + machine/consumer state, tracked as follow-ups after merge. +- Gate hardening (issues #19, #20) — already filed, unchanged. + +## Work relationships + +None — terminal correction to the restore PR. diff --git a/docs/plans/2026-09-07-1721-the-maker-never-holds-the-instrument-plan.md b/docs/plans/2026-09-07-1721-the-maker-never-holds-the-instrument-plan.md new file mode 100644 index 0000000..beaf29d --- /dev/null +++ b/docs/plans/2026-09-07-1721-the-maker-never-holds-the-instrument-plan.md @@ -0,0 +1,131 @@ +# Plan — The maker never holds the instrument (corrects the 26a527c enforcement reasoning) + +```yaml +plan: + id: unified-ce-unified-plan/v1 + status: implementation-ready + created: 2026-09-07-1721 + delivery: one branch (doctrine/maker-never-holds-the-instrument), one PR, own commits +``` + +## Problem + +Commit `26a527c` ("restore the single resident document") merged the corpus +correctly but reasoned about enforcement incorrectly, three ways: + +1. **It minted enforcement law in the maker's voice.** CONST-E6 ("make any + principle that can fail a command fail that command") instructs the agent + doing the work to construct the machinery that judges that work. The + settled doctrine (wiki canon `reviewer-agent-separation`, ten sources) is + the opposite: the maker cannot be the sole checker; the check must be + structurally separated. The reward-tampering literature measures the + exploit class (arXiv:2406.10162, arXiv:2609.00069). +2. **It deleted the refusal channel on a conflation.** The TTSR plugin was + retired because "an interrupt can only inject text the agent already + has." An interrupt that re-injects resident text is redundant; an + interrupt that *refuses* the violating write re-fires at the decision + point — after compaction, at turn 40 — which residency cannot do + (wiki `compelled-retrieval-gate-limits`, `pin-or-gate` measurements). +3. **It flipped the corpus gate inside the same commit as the corpus it + gates** ("no legal intermediate commit exists") — the expedience plea the + evaluator-boundary doctrine exists to refuse. + +The residency merge itself stands: the maker's law is resident, single, and +always-on. What changes is who enforcement doctrine is for and who may +touch the instruments. + +## Doctrine + +The constitution is the maker's law — obligations written to the agent doing +the work. Instrument doctrine (how gates are designed, keyed, enrolled, and +changed) is not maker law: it moves to `ENFORCEMENT.md`, read by the channel +that owns the instruments and never loaded into the maker's context. The +maker's side of the boundary is one rule, action-keyed so no role inference +is needed: whatever your change must pass is read-only to you; you cannot +enumerate your graders, so the ban attaches to the judgment surface class, +not to whether you believe it grades this task. + +## Requirements + +- **R1 — Retire CONST-E6** (Prefer the Gate) and **CONST-E8** (The Evaluator + Is Not the Agent's to Edit): both permit the maker to author or edit the + instrument (E6 by instruction, E8 by its own-commit exception). Ids vacate + forever; citations to them must resolve nowhere in the corpus. +- **R2 — Mint CONST-E9** (Never Edit What Grades Your Work) in the + Application section, action-keyed: the ban attaches to the surface class + (lint configs, thresholds, baselines, CI checks, rubrics, validators), and + a needed gate is a proposal to the owner and a wait, never a build by the + graded. +- **R3 — Repoint CONST-G5**: the gate stays the final word; disagreements + escalate to the channel that owns the instrument, never resolved by the + maker editing the gate. +- **R4 — Vacate CONST-E5** from the maker corpus; its recomputation doctrine + continues in `ENFORCEMENT.md` for the instrument owner. +- **R5 — Write `ENFORCEMENT.md`**: the ladder (type > command > refusal at + the decision point > resident prose as the floor), gate-design law, + instrument-change discipline (land alone, red before green, other hands), + enrollment law (clean tree, no baselines, warn is dominated, carriage + proven at the published surface), and the historical correction to the + 2026-09-01 rationale. +- **R6 — Reconcile prose**: `CONCEPTS.md` residency entry, `AGENTS.md` corpus + section and E-family row, `README.md` counts and quick start, + `scripts/validate-constitution.ts` docstring, and annotations on the two + solutions docs that cite the retired ids or the superseded rationale. +- **R7 — No new tests.** The admission gate ran (skill://choose-test-layer + step 0): this amendment proposes none; verification is the repo's + pre-existing corpus gate, which has real non-test consumers (CI and the + pre-commit hook). + +## Verification Contract + +| # | Check | Command | Expected | +|---|---|---|---| +| 1 | corpus gate | `deno task test` | `valid: 38 rules across 6 yaml blocks in 1 files, 9 families`, exit 0 | +| 2 | reassignment | `deno task test --against 26a527c` | E5/E6/E8 reported vacated, E9 minted, no reassignment errors | +| 3 | citation sweep | `grep -nE 'CONST-E[568]' CONSTITUTION.md` | zero lines | +| 4 | lint | `deno lint` | clean | +| 5 | clean tree | `git status --porcelain` | empty after commits | + +## Key technical decisions + +```yaml +- id: KTD1 + decision: Split by audience, not by presence. + why: The pre-26a527c resident/retrieved split failed on presence; that + failure is dead and stays dead. The instrument doctrine's reader is the + instrument owner at build time — a moment where retrieval is correct. + (session-settled: user-directed — "the constitution should be for the + maker only".) +- id: KTD2 + decision: E9 is action-keyed ("never edit what grades your work"), not + role-keyed. + why: The document is always-on for every reader — makers, reviewers, + advisors — and the reader cannot be relied on to know its role or to + enumerate its graders. The act is recognizable in the moment; the role + is not. (session-settled: user-directed.) +- id: KTD3 + decision: The validator's docstring is repointed, its behavior unchanged. + why: The corpus gate is the instrument that grades this amendment; E9 + forbids editing it in service of this work. The docstring's retired-id + citation is prose repair, not behavior change. +- id: KTD4 + decision: No gate change accompanies the corpus change. + why: 26a527c's same-commit gate flip is the violation being corrected; + this amendment ships with the gate's behavior untouched. +``` + +## Research record + +- Wiki (taste, canon band): `reviewer-agent-separation` (the maker cannot be + the sole checker; the check must be structurally separated), + `form-checking-gate-is-a-ritual` (the governed party writing the checked + field has no independent witness), `compelled-retrieval-gate-limits` + (presence is not conformance), `restraint-gates-must-be-emission-gated` + (a reminder is not a refusal). +- Web (substance): arXiv:2406.10162 (reward tampering), arXiv:2609.00069 + (harness tampering in self-improving agents), arXiv:2609.02246 + (deterministic guardrails for self-improving agents). +- Destructive review: three assumptions surfaced (separation doctrine's + warrant band; residency's narrowed survival; the non-resident doctrine + file's audience-moment distinction); lens: band vs weakest atom plus claim + vs bytes; all three survive, the second narrowed. diff --git a/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md b/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md new file mode 100644 index 0000000..532d3a0 --- /dev/null +++ b/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md @@ -0,0 +1,195 @@ +--- +title: A gate can go green because it stopped looking +date: 2026-08-18 +last_updated: 2026-09-01 +category: architecture-patterns +module: constitution corpus validator +problem_type: architecture_pattern +component: tooling +severity: high +applies_when: + - A gate's input set is named in the gate rather than derived from the tree + - A single-artifact check is widened to cover several artifacts + - A merge-blocking check reports a count it computed itself + - Restructuring a corpus the repo's own pre-commit gate certifies + - Authoring cross-revision fixtures after narrowing the gate's input set +tags: [gate-design, vacuous-pass, verification, fail-closed, known-bad-fixture, corpus-surgery, atomic-commit] +--- + +# A gate can go green because it stopped looking + +## Context + +The constitution's format gate validated one markdown file whose path was a module-level +constant. The law was then split across two files. The gate's logic was untouched and +entirely correct — it parsed, it compared declared rule ids against parsed rule ids, it +found no discrepancy, it exited 0. It was reporting on a third of the rules. + +Nothing in the output distinguished that from a healthy corpus. The same run over the +whole corpus and the run over a third of it print the same shape, differing only in a +number nobody had a baseline for. + +Two further shapes of the same defect surfaced during review of the fix, both after the +input set had been widened to a tuple of paths: + +- A path present in the tuple but **absent from disk** was caught by a hard failure. A + path present, parsing cleanly, and declaring **zero rules** was not — emptying the + resident half to a preamble plus two `placeholder: true` blocks printed a valid line + with a smaller count and exited 0. +- The cross-revision arm tolerated a path that did not exist at the older revision, + because a newly created file legitimately has nothing to compare. That tolerance also + swallowed a path **renamed** in the same commit that re-scoped a rule: the older + revision had no such path, so the rule's retitle went uncompared and the run reported +- A fourth shape surfaced when the surgery ran in the other direction — merging the two + files back into one while the gate's input tuple narrowed to a single path. There was + no legal intermediate commit. Landing the gate first left the live citation from the + resident file to an articles-resident rule dangling; landing the corpus first deleted a + path the gate still named, hitting the missing-input hard failure. The pre-commit hook + runs the gate at commit time, so both orderings are unlandable, not merely ugly. The + merge, the input-set change, the permission scopes, and the deletion had to arrive as + one atomic commit for the hook to grade a coherent end state. + +## Guidance + +**Assert the corpus, not only the contents.** A check over a subset is +indistinguishable from a check over the whole unless the gate says which inputs it +measured. Three rules follow. + +1. **A missing input is a hard failure, never a smaller pass.** Absence of an expected + input is a defect in the gate's own configuration, and it must exit non-zero rather + than validate what remains. + +2. **Presence is not contribution.** An input that resolves, parses, and yields nothing + scores exactly like one that yields its half. Require every declared input to + contribute at least one unit of the thing being validated. This is a recomputation + from the bytes — *did this file produce a rule?* — not a number the author supplies. + This rule is the prescription, not a description of the gate that prompted it: that + gate enforced the rule only through its whole-corpus emptiness check, and a single + path parsing to nothing while another contributed would still have passed. A gate + with several named inputs needs the per-path check written in, or shape 2 stays open. + +3. **What cannot be failed must be reported.** Some gaps are legal and failing them + would fire on correct work: a deliberate deletion, a genuinely new input with no + history. Name them on the success line — which inputs were not compared, which + identifiers vacated — so the reader sees the reduced coverage instead of inferring + +**Enumerate the orderings before choosing atomicity.** When the change edits both the +corpus and the gate that certifies it, write out every commit ordering and prove each +non-atomic one fails before accepting that one commit is forced. Then pin the atomic +commit's contents so the hook grades the end state, not a transition: the corpus merge, +the input-set constant, the shebang and task permission scopes, and the file deletion +land together. One site left at the old shape is the failure path. + +**Anchor cross-revision fixtures where the input set was complete.** A `--against` +comparison reads only the paths the gate names — at the compared revision, not just at +HEAD. A vacancy fixture anchored on a revision where part of the corpus lived outside +the current input set measures less than it claims and exits 0 saying nothing: the same +vacuous pass, wearing a diff arm. Anchor on the revision where the current input set +held the entire historical corpus, so the fixture's clean exit actually proves the +declared vacancy. + +**Never key a gate on a value its own author supplies.** The tempting fix for shape 1 is +a pinned expected count. That is a field the author writes, so the gate never runs on the +case it exists to catch. Prefer a key the gate recomputes: a digest over current bytes, a +compiler verdict, a re-derivation from the tree. + +**Prove the widening with a known-bad fixture.** A gate that has only ever been run +against a healthy input has demonstrated that it can print a success line. Run it against +an input that must fail, and read the failure text. Each defect above was found by a +fixture, not by reasoning about the code. + +## Why This Matters + +The failure is silent by construction and lands in the one place nobody reads twice: +output that already says everything is fine. Downstream, the green result is then cited as +evidence the invariant holds — so the gate does not merely fail to catch the defect, it +actively certifies its absence. + +The documentation compounds the harm. Prose asserting a guarantee the gate does not +implement ("a rule dropped in a move fails the gate") trains a maintainer to skip the +manual review the gate is not doing. Two independent reviewers caught that sentence here; +it had been written in the same change that widened the gate, by the author most convinced +the gate was now sound. + +## When to Apply + +- Widening any check from one artifact to several — the moment the input set becomes a + collection, it becomes a thing that can silently shrink. +- Any gate that prints a count it computed itself, with no baseline the reader can check. +- Any check whose failure path is reachable only by a state the repository never normally + reaches; that path has almost certainly never run. +- Reviewing a claim about what a gate enforces. Run the fixture; do not read the code and + agree with it. +- Restructuring a corpus that a merge-blocking hook certifies — merging files, moving + rules, narrowing the input set. Prove the orderings first; atomicity is a conclusion, + not a style. + +## Examples + +Before — a single named input. Correct, and silently correct about a third of the corpus +once the corpus grew: + +```python +PATH = "ONE_FILE.md" +text = open(PATH, encoding="utf-8").read() +# ... one file's worth of checking, exit 0 +``` + +After — the input set is a collection, absence is fatal, and emptiness is fatal: + +```python +PATHS = ("RESIDENT.md", "RETRIEVED.md") + +for p in PATHS: + try: + texts[p] = open(p, encoding="utf-8").read() + except FileNotFoundError: + fail([f"{p}: missing — half a corpus scores exactly like a whole one"]) + +# ... after parsing, every declared input must have produced something +for p in PATHS: + if p not in contributors: + errors.append(f"{p}: parses but declares no rule") +``` + +And what cannot be failed is stated rather than omitted: + +``` +valid: 40 rules across 6 yaml blocks in 1 files, 9 families; + 9 id(s) vacated since 9654836~1: CONST-E1,CONST-E2,CONST-E3,CONST-E4, + CONST-G1,CONST-G2,CONST-T1,CONST-T2,CONST-T5 +``` + +And the fixture-anchoring pair from the reverse surgery — same gate, same narrowing, +different anchor: + +| Anchor revision | Corpus shape at that revision | Result | +|---|---|---| +| a split-era revision | vacated ids lived in the file outside the gate's current input set | exit 0, zero vacancies named — silently measures nothing | +| the pre-split revision | the one named file held every historical rule | exit 0, all nine vacancies named | + +The fixture battery that found all of it — each must fail, and the failure text is the +artifact worth keeping: + +| Fixture | Required result | +|---|---| +| one corpus path absent from disk | fail, naming the path | +| one corpus path present but declaring nothing | fail, naming the path | +| an identifier duplicated across two paths | fail on the duplicate | +| a rule deleted while a citation to it survives | fail on the dangling citation | +| a malformed fence in the second path | fail, naming that path and a path-local block index | +| a rule deleted with nothing citing it | pass, and name the vacated identifier | +| a path renamed alongside a rule retitle | pass, and name the uncompared path | + +## Related + +- The gate discussed here is the constitution corpus validator invoked by the repository's + `test` script; its module docstring carries the same argument at the point of use. +- `CONST-E6` (Prefer the Gate) made a gate the final word; it was retired 2026-09-07 in the + audience split — its substance lives in ENFORCEMENT.md, and the maker-side boundary is + CONST-E9. The rule that priced a gate's false-positive budget is gone with its vacated + number. This learning is the counterweight either way — a gate that cannot fail is not + enforcement, it is a certificate. +- The transition-state shape and the fixture-anchoring rule were added by the + single-document restore (branch `restore-single-document`); the gate-fidelity residuals + from that surgery are tracked in this repo's issues #19 and #20. diff --git a/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md b/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md new file mode 100644 index 0000000..8b15c46 --- /dev/null +++ b/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md @@ -0,0 +1,40 @@ +--- +title: TTSR regex rules require explicit flag translation and generic backreferences +date: 2026-08-30 +category: architecture-patterns +module: omp-plugin-constitution +problem_type: architecture_pattern +component: tooling +severity: medium +applies_when: + - Authoring Time-Traveling Stream Rules (TTSR) markdown files with condition regexes + - Writing validators for stream rules with PCRE inline flags like `(?i)` or `(?ims)` + - Matching self-referential or tautological anti-patterns across tool streams +root_cause: tool_interface +tags: [ttsr, regex, omp, stream-rules, flag-translation, backreference] +--- + +# TTSR regex rules require explicit flag translation and generic backreferences + +## Context + +When creating an OMP plugin providing Time-Traveling Stream Rules (TTSR), rules are authored as Markdown documents with YAML frontmatter containing `condition` regex patterns and `scope` tool selectors. + +Historical note (2026-09-01): the constitution plugin that surfaced these traps +has been removed entirely; the constitution corpus is delivered by residency. +Correction (2026-09-07, ENFORCEMENT.md): the removal's rationale — "an interrupt +can only inject text the agent already has" — conflated text injection with +refusal. An interrupt that re-injects resident text is redundant; one that +refuses the violating write re-fires at the decision point, after compaction, +which residency cannot do. The two traps below remain valid for any TTSR +plugin; the constitution corpus they were measured against is gone. + +During implementation and review of the constitution plugin, two critical regex compilation and matching traps emerged: +1. **PCRE Inline Flags:** Authors frequently write `(?i)` or `(?ims)` in condition strings. Standard JavaScript / V8 `RegExp` engines reject leading `(?i)` group syntax as invalid groups (`SyntaxError: Invalid group`). The OMP capability layer translates leading PCRE flag groups (`/^\(\?([ims]+)\)/`) into native `RegExp` flags, but a custom validator that passes raw strings to `new RegExp(pattern)` fails valid rules, while failing to reject unsupported inline combinations (`(?imsu)` or mid-pattern flags). +2. **Literal vs Generic Anti-Pattern Matching:** Condition patterns targeting self-referential statements (e.g., asserting `expect(fn()).toBe(fn())`) fail completely when hardcoded to literal identifiers. To detect tautologies generically across all codebases without false negatives, condition regexes must use capturing groups and backreferences (`expect\(\s*(\w+)\s*\([^)]*\)\s*\)\.to(?:Be|Equal)\(\s*\1\s*\([^)]*\)\s*\)`). + +## Guidance + +- When validating TTSR condition regexes, implement the exact OMP inline flag translation (`/^\(\?([ims]+)\)/`) and reject unsupported flags explicitly before compiling. +- For anti-pattern rules that detect identical calls or operands on both sides of an operator, use regex backreferences (`\1`) rather than example identifiers. +- Validate the expected rule set strictly by exact filename or ID rather than loosely testing directory file counts. diff --git a/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md b/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md new file mode 100644 index 0000000..4ebe0db --- /dev/null +++ b/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md @@ -0,0 +1,34 @@ +--- +title: Plans freeze observations about a moving base +date: 2026-08-30 +category: workflow-issues +module: constitution amendment workflow +problem_type: workflow_issue +component: development_workflow +severity: medium +applies_when: + - A plan names a base revision and its verification rows carry expectations measured at research time + - Commits land on the branch between plan freeze and the first implementation unit + - A fixture expectation in a plan contradicts what the gate prints at execution time +root_cause: stale planning baseline +tags: [plan-freeze, base-drift, fixtures, measured-expectations, review-findings] +--- + +# Plans freeze observations about a moving base + +## Context + +During the Article III verification amendment, the plan was researched and frozen against base `dd5722a`. Six commits landed on the branch before the implementation units started. By execution time, plan rows that named the pre-split tree ("exactly six vacated ids at this revision", "the uncompared-file clause fires at revision X") described a tree that no longer existed: the corpus split and corpus-commit had moved the vacated-id set to nine and moved the uncompared-file boundary to an earlier revision. The multi-agent review wave flagged the unsatisfiable rows; execution had already recorded the honest outputs by running the gate and keeping what it printed. + +The defect is not that the plan was wrong when written — it was measured against the base it named. The defect is that nothing re-derived the expectations when the base moved, and the plan's freeze rule (byte-identical after start) made correcting the rows in place the wrong move. + +## Guidance + +- At the first implementation unit, re-run every plan verification command against the current head. Record divergent outputs in the run record as measured facts; do not edit frozen plan rows to match. +- Derive fixture expectations at execution time from the gate's actual output. A number carried from research time is a hypothesis about the terminal; the run's own execution is the terminal. +- When a plan row and a measured output disagree, trust the measurement, note the divergence in the run record, and let the review (not the plan edit) carry the correction. Two unsatisfiable rows in this session were caught exactly this way — by a reviewer executing the named revision and comparing against the plan's text. +- Reviewers verify plan claims by execution against named revisions, not by re-reading the plan. That stance is what surfaced the drift; keep it in every review brief. + +## Applicability + +This applies to any planned change whose plan encodes observations about tree state (counts, absence boundaries, report shapes) rather than only intent. A plan that names only invariants and lets execution derive numbers has no rows to go stale. Where the plan must carry concrete expectations (fixtures for a gate), scope each expectation to the revision it was measured at, so a moved base invalidates the row loudly instead of silently. diff --git a/scripts/validate-constitution.ts b/scripts/validate-constitution.ts new file mode 100755 index 0000000..c099e37 --- /dev/null +++ b/scripts/validate-constitution.ts @@ -0,0 +1,282 @@ +#!/usr/bin/env -S deno run --allow-read=CONSTITUTION.md --allow-run=git +/** + * Validate the constitution corpus against constitution-rule/v1. + * + * The enforcement doctrine applied reflexively (ENFORCEMENT.md): the + * constitution's own format must fail a command, not a cited clause. Validates + * every fenced ```yaml block against hardcoded schema fields (required_fields, + * optional_fields, gate_values). + * + * Coverage is checked before schema. A rule the parser never reaches cannot be + * validated, and an unterminated fence silently removes every rule after it from + * the block — so counting ids in the raw text and comparing against ids parsed + * out of blocks is the only way this gate can report on what it did NOT see. + * Without that comparison a green run means "no rule I happened to parse was + * malformed", which is not the claim the gate is making. + * + * The corpus is one file — the resident law, whole and always in context. Ids are + * unique across it and citations resolve within it. A corpus path that is merely + * absent is not the only shape of a vacuous pass: a file present and parsing but + * declaring no rule scores identically to a healthy one, so the corpus path must + * contribute at least one rule of its own. + * + * There is no backwards compatibility and no retirement ledger. A deleted rule + * leaves its number vacant and a citation to it resolves to nothing, which is a + * loud failure and needs no gate. Vacancy is named, not enforced: --against lists + * ids vacated since the revision and corpus files absent at it on the success + * line, because a green line that silently measured less is the vacuous pass this + * gate exists to prevent. The one identifier defect that is NOT loud is an id that + * survives while its rule changes underneath it: every citation keeps resolving, + * to the wrong rule. No single revision can see that, so `--against ` + * recomputes it from git. + * + * Exit 0 clean, 1 with a named defect list, 3 unmeasurable — no identifiers matched + * at all, reported distinctly because an id pattern that matches nothing scores a + * healthy corpus and an id-free one identically. + */ +import { parse } from "@std/yaml"; + +const PATHS = ["CONSTITUTION.md"] as const; + +const ID_RE = /^CONST-[A-Z]\d+$/; +const ID_IN_TEXT_RE = /^\s*- id:\s*(\S+)\s*$/gm; +const TITLE_IN_TEXT_RE = /^\s*- id:\s*(\S+)\s*\n\s*title:\s*(.+?)\s*$/gm; +const CITE_RE = /\bCONST-[A-Z]\d+\b/g; + +// A family letter names what a rule is ABOUT, never where it sits. Adding a +// letter here is half the change; the other half is the registry in AGENTS.md. +const FAMILIES: Record = { + "G": "Governance", + "E": "Enforcement", + "P": "Purity", + "D": "Domain modelling", + "B": "Boundary", + "T": "Testing", + "N": "Naming & structure", + "W": "Work discipline", + "S": "Subtraction", +}; + +const REQUIRED_FIELDS = ["id", "title", "gate", "do", "dont", "harm", "check"]; +const OPTIONAL_FIELDS = ["scope", "example", "layers"]; +const GATE_VALUES: Record = { + "lint": true, + "type-checker": true, + "mutation": true, + "review": true, +}; +const KNOWN_FAMILIES = Object.keys(FAMILIES).sort().join(", "); +const KNOWN_GATES = Object.keys(GATE_VALUES).sort().join(", "); + +type Rule = Record; + +function fail(errors: string[]): never { + for (const e of errors) console.log(`FAIL ${e}`); + Deno.exit(1); +} + +function titlesFrom(text: string): Map { + const titles = new Map(); + for (const m of text.matchAll(TITLE_IN_TEXT_RE)) titles.set(m[1], m[2]); + return titles; +} + +const againstIndex = Deno.args.indexOf("--against"); +const againstEquals = Deno.args.find((a) => a.startsWith("--against=")); +const against = againstIndex >= 0 + ? Deno.args[againstIndex + 1] + : againstEquals?.slice("--against=".length); +const againstRequested = against !== undefined || againstIndex >= 0 || + againstEquals !== undefined; +if (againstRequested && (against === undefined || against.length === 0)) { + fail(["--against requires a revision (form: --against or --against=)"]); +} +const strayAgainst = Deno.args.find((a) => + a.startsWith("--against") && a !== "--against" && !a.startsWith("--against=") +); +if (strayAgainst !== undefined) { + fail([`unknown flag '${strayAgainst}' — did you mean --against ?`]); +} + +const errors: string[] = []; + +const texts: Record = {}; +for (const p of PATHS) { + try { + texts[p] = await Deno.readTextFile(p); + } catch { + fail([`${p}: missing`]); + } +} + +const blocks: Array<{ path: string; index: number; body: string }> = []; +for (const [p, t] of Object.entries(texts)) { + const found = [...t.matchAll(/```yaml\n([\s\S]*?)```/g)]; + if (found.length === 0) { + errors.push(`${p}: no fenced yaml rule blocks found`); + } + found.forEach((m, j) => blocks.push({ path: p, index: j, body: m[1] })); +} +if (blocks.length === 0) { + fail([...errors, "no fenced yaml rule blocks found in any corpus file"]); +} + +const rules: Rule[] = []; +for (const b of blocks) { + try { + const doc = parse(b.body) as { rules?: Rule[] } | null; + rules.push(...(doc?.rules ?? [])); + } catch (e) { + errors.push( + `${b.path} block ${b.index}: YAML parse error: ${(e as Error).message}`, + ); + } +} + +const parsedIds = rules.map((r) => String(r.id)); +const declaredIds = Object.values(texts).flatMap((t) => + [...t.matchAll(ID_IN_TEXT_RE)].map((m) => m[1]) +); +const parsedIdSet = new Set(parsedIds); +const uncovered = declaredIds.filter((i) => !parsedIdSet.has(i)); +if (uncovered.length > 0) { + errors.push( + `${uncovered.length} rule(s) declared in the corpus but never parsed into a yaml block: [${uncovered.join(", ")}] — check for an unterminated \`\`\`yaml fence`, + ); +} + +if (declaredIds.length === 0) { + console.log( + "UNMEASURABLE: no rule identifiers matched — the corpus is empty, or the id syntax moved", + ); + Deno.exit(3); +} + +const seen = new Set(); +for (const r of rules) { + const rid = String(r.id ?? ""); + for (const f of REQUIRED_FIELDS) { + if (!(f in r)) errors.push(`${rid}: missing required field '${f}'`); + } + const unknown = Object.keys(r).filter((k) => + !REQUIRED_FIELDS.includes(k) && !OPTIONAL_FIELDS.includes(k) + ); + if (unknown.length > 0) { + errors.push(`${rid}: unknown fields [${unknown.sort().join(", ")}]`); + } + if (!ID_RE.test(rid)) { + errors.push(`${rid}: id does not match ${ID_RE.source}`); + } else if (!Object.hasOwn(FAMILIES, rid["CONST-".length])) { + errors.push( + `${rid}: family '${rid["CONST-".length]}' is not registered — known families are [${KNOWN_FAMILIES}]`, + ); + } + if (seen.has(rid)) errors.push(`${rid}: duplicate id`); + seen.add(rid); + if (typeof r.gate !== "string" || !Object.hasOwn(GATE_VALUES, r.gate)) { + errors.push(`${rid}: gate '${String(r.gate)}' not in [${KNOWN_GATES}]`); + } + for (const f of ["do", "dont"] as const) { + const v = r[f]; + const shaped = typeof v === "string" || + (Array.isArray(v) && v.every((x) => typeof x === "string")); + if (!shaped) errors.push(`${rid}: '${f}' must be a string or list of strings`); + } + const ex = r.example; + if (ex !== undefined && ex !== null) { + const shaped = typeof ex === "object" && !Array.isArray(ex) && + Object.values(ex).every((v) => typeof v === "string"); + if (!shaped) errors.push(`${rid}: 'example' must be a map of strings`); + } +} + +const cites: Record> = {}; +for (const [p, t] of Object.entries(texts)) { + cites[p] = new Set([...t.matchAll(CITE_RE)].map((m) => m[0])); +} +const allCited = new Set(); +for (const s of Object.values(cites)) { + for (const c of s) allCited.add(c); +} +for (const cited of [...allCited].sort()) { + if (seen.has(cited)) continue; + for (const p of PATHS) { + if (cites[p]?.has(cited)) { + errors.push(`dangling citation: '${cited}' is cited in ${p} but names no rule`); + } + } +} + +async function checkAgainst( + rev: string, + errors: string[], + liveTitles: Map, +): Promise<{ vacated: string[]; uncompared: string[] }> { + const oldTitles = new Map(); + const uncompared: string[] = []; + try { + const results = await Promise.all(PATHS.map(async (p) => { + const cmd = new Deno.Command("git", { + args: ["show", `${rev}:${p}`], + stdout: "piped", + stderr: "piped", + }); + const out = await cmd.output(); + if (!out.success) return { p, absent: true, titles: [] }; + return { p, absent: false, titles: [...titlesFrom(new TextDecoder().decode(out.stdout))] }; + })); + for (const r of results) { + if (r.absent) { + uncompared.push(r.p); + continue; + } + for (const [rid, title] of r.titles) oldTitles.set(rid, title); + } + } catch (e) { + errors.push(`--against ${rev}: git is not runnable (${(e as Error).message})`); + return { vacated: [], uncompared: [] }; + } + + if (oldTitles.size === 0) { + errors.push( + `--against ${rev}: no rules found in any corpus file at that revision — wrong rev, or every file was renamed`, + ); + return { vacated: [], uncompared: [] }; + } + + for (const [rid, oldTitle] of oldTitles) { + const live = liveTitles.get(rid); + if (live !== undefined && live !== oldTitle) { + errors.push( + `reassigned id: '${rid}' named "${oldTitle}" at ${rev} and names "${live}" now — every citation to it resolves to a different rule`, + ); + } + } + + const vacated = [...oldTitles.keys()].filter((rid) => !liveTitles.has(rid)) + .sort(); + return { vacated, uncompared: uncompared.sort() }; +} + +let vacated: string[] = []; +let uncompared: string[] = []; +if (against !== undefined) { + const liveTitles = new Map(); + for (const t of Object.values(texts)) { + for (const [rid, title] of titlesFrom(t)) liveTitles.set(rid, title); + } + ({ vacated, uncompared } = await checkAgainst(against, errors, liveTitles)); +} + +if (errors.length > 0) fail(errors); + +const suffix = against !== undefined ? `; no id reassigned since ${against}` : ""; +console.log( + `valid: ${rules.length} rules across ${blocks.length} yaml blocks in ${Object.keys(texts).length} files, ${Object.keys(FAMILIES).length} families${suffix}`, +); +if (against !== undefined && uncompared.length > 0) { + console.log(` not compared, absent at ${against}: ${uncompared.join(", ")}`); +} +if (against !== undefined && vacated.length > 0) { + console.log(` ${vacated.length} id(s) vacated since ${against}: ${vacated.join(", ")}`); +} From dc38456e218624f21e75bc03adde7e9dfbf4dfd2 Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 11 Sep 2026 13:42:04 +0000 Subject: [PATCH 2/4] Squashed 'repos/worktrunk-scripts/' content from commit e745dc91215 git-subtree-dir: repos/worktrunk-scripts git-subtree-split: e745dc91215019793f95844f1088b55d666b1392 --- .codegraph/.gitignore | 5 + .github/workflows/ci.yml | 31 + .husky/commit-msg | 6 + .husky/pre-commit | 22 + .husky/pre-push | 22 + .husky/prepare-commit-msg | 24 + AGENTS.md | 64 ++ CLAUDE.md | 3 + CONSTITUTION-ARTICLES.md | 1 + CONSTITUTION.md | 1 + CONTRIBUTING.md | 16 + LICENSE | 203 +++++ README.md | 57 ++ commitlint.config.ts | 194 +++++ deno.json | 26 + deno.lock | 699 ++++++++++++++++++ .../2026-08-30-deno-worktrunk-conversion.md | 108 +++ .../tooling/bash-to-deno-port-semantics.md | 54 ++ dprint.json | 31 + .../.claude-plugin/marketplace.json | 30 + repos/constitution/.gitignore | 0 repos/constitution/.husky/commit-msg | 1 + repos/constitution/.husky/pre-commit | 1 + .../constitution/.omp-plugin/marketplace.json | 30 + repos/constitution/AGENTS.md | 121 +++ repos/constitution/CLAUDE.md | 1 + repos/constitution/CONSTITUTION-ARTICLES.md | 294 ++++++++ repos/constitution/CONSTITUTION.md | 85 +++ repos/constitution/LICENSE | 201 +++++ repos/constitution/README.md | 124 ++++ repos/constitution/commitlint.config.cjs | 3 + repos/constitution/deno.json | 10 + repos/constitution/deno.lock | 16 + ...-refactor-article-iii-verification-plan.md | 432 +++++++++++ ...-feat-omp-constitution-ttsr-plugin-plan.md | 49 ++ .../the-vacuous-pass-gate-input-sets.md | 152 ++++ ...x-rules-flag-translation-backreferences.md | 31 + ...freeze-observations-about-a-moving-base.md | 34 + .../constitution/.claude-plugin/plugin.json | 21 + .../constitution/.omp-plugin/plugin.json | 21 + .../plugins/constitution/README.md | 29 + .../rules/constitution-boundary.md | 33 + .../rules/constitution-conduct-review.md | 37 + .../rules/constitution-pure-core.md | 33 + .../rules/constitution-verification.md | 42 ++ .../scripts/validate-constitution.ts | 288 ++++++++ scripts/codegraph-worktree-mcp.ts | 89 +++ scripts/convert-to-relative-paths.ts | 44 ++ scripts/copy-codegraph.ts | 231 ++++++ scripts/generate-artifacts.ts | 39 + scripts/install-deps.ts | 78 ++ scripts/lib/fs.ts | 26 + scripts/lib/git.ts | 41 + scripts/lib/mod.ts | 3 + scripts/lib/paths.ts | 14 + scripts/post-switch.ts | 19 + scripts/pre-merge.ts | 34 + scripts/pre-start.ts | 136 ++++ scripts/worktree-to-relative.ts | 51 ++ subtrees.toml | 5 + 60 files changed, 4496 insertions(+) create mode 100644 .codegraph/.gitignore create mode 100644 .github/workflows/ci.yml create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100755 .husky/prepare-commit-msg create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 120000 CONSTITUTION-ARTICLES.md create mode 120000 CONSTITUTION.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 commitlint.config.ts create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 docs/plans/2026-08-30-deno-worktrunk-conversion.md create mode 100644 docs/solutions/tooling/bash-to-deno-port-semantics.md create mode 100644 dprint.json create mode 100644 repos/constitution/.claude-plugin/marketplace.json create mode 100644 repos/constitution/.gitignore create mode 100755 repos/constitution/.husky/commit-msg create mode 100755 repos/constitution/.husky/pre-commit create mode 100644 repos/constitution/.omp-plugin/marketplace.json create mode 100644 repos/constitution/AGENTS.md create mode 100644 repos/constitution/CLAUDE.md create mode 100644 repos/constitution/CONSTITUTION-ARTICLES.md create mode 100644 repos/constitution/CONSTITUTION.md create mode 100644 repos/constitution/LICENSE create mode 100644 repos/constitution/README.md create mode 100644 repos/constitution/commitlint.config.cjs create mode 100644 repos/constitution/deno.json create mode 100644 repos/constitution/deno.lock create mode 100644 repos/constitution/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md create mode 100644 repos/constitution/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md create mode 100644 repos/constitution/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md create mode 100644 repos/constitution/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md create mode 100644 repos/constitution/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md create mode 100644 repos/constitution/plugins/constitution/.claude-plugin/plugin.json create mode 100644 repos/constitution/plugins/constitution/.omp-plugin/plugin.json create mode 100644 repos/constitution/plugins/constitution/README.md create mode 100644 repos/constitution/plugins/constitution/rules/constitution-boundary.md create mode 100644 repos/constitution/plugins/constitution/rules/constitution-conduct-review.md create mode 100644 repos/constitution/plugins/constitution/rules/constitution-pure-core.md create mode 100644 repos/constitution/plugins/constitution/rules/constitution-verification.md create mode 100755 repos/constitution/scripts/validate-constitution.ts create mode 100755 scripts/codegraph-worktree-mcp.ts create mode 100755 scripts/convert-to-relative-paths.ts create mode 100755 scripts/copy-codegraph.ts create mode 100755 scripts/generate-artifacts.ts create mode 100755 scripts/install-deps.ts create mode 100644 scripts/lib/fs.ts create mode 100644 scripts/lib/git.ts create mode 100644 scripts/lib/mod.ts create mode 100644 scripts/lib/paths.ts create mode 100755 scripts/post-switch.ts create mode 100755 scripts/pre-merge.ts create mode 100755 scripts/pre-start.ts create mode 100755 scripts/worktree-to-relative.ts create mode 100644 subtrees.toml diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..d20c0fe --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..238f1bd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + - name: Install dprint + run: | + curl -fsSL https://dprint.dev/install.sh | sh + echo "$HOME/.dprint/bin" >> "$GITHUB_PATH" + - name: dprint check + run: dprint check + - name: deno lint + run: deno lint + - name: deno check + run: deno check scripts/*.ts scripts/lib/*.ts diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..e07916d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e +# Skip for pure vendored commits (repos/) and the subtrees.toml registration. +if git diff --cached --name-only | grep -qvE '^(repos/.*|subtrees\.toml)$'; then + deno run --allow-read --allow-run --allow-env --allow-sys npm:@commitlint/cli@19.8.1 --edit "$1" +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..6a440fd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +# Skip during a merge: the staged set is the merge result, not authored work. +if [ -e "$(git rev-parse --git-path MERGE_HEAD)" ]; then + exit 0 +fi + +# Skip for pure repos/ subtree commits and the subtrees.toml registration commit. +if git diff --cached --name-only | grep -qvE '^(repos/.*|subtrees\.toml)$'; then + # Format staged files that are ours (skip vendored repos/) + staged=$(git diff --cached --name-only --diff-filter=ACMR | grep -vE '^repos/' | tr '\n' ' ') + if [ -n "$staged" ]; then + # dprint on staged non-vendored files + echo "$staged" | xargs dprint fmt --allow-no-files -- 2>/dev/null || true + # deno lint on staged TS/JS files (commitlint config excluded via deno.json) + lint_staged=$(echo "$staged" | tr ' ' '\n' | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$' | grep -vE 'commitlint\.config\.ts' | tr '\n' ' ') + if [ -n "$lint_staged" ]; then + echo "$lint_staged" | xargs deno lint 2>/dev/null || true + fi + # re-add formatted files + echo "$staged" | xargs git add -- 2>/dev/null || true + fi +fi diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..48a4559 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,22 @@ +#!/usr/bin/env sh +# Refuse a push whose refs do not contain remote main. +# Local main is not the source of truth — fetch the remote tip. +set -e +remote=origin +# No remote main yet (new repo) — allow +if ! git ls-remote --exit-code "$remote" refs/heads/main >/dev/null 2>&1; then + exit 0 +fi +main_sha=$(git ls-remote "$remote" refs/heads/main | cut -f1) +if [ -z "$main_sha" ]; then + exit 0 +fi +# Ensure we have the object locally; fetch if missing +if ! git cat-file -e "$main_sha^{commit}" 2>/dev/null; then + git fetch --quiet "$remote" refs/heads/main || exit 1 +fi +if ! git merge-base --is-ancestor "$main_sha" HEAD; then + echo "pre-push: behind remote main ($main_sha)." >&2 + echo "Fetch and rebase onto $remote/main before pushing." >&2 + exit 1 +fi diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg new file mode 100755 index 0000000..9118872 --- /dev/null +++ b/.husky/prepare-commit-msg @@ -0,0 +1,24 @@ +#!/bin/sh +# prepare-commit-msg — Strip AI co-author trailers +# Preserves human pair attribution while removing AI credit lines. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 + +# Only process standard commits +case "$COMMIT_SOURCE" in + merge|squash) + exit 0 + ;; +esac + +if [ -f "$COMMIT_MSG_FILE" ]; then + temp_file=$(mktemp) + grep -v -E -i "^Co-?-?[Aa]uthored-by:.*<.*(noreply@anthropic\.com|cursoragent@cursor\.com|noreply@aider\.dev|cascade@windsurf\.com|clio-agent@sisyphuslabs\.ai)>" "$COMMIT_MSG_FILE" > "$temp_file" + if [ -s "$temp_file" ]; then + mv "$temp_file" "$COMMIT_MSG_FILE" + else + rm "$temp_file" + exit 1 + fi +fi diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..54847f5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +# AGENTS.md — Worktrunk Scripts + +Deno worktree hooks for `worktrunk` automating environment setup, warm-copy indexing, and path isolation. + +## Standing Law + +Read `CONSTITUTION.md` before architecture or rule authoring. + +Startup: confirm working directory and active task; run `deno task check` and repair failures before adding scope. + +## Routing + +Load docs on-demand when triggers fire; do not perform eager multi-spec reads at startup. + +| Doc | Trigger | +| ------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `CONSTITUTION.md` | architecture changes, rule authoring, or design disputes | +| `CONSTITUTION-ARTICLES.md` | editing source files (pure core, types, boundaries, testing) | +| `docs/solutions/tooling/bash-to-deno-port-semantics.md` | modifying path mapping, git command wrappers, or cross-platform Deno CLI semantics | +| `subtrees.toml` | updating or adding vendored git subtrees | + +## Stack & Conventions + +- Deno 2.x (`deno task check`, `deno task test`). +- Hook scripts under `scripts/` are standalone CLIs (no exports). +- Shared utilities live under `scripts/lib/`. +- Lint via `deno lint`; formatting via `dprint`. Gate: `deno task check`. + +## Surface Classes + +| Surface | Examples | Rule | +| ------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------- | +| **Evaluator** | `.github/workflows/` | Its own commit, never shared with the work it judges; gate observed red before and green after. | +| **Doctrine** | `CONSTITUTION.md`, `AGENTS.md`, `docs/` | Editable, but never an input to a gate. | +| **Editable** | `scripts/`, `deno.json`, `dprint.json` | Edit freely within active task. | + +## Directory Map + +Root doctrine files `CONSTITUTION.md`, `CONSTITUTION-ARTICLES.md`. + +| Directory | What it is | Governance | +| -------------- | --------------------------------------------------------- | ----------------- | +| `scripts/` | Standalone lifecycle CLI entrypoints invoked by worktrunk | Root | +| `scripts/lib/` | Shared git, path, and filesystem utilities | Root | +| `repos/` | Vendored third-party trees (`subtrees.toml`), read-only | `WT-S3` read-only | +| `docs/` | Solutions, plans, and technical documentation | Editable | + +## Rules — Must Hold At Done + +Every row is load-bearing and names its runnable gate. + +| ID | Rule | Gate | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| **WT-S3** | `repos/` is a vendored third-party reference subtree; never edit it. | `review — git status check` | +| **WT-R1** | Hook scripts in `scripts/` must declare explicit Deno permission shebangs (`#!/usr/bin/env -S deno run --allow-...`) and avoid `-A`/`--allow-all`. | `deno task check` | +| **WT-R2** | Shared utilities belong in `scripts/lib/`; hook entrypoints under `scripts/` are standalone CLIs with no exports. | `deno task check` | +| **WT-R3** | Code formatting and linting pass with zero diagnostics. | `deno task check` | +| **WT-C1** | `type(scope): subject` with no trailing period. Keep subjects concise. | `review — commit message inspection` | + +## Verification Commands + +```bash +deno task check +``` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..58bc96f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +@AGENTS.md +@subtrees.toml +@CONSTITUTION.md diff --git a/CONSTITUTION-ARTICLES.md b/CONSTITUTION-ARTICLES.md new file mode 120000 index 0000000..5f15994 --- /dev/null +++ b/CONSTITUTION-ARTICLES.md @@ -0,0 +1 @@ +repos/constitution/CONSTITUTION-ARTICLES.md \ No newline at end of file diff --git a/CONSTITUTION.md b/CONSTITUTION.md new file mode 120000 index 0000000..67e49c5 --- /dev/null +++ b/CONSTITUTION.md @@ -0,0 +1 @@ +repos/constitution/CONSTITUTION.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6cceb83 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing + +## Workflow + +Format and check before committing: + +```bash +deno task fmt +deno task check +``` + +## Conventions + +- Hook scripts under `scripts/` are standalone CLIs (no exports). +- Shared code lives in `scripts/lib/`. +- Commits follow [Conventional Commits](https://www.conventionalcommits.org/). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..896c374 --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ +Copyright (c) 2026 Ryan Lee (systemfsoftware) + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Ryan Lee (systemfsoftware) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..90177a3 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# worktrunk-config + +> Deno worktree hooks for `worktrunk` automating environment setup, warm-copy indexing, and path isolation. + +When creating isolated git worktrees, new checkouts often start cold: index databases must be rebuilt from scratch, nested `.git` references break across directory boundaries, and development dependencies require manual setup. This repository provides executable TypeScript hooks that wire into `worktrunk` lifecycle events (`pre-start`, `post-start`, `post-switch`, `pre-merge`) to make every worktree immediately ready for development. + +```toml +# .config/wt.toml +[hooks] +pre-start = "deno run --allow-read --allow-write --allow-run --allow-env ./scripts/pre-start.ts {{worktree_path}} {{primary_worktree_path}}" +post-start = "deno run --allow-read --allow-write --allow-run --allow-env ./scripts/copy-codegraph.ts {{worktree_path}} {{primary_worktree_path}}" +post-switch = "deno run --allow-run ./scripts/post-switch.ts {{worktree_path}}" +pre-merge = "deno run --allow-read --allow-write --allow-run ./scripts/pre-merge.ts {{worktree_path}}" +``` + +## Quick Start + +Ensure [Deno](https://deno.land/) is installed on your workstation, then verify the codebase: + +```bash +git clone https://github.com/systemfsoftware/worktrunk-scripts.git +cd worktrunk-scripts +deno task check +``` + +The hooks execute directly with `deno run` using explicit permission flags declared in each script's shebang. + +## Design + +Scripts under `scripts/` are standalone CLI entrypoints invoked by `worktrunk` lifecycle events. Shared helpers (git resolution, relative path mapping, filesystem utilities) live under `scripts/lib/`. + +## Configuration + +In your primary repository, configure `worktrunk` to call the desired hook scripts in `.config/wt.toml`. Every script accepts standard arguments supplied by the runner: + +```bash +deno run --allow-read --allow-write --allow-run --allow-env ./scripts/.ts [primary_path] +``` + +If the secondary `primary_path` argument is omitted, the hook automatically resolves the primary repository root using the shared git common directory. + +## Quality Gates + +Code formatting and linting are enforced via `dprint` and `deno lint`: + +```bash +deno task fmt # Format all files using dprint +deno task check # Run dprint verification and deno lint +``` + +## Contributing + +Development setup, conventions, and workflow: [CONTRIBUTING.md](CONTRIBUTING.md). + +## License + +See [LICENSE](LICENSE) for terms. diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 0000000..e3f3e56 --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,194 @@ +import type { UserConfig } from '@commitlint/types' +import { execFileSync } from 'node:child_process' + +const EXTRA_SCOPES = ['repo', 'deps', 'release', 'ci', 'scripts', 'hooks'] as const + +const matchesAny = (...patterns: readonly RegExp[]) => (path: string) => patterns.some((p) => p.test(path)) + +const isDoc = matchesAny( + /\.mdx?$/, + /^docs\//, + /(^|\/)README\.md$/i, + /(^|\/)AGENTS\.md$/i, + /(^|\/)CLAUDE\.md$/i, + /(^|\/)CHANGELOG\.md$/i, +) + +const isTest = matchesAny( + /\.(test|spec|tst)\.(ts|tsx|js|jsx|mjs|cjs)$/, + /(^|\/)__tests__\//, + /(^|\/)__mocks__\//, + /(^|\/)tests\//, + /(^|\/)test-helpers\//, + /(^|\/)e2e\//, + /(^|\/)fixtures\//, +) + +const isCI = matchesAny( + /^\.github\/workflows\//, + /^\.github\/actions\//, + /^\.github\/dependabot\.ya?ml$/, +) + +const isLockfile = matchesAny( + /(^|\/)pnpm-lock\.yaml$/, + /(^|\/)package-lock\.json$/, + /(^|\/)bun\.lockb?$/, + /(^|\/)yarn\.lock$/, + /(^|\/)deno\.lock$/, +) + +const isTooling = matchesAny( + /^\.husky\//, + /(^|\/)commitlint\.config\.[mc]?[jt]s$/, + /(^|\/)\.releaserc(\..+)?$/, + /(^|\/)\.lintstagedrc(\..+)?$/, + /(^|\/)tsconfig.*\.json$/, + /(^|\/)deno\.jsonc?$/, + /(^|\/)dprint\.json$/, + /(^|\/)\.editorconfig$/, + /(^|\/)\.gitignore$/, + /(^|\/)\.prettierrc(\..+)?$/, + /(^|\/)package\.json$/, + /(^|\/)pnpm-workspace\.yaml$/, +) + +const ALLOWED_BY_SHAPE: readonly { + readonly name: string + readonly match: (path: string) => boolean + readonly allowed: Readonly> +}[] = [ + { name: 'docs', match: isDoc, allowed: { docs: true, chore: true, ai: true } }, + { name: 'test', match: isTest, allowed: { test: true, chore: true } }, + { name: 'CI', match: isCI, allowed: { ci: true, chore: true } }, + { name: 'lockfile', match: isLockfile, allowed: { deps: true, chore: true } }, + { + name: 'tooling', + match: isTooling, + allowed: { chore: true, build: true, ci: true, deps: true, ai: true, security: true }, + }, +] + +const stagedFiles = (): readonly string[] => { + try { + return execFileSync('git', ['diff', '--cached', '--name-only'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }) + .split('\n') + .map((l: string) => l.trim()) + .filter(Boolean) + } catch { + return [] + } +} + +const configuration: UserConfig = { + extends: ['@commitlint/config-conventional'], + plugins: [ + { + rules: { + 'no-ai-coauthors': ({ raw }: { raw: string }) => { + if (!raw) { + return [true, 'OK'] + } + const aiEmailPatterns = [ + /noreply@anthropic\.com/i, + /cursoragent@cursor\.com/i, + /noreply@aider\.dev/i, + /cascade@windsurf\.com/i, + /noreply@codeium\.com/i, + /clio-agent@sisyphuslabs\.ai/i, + /factory-droid\[bot\]@users\.noreply\.github\.com/i, + ] as const + const coauthorLines = raw.match(/^Co-?-?[Aa]uthored-by:.*$/gmi) || [] + const aiModelPatterns = [ + /\b(Claude\s+)?(Opus|Sonnet|Haiku)\b/i, + /\bgpt-4o\b/i, + /\bClaude\b.*\b3\.\d+\b/i, + ] as const + const hasAIModelInCoauthor = coauthorLines.some((line: string) => + aiModelPatterns.some((pattern) => pattern.test(line)) + ) + const hasAIEmail = aiEmailPatterns.some((pattern) => pattern.test(raw)) + const hasAICoauthor = hasAIEmail || hasAIModelInCoauthor + return [ + !hasAICoauthor, + hasAICoauthor + ? 'AI co-authors and AI model references are not allowed in commit messages' + : 'OK', + ] + }, + 'type-matches-diff-shape': ({ type }: { type?: string }) => { + const files = stagedFiles() + if (files.length === 0 || !type) return [true, 'OK'] + const allMatch = (m: (p: string) => boolean) => files.every(m) + for (const shape of ALLOWED_BY_SHAPE) { + if (allMatch(shape.match) && !shape.allowed[type]) { + const allowed = Object.keys(shape.allowed).sort().join(' / ') + return [false, `'${type}' with 100% ${shape.name} paths — REQUIRED type: ${allowed}`] + } + } + if (type === 'feat' || type === 'fix') { + const hasProductionSource = files.some( + (p) => !isDoc(p) && !isTest(p) && !isCI(p) && !isLockfile(p) && !isTooling(p), + ) + if (!hasProductionSource) { + return [ + false, + `'${type}' MUST touch >=1 production source file (none of: docs, test, CI, lockfile, tooling)`, + ] + } + } + return [true, 'OK'] + }, + }, + }, + ], + rules: { + 'no-ai-coauthors': [2, 'always'], + 'type-matches-diff-shape': [2, 'always'], + 'type-enum': [ + 2, + 'always', + [ + 'ai', + 'api', + 'build', + 'chore', + 'ci', + 'deps', + 'docs', + 'feat', + 'fix', + 'improvement', + 'perf', + 'refactor', + 'revert', + 'security', + 'style', + 'test', + ], + ], + 'scope-enum': [2, 'always', [...EXTRA_SCOPES]], + 'scope-case': [2, 'always', 'kebab-case'], + 'type-case': [2, 'always', 'lower-case'], + 'type-empty': [2, 'never'], + 'subject-case': [0], + 'subject-empty': [2, 'never'], + 'subject-full-stop': [2, 'never', '.'], + 'header-max-length': [0], + 'body-max-line-length': [0], + 'footer-max-line-length': [0], + 'body-leading-blank': [0], + 'footer-leading-blank': [0], + 'header-full-stop': [2, 'never', '.'], + 'body-full-stop': [2, 'never', '.'], + 'references-empty': [1, 'never'], + }, + defaultIgnores: true, + ignores: [(commit: string) => commit.startsWith("Squashed '") || commit.includes('git-subtree-dir:')], + formatter: '@commitlint/format', +} + +export default configuration diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..5eb3285 --- /dev/null +++ b/deno.json @@ -0,0 +1,26 @@ +{ + "description": "Worktrunk worktree hooks", + "tasks": { + "check": "dprint check && deno lint && deno check scripts/*.ts scripts/lib/*.ts", + "lint": "deno lint", + "fmt": "dprint fmt", + "fmt:check": "dprint check", + "test": "deno test --allow-read --allow-write --allow-run --allow-env", + "commitlint": "deno run --allow-read --allow-run --allow-env --allow-sys npm:@commitlint/cli@19.8.1 --edit", + "prepare": "deno run --allow-read --allow-write --allow-run --allow-sys npm:husky@9.1.7" + }, + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.14", + "@std/fs": "jsr:@std/fs@^1.0.19", + "@std/path": "jsr:@std/path@^1.0.9", + "@commitlint/cli": "npm:@commitlint/cli@19.8.1", + "@commitlint/config-conventional": "npm:@commitlint/config-conventional@19.8.1" + }, + "nodeModulesDir": "auto", + "lint": { + "rules": { + "exclude": ["no-slow-types"] + } + }, + "exclude": [".codegraph", ".git", "commitlint.config.ts"] +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..06d8328 --- /dev/null +++ b/deno.lock @@ -0,0 +1,699 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.14": "1.0.19", + "jsr:@std/fs@^1.0.19": "1.0.24", + "jsr:@std/internal@^1.0.12": "1.0.14", + "jsr:@std/internal@^1.0.14": "1.0.14", + "jsr:@std/path@^1.0.9": "1.1.6", + "jsr:@std/path@^1.1.5": "1.1.6", + "npm:@commitlint/cli@19.8.1": "19.8.1_@types+node@26.4.0_typescript@7.0.2", + "npm:@commitlint/config-conventional@19.8.1": "19.8.1" + }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal@^1.0.12" + ] + }, + "@std/fs@1.0.24": { + "integrity": "f3061b45b81673a2bece689da041df32d174be064c89eb6397fb5718d3fb7877", + "dependencies": [ + "jsr:@std/internal@^1.0.14", + "jsr:@std/path@^1.1.5" + ] + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + }, + "@std/path@1.1.6": { + "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe", + "dependencies": [ + "jsr:@std/internal@^1.0.14" + ] + } + }, + "npm": { + "@babel/code-frame@7.29.7": { + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dependencies": [ + "@babel/helper-validator-identifier", + "js-tokens", + "picocolors" + ] + }, + "@babel/helper-validator-identifier@7.29.7": { + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==" + }, + "@commitlint/cli@19.8.1_@types+node@26.4.0_typescript@7.0.2": { + "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==", + "dependencies": [ + "@commitlint/format", + "@commitlint/lint", + "@commitlint/load", + "@commitlint/read", + "@commitlint/types", + "tinyexec", + "yargs" + ], + "bin": true + }, + "@commitlint/config-conventional@19.8.1": { + "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", + "dependencies": [ + "@commitlint/types", + "conventional-changelog-conventionalcommits" + ] + }, + "@commitlint/config-validator@19.8.1": { + "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==", + "dependencies": [ + "@commitlint/types", + "ajv" + ] + }, + "@commitlint/ensure@19.8.1": { + "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==", + "dependencies": [ + "@commitlint/types", + "lodash.camelcase", + "lodash.kebabcase", + "lodash.snakecase", + "lodash.startcase", + "lodash.upperfirst" + ] + }, + "@commitlint/execute-rule@19.8.1": { + "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==" + }, + "@commitlint/format@19.8.1": { + "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==", + "dependencies": [ + "@commitlint/types", + "chalk" + ] + }, + "@commitlint/is-ignored@19.8.1": { + "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==", + "dependencies": [ + "@commitlint/types", + "semver" + ] + }, + "@commitlint/lint@19.8.1": { + "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==", + "dependencies": [ + "@commitlint/is-ignored", + "@commitlint/parse", + "@commitlint/rules", + "@commitlint/types" + ] + }, + "@commitlint/load@19.8.1_@types+node@26.4.0_typescript@7.0.2": { + "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==", + "dependencies": [ + "@commitlint/config-validator", + "@commitlint/execute-rule", + "@commitlint/resolve-extends", + "@commitlint/types", + "chalk", + "cosmiconfig", + "cosmiconfig-typescript-loader", + "lodash.isplainobject", + "lodash.merge", + "lodash.uniq" + ] + }, + "@commitlint/message@19.8.1": { + "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==" + }, + "@commitlint/parse@19.8.1": { + "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==", + "dependencies": [ + "@commitlint/types", + "conventional-changelog-angular", + "conventional-commits-parser" + ] + }, + "@commitlint/read@19.8.1": { + "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==", + "dependencies": [ + "@commitlint/top-level", + "@commitlint/types", + "git-raw-commits", + "minimist", + "tinyexec" + ] + }, + "@commitlint/resolve-extends@19.8.1": { + "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==", + "dependencies": [ + "@commitlint/config-validator", + "@commitlint/types", + "global-directory", + "import-meta-resolve", + "lodash.mergewith", + "resolve-from@5.0.0" + ] + }, + "@commitlint/rules@19.8.1": { + "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==", + "dependencies": [ + "@commitlint/ensure", + "@commitlint/message", + "@commitlint/to-lines", + "@commitlint/types" + ] + }, + "@commitlint/to-lines@19.8.1": { + "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==" + }, + "@commitlint/top-level@19.8.1": { + "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==", + "dependencies": [ + "find-up" + ] + }, + "@commitlint/types@19.8.1": { + "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", + "dependencies": [ + "@types/conventional-commits-parser", + "chalk" + ] + }, + "@types/conventional-commits-parser@5.0.2": { + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dependencies": [ + "@types/node" + ] + }, + "@types/node@26.4.0": { + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "dependencies": [ + "undici-types" + ] + }, + "@typescript/typescript-aix-ppc64@7.0.2": { + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-darwin-arm64@7.0.2": { + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@typescript/typescript-darwin-x64@7.0.2": { + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@typescript/typescript-freebsd-arm64@7.0.2": { + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-freebsd-x64@7.0.2": { + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-linux-arm64@7.0.2": { + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@typescript/typescript-linux-arm@7.0.2": { + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@typescript/typescript-linux-loong64@7.0.2": { + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@typescript/typescript-linux-mips64el@7.0.2": { + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@typescript/typescript-linux-ppc64@7.0.2": { + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@typescript/typescript-linux-riscv64@7.0.2": { + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@typescript/typescript-linux-s390x@7.0.2": { + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@typescript/typescript-linux-x64@7.0.2": { + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@typescript/typescript-netbsd-arm64@7.0.2": { + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-netbsd-x64@7.0.2": { + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-openbsd-arm64@7.0.2": { + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@typescript/typescript-openbsd-x64@7.0.2": { + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@typescript/typescript-sunos-x64@7.0.2": { + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@typescript/typescript-win32-arm64@7.0.2": { + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@typescript/typescript-win32-x64@7.0.2": { + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "os": ["win32"], + "cpu": ["x64"] + }, + "JSONStream@1.3.5": { + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dependencies": [ + "jsonparse", + "through" + ], + "bin": true + }, + "ajv@8.20.0": { + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dependencies": [ + "fast-deep-equal", + "fast-uri", + "json-schema-traverse", + "require-from-string" + ] + }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "argparse@2.0.1": { + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "array-ify@1.0.0": { + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==" + }, + "callsites@3.1.0": { + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "chalk@5.6.2": { + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" + }, + "cliui@8.0.1": { + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": [ + "string-width", + "strip-ansi", + "wrap-ansi" + ] + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "compare-func@2.0.0": { + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dependencies": [ + "array-ify", + "dot-prop" + ] + }, + "conventional-changelog-angular@7.0.0": { + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dependencies": [ + "compare-func" + ] + }, + "conventional-changelog-conventionalcommits@7.0.2": { + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dependencies": [ + "compare-func" + ] + }, + "conventional-commits-parser@5.0.0": { + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dependencies": [ + "JSONStream", + "is-text-path", + "meow", + "split2" + ], + "bin": true + }, + "cosmiconfig-typescript-loader@6.3.0_@types+node@26.4.0_cosmiconfig@9.0.2__typescript@7.0.2_typescript@7.0.2": { + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "dependencies": [ + "@types/node", + "cosmiconfig", + "jiti", + "typescript" + ] + }, + "cosmiconfig@9.0.2_typescript@7.0.2": { + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dependencies": [ + "env-paths", + "import-fresh", + "js-yaml", + "parse-json", + "typescript" + ], + "optionalPeers": [ + "typescript" + ] + }, + "dargs@8.1.0": { + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==" + }, + "dot-prop@5.3.0": { + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dependencies": [ + "is-obj" + ] + }, + "emoji-regex@8.0.0": { + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "env-paths@2.2.1": { + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" + }, + "error-ex@1.3.4": { + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dependencies": [ + "is-arrayish" + ] + }, + "escalade@3.2.0": { + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-uri@3.1.6": { + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==" + }, + "find-up@7.0.0": { + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dependencies": [ + "locate-path", + "path-exists", + "unicorn-magic" + ] + }, + "get-caller-file@2.0.5": { + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "git-raw-commits@4.0.0": { + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dependencies": [ + "dargs", + "meow", + "split2" + ], + "deprecated": true, + "bin": true + }, + "global-directory@4.0.1": { + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dependencies": [ + "ini" + ] + }, + "import-fresh@3.3.1": { + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dependencies": [ + "parent-module", + "resolve-from@4.0.0" + ] + }, + "import-meta-resolve@4.2.0": { + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==" + }, + "ini@4.1.1": { + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==" + }, + "is-arrayish@0.2.1": { + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-fullwidth-code-point@3.0.0": { + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-obj@2.0.0": { + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-text-path@2.0.0": { + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dependencies": [ + "text-extensions" + ] + }, + "jiti@2.6.1": { + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "bin": true + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml@4.3.2": { + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dependencies": [ + "argparse" + ], + "bin": true + }, + "json-parse-even-better-errors@2.3.1": { + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema-traverse@1.0.0": { + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "jsonparse@1.3.1": { + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==" + }, + "lines-and-columns@1.2.4": { + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path@7.2.0": { + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dependencies": [ + "p-locate" + ] + }, + "lodash.camelcase@4.3.0": { + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "lodash.isplainobject@4.0.6": { + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.kebabcase@4.1.1": { + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==" + }, + "lodash.merge@4.6.2": { + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.mergewith@4.6.2": { + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, + "lodash.snakecase@4.1.1": { + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "lodash.startcase@4.4.0": { + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==" + }, + "lodash.uniq@4.5.0": { + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "lodash.upperfirst@4.3.1": { + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==" + }, + "meow@12.1.1": { + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==" + }, + "minimist@1.2.8": { + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "p-limit@4.0.0": { + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dependencies": [ + "yocto-queue" + ] + }, + "p-locate@6.0.0": { + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dependencies": [ + "p-limit" + ] + }, + "parent-module@1.0.1": { + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": [ + "callsites" + ] + }, + "parse-json@5.2.0": { + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": [ + "@babel/code-frame", + "error-ex", + "json-parse-even-better-errors", + "lines-and-columns" + ] + }, + "path-exists@5.0.0": { + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==" + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "require-directory@2.1.1": { + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string@2.0.2": { + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "resolve-from@4.0.0": { + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-from@5.0.0": { + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "semver@7.8.5": { + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": true + }, + "split2@4.2.0": { + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "string-width@4.2.3": { + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": [ + "emoji-regex", + "is-fullwidth-code-point", + "strip-ansi" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex" + ] + }, + "text-extensions@2.4.0": { + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==" + }, + "through@2.3.8": { + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "tinyexec@1.3.0": { + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==" + }, + "typescript@7.0.2": { + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "optionalDependencies": [ + "@typescript/typescript-aix-ppc64", + "@typescript/typescript-darwin-arm64", + "@typescript/typescript-darwin-x64", + "@typescript/typescript-freebsd-arm64", + "@typescript/typescript-freebsd-x64", + "@typescript/typescript-linux-arm", + "@typescript/typescript-linux-arm64", + "@typescript/typescript-linux-loong64", + "@typescript/typescript-linux-mips64el", + "@typescript/typescript-linux-ppc64", + "@typescript/typescript-linux-riscv64", + "@typescript/typescript-linux-s390x", + "@typescript/typescript-linux-x64", + "@typescript/typescript-netbsd-arm64", + "@typescript/typescript-netbsd-x64", + "@typescript/typescript-openbsd-arm64", + "@typescript/typescript-openbsd-x64", + "@typescript/typescript-sunos-x64", + "@typescript/typescript-win32-arm64", + "@typescript/typescript-win32-x64" + ], + "bin": true + }, + "undici-types@8.3.0": { + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + }, + "unicorn-magic@0.1.0": { + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==" + }, + "wrap-ansi@7.0.0": { + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": [ + "ansi-styles", + "string-width", + "strip-ansi" + ] + }, + "y18n@5.0.8": { + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs-parser@21.1.1": { + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yargs@17.7.3": { + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dependencies": [ + "cliui", + "escalade", + "get-caller-file", + "require-directory", + "string-width", + "y18n", + "yargs-parser" + ] + }, + "yocto-queue@1.2.2": { + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.14", + "jsr:@std/fs@^1.0.19", + "jsr:@std/path@^1.0.9", + "npm:@commitlint/cli@19.8.1", + "npm:@commitlint/config-conventional@19.8.1" + ] + } +} diff --git a/docs/plans/2026-08-30-deno-worktrunk-conversion.md b/docs/plans/2026-08-30-deno-worktrunk-conversion.md new file mode 100644 index 0000000..35a83ac --- /dev/null +++ b/docs/plans/2026-08-30-deno-worktrunk-conversion.md @@ -0,0 +1,108 @@ +--- +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +execution: code +title: Deno project setup + worktrunk scripts conversion +created: 2026-08-30 +--- + +# Deno Project + Worktrunk Scripts Conversion + +## Objective + +Initialize this repo as a Deno project and port the 10 bash hook scripts from +`https://github.com/systemfsoftware/systemfsoftware/tree/main/scripts/tools/worktrunk` to Deno +TypeScript, preserving behavior for `wt.toml` integration. + +Wiki query ran: `deno project setup deno.json conventions`, +`converting bash shell scripts to Deno TypeScript worktrunk hooks`, +`worktrunk wt.toml hook scripts Deno vs bash implementation` against software-wiki (7 results, none +directly on Deno project conventions; closest #32318f module-resolution). Web-equivalent: local +`deno --version` 2.9.5 verified; primary Deno docs would be fetched via ctx7 if needed during +implementation. + +## Context + +- Repo `worktrunk-config` is empty (no commits, only `.git` + remote `origin`). +- Source: 10 files in `systemfsoftware/systemfsoftware/scripts/tools/worktrunk` (listed via GitHub + API 2026-08-30): + - `lib.sh`, `pre-start.sh`, `copy-codegraph.sh`, `codegraph-worktree-mcp.sh`, + `convert-to-relative-paths.sh`, `generate-artifacts.sh`, `install-deps.sh`, `post-switch.sh`, + `pre-merge.sh`, `worktree-to-relative.sh` +- Requirements: 1) `deno.json` project setup, 2) copy+convert scripts to Deno. + +## Requirements + +### Functional + +- R1: `deno.json` exists at repo root with tasks, fmt/lint config, imports for `@std/*` as needed. +- R2: All 10 bash scripts have Deno equivalents under `scripts/tools/worktrunk/` (or + `scripts/worktrunk/`) as `.ts` files with `#!/usr/bin/env -S deno run --allow-*` shebangs, exact + allow scopes, and no `--allow-all`. +- R3: Each Deno script preserves observable behavior: gitdir relative conversion, shared dir + symlinking, codegraph warm-copy fallback chain (sqlite .backup → reflink → cp), MCP provisioning, + dep detection, artifact generation, pre-merge cleanup, post-switch config unset, bulk conversion. +- R4: `wt.toml` / `.config/wt.toml` updated to invoke `deno run` scripts (or shim) if required for + hook integration. + +### Non-functional + +- N1: `deno check`, `deno lint`, `deno fmt --check` pass. +- N2: Executable permissions preserved; async I/O awaited, no `*Sync` in async paths. + +## Units + +### U1: Deno project scaffold + +- Files: `deno.json`, `deno.lock` (after `deno install`/`deno cache`), `.gitignore` entry for + `.deno/` if needed. +- Tasks: `deno task` entries for `check`, `lint`, `fmt`, `test` as applicable. +- Verification: `deno --version` ok, `deno task check` runs `deno check`/`lint`/`fmt`. + +### U2: Shared lib.ts (port of lib.sh) + +- Source `lib.sh: resolve_primary_repo` → + `lib.ts: resolvePrimaryRepo(worktreeRoot: string): Promise` using `Deno.Command` + `git rev-parse`. +- Verification: unit test or manual `deno run` with temp worktree fixture. + +### U3: pre-start.ts, post-switch.ts, pre-merge.ts, convert-to-relative-paths.ts, worktree-to-relative.ts + +- Port path-conversion logic (`realpath --relative-to` → `std/path` relative + `Deno.realPath`), + gitdir file I/O, symlink handling. +- Keep idempotency. +- Verification: run each with fixture dirs, assert file contents. + +### U4: copy-codegraph.ts + codegraph-worktree-mcp.ts + +- Port warm-copy chain, integrity check via `sqlite3` if present, `cp --reflink` fallback via + `Deno.Command`. +- MCP provisioning: `Deno.Command` for codegraph CLI, Python-like JSON merge rewritten in TS via + `@std/json`/`Deno.readTextFile`. +- Verification: dry-run with mocked `codegraph`/`sqlite3` not present paths covered. + +### U5: install-deps.ts + generate-artifacts.ts + +- Detect lockfiles (`pnpm-lock.yaml`, `package-lock.json`, etc.) and run equivalent `Deno.Command` + with correct args. +- Verification: `deno check` type-checks; manual invocation in repo with no lockfile → skips. + +## Verification + +- `deno check scripts/tools/worktrunk/*.ts` +- `deno lint` +- `deno fmt --check` +- Manual smoke: + `deno run --allow-read --allow-run --allow-write scripts/tools/worktrunk/pre-start.ts --help` or + with temp dir. +- Ensure `git ls-files` shows new `.ts` files, no `.sh` left unless shim retained. + +## Risks + +- `realpath --relative-to` semantics differ on missing paths → handle via `std/path.relative`. +- Worktree detection depends on `git rev-parse` output normalization. +- `codegraph` binary absent → must be best-effort, not failure. + +## Out of Scope + +- Publishing to JSR, CI workflow changes beyond basic `deno task` verification. diff --git a/docs/solutions/tooling/bash-to-deno-port-semantics.md b/docs/solutions/tooling/bash-to-deno-port-semantics.md new file mode 100644 index 0000000..28997b2 --- /dev/null +++ b/docs/solutions/tooling/bash-to-deno-port-semantics.md @@ -0,0 +1,54 @@ +--- +title: Bash-to-Deno hook ports must preserve set -e and expansion semantics +date: 2026-08-30 +category: tooling +module: worktrunk hooks +problem_type: best_practice +component: tooling +severity: high +applies_when: + - Porting bash lifecycle hooks to Deno TypeScript + - Verifying a port preserves observable behavior against the original +tags: [bash-port, set-e, exit-code, glob, deno] +--- + +# Bash-to-Deno hook ports must preserve set -e and expansion semantics + +## Context + +Porting the 10 bash worktrunk hooks to Deno exposed three silent-behavior gaps that a line-by-line port misses: child exit codes, shell glob semantics, and variable expansion. Each one shipped green and broke the bash contract. + +## Guidance + +- **Propagate every child exit code.** Bash `set -e` aborts the hook when `npm ci` or `pnpm build` fails. The Deno port must `Deno.exit(code)` on non-zero child output, not log-and-continue: + +```ts +const { code } = await cmd.output() +if (code !== 0) { + console.error(`build exited ${code}`) + Deno.exit(code) +} +``` + +- **Do not translate a shell glob to a regex by escaping metacharacters.** Escaping `[` breaks character classes — `[0-9]*-*.md` became `^\[0-9\].*-.*\.md$` and matched nothing. Write the regex from the glob's matched set, or test the port against `shopt -s nullglob` expansion. +- **Bash substitution and JS regex collapse are NOT equivalent.** Bash `${x//--/-}` is one non-overlapping pass: `a---b` -> `a--b`. JS `/--+/g` fully collapses: `a-b`. Port instance/name sanitization with the same pass semantics (`/--/g`), or the derived socket paths diverge. +- **Check template variables against the caller's docs.** wt.toml exposes `{{ primary_worktree_path }}`, not `{{ primary_path }}`; the undefined variable aborts hook expansion at runtime. +- **Verify symlink-removal semantics empirically per runtime.** On Deno 2.9, `Deno.remove` on a symlink removes the link and keeps the target (refutes denoland/deno#1947 claims of target deletion). Do not trust issue-tracker claims over a one-line runtime test. + +## Why This Matters + +Hooks run as worktree lifecycle callbacks; a hook that exits 0 after a failed install lets the merge proceed with broken state and misattributes later failures. Divergent instance names produce dead MCP sockets that fail confusingly. + +## When to Apply + +- Any bash-to-TS/Deno port reviewed for behavioral parity. +- Hooks whose failure must block the caller (pre-merge, post-start install/build). + +## Examples + +- `install-deps.ts` / `generate-artifacts.ts` now exit non-zero when the child command fails (restores `set -e` aborts). +- `codegraph-worktree-mcp.ts` sanitization matches bash's non-overlapping dash collapse. + +## Related + +- docs/plans/2026-08-30-deno-worktrunk-conversion.md diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..012b70a --- /dev/null +++ b/dprint.json @@ -0,0 +1,31 @@ +{ + "lineWidth": 120, + "typescript": { + "semiColons": "asi", + "quoteStyle": "preferSingle", + "trailingCommas": "onlyMultiLine", + "binaryExpression.operatorPosition": "sameLine" + }, + "json": {}, + "markdown": {}, + "toml": {}, + "yaml": {}, + "excludes": [ + "**/node_modules", + "**/dist", + "**/*-lock.json", + ".turbo", + ".worktrees/**", + "pnpm-lock.yaml", + "**/etc/*.api.md", + "vendor/**", + "repos/**" + ], + "plugins": [ + "https://plugins.dprint.dev/typescript-0.96.1.wasm", + "https://plugins.dprint.dev/json-0.19.3.wasm", + "https://plugins.dprint.dev/markdown-0.17.8.wasm", + "https://plugins.dprint.dev/toml-0.6.2.wasm", + "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm" + ] +} diff --git a/repos/constitution/.claude-plugin/marketplace.json b/repos/constitution/.claude-plugin/marketplace.json new file mode 100644 index 0000000..f8f9627 --- /dev/null +++ b/repos/constitution/.claude-plugin/marketplace.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://anthropic.com/schemas/claude-code-marketplace.json", + "name": "systemfsoftware-marketplace", + "owner": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "plugins": [ + { + "name": "constitution", + "source": "./plugins/constitution", + "description": "Time-Traveling Stream Rules (TTSR) enforcing strict adherence to the Supreme Engineering Constitution (Articles I–V).", + "version": "0.1.0", + "author": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "homepage": "https://github.com/systemfsoftware/constitution", + "repository": "https://github.com/systemfsoftware/constitution", + "license": "Apache-2.0", + "keywords": [ + "constitution", + "ttsr", + "rules", + "omp", + "claude-code" + ] + } + ] +} diff --git a/repos/constitution/.gitignore b/repos/constitution/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/repos/constitution/.husky/commit-msg b/repos/constitution/.husky/commit-msg new file mode 100755 index 0000000..74f6a8c --- /dev/null +++ b/repos/constitution/.husky/commit-msg @@ -0,0 +1 @@ +deno run --allow-read --allow-env --allow-run --allow-sys npm:@commitlint/cli@21 --edit $1 diff --git a/repos/constitution/.husky/pre-commit b/repos/constitution/.husky/pre-commit new file mode 100755 index 0000000..9b938ee --- /dev/null +++ b/repos/constitution/.husky/pre-commit @@ -0,0 +1 @@ +deno task test diff --git a/repos/constitution/.omp-plugin/marketplace.json b/repos/constitution/.omp-plugin/marketplace.json new file mode 100644 index 0000000..f8f9627 --- /dev/null +++ b/repos/constitution/.omp-plugin/marketplace.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://anthropic.com/schemas/claude-code-marketplace.json", + "name": "systemfsoftware-marketplace", + "owner": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "plugins": [ + { + "name": "constitution", + "source": "./plugins/constitution", + "description": "Time-Traveling Stream Rules (TTSR) enforcing strict adherence to the Supreme Engineering Constitution (Articles I–V).", + "version": "0.1.0", + "author": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "homepage": "https://github.com/systemfsoftware/constitution", + "repository": "https://github.com/systemfsoftware/constitution", + "license": "Apache-2.0", + "keywords": [ + "constitution", + "ttsr", + "rules", + "omp", + "claude-code" + ] + } + ] +} diff --git a/repos/constitution/AGENTS.md b/repos/constitution/AGENTS.md new file mode 100644 index 0000000..bb2c79f --- /dev/null +++ b/repos/constitution/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md — Constitution Repository + +Single source of truth for the supreme design law of [System F Software](https://systemfsoftware.com). Consumer repos vendor via `git subtree` + symlink. This repo has no production code, no test suite, and no build step — it is two markdown documents plus their governance tooling (commit validation, agent harness): `CONSTITUTION.md`, resident in every agent's context, and `CONSTITUTION-ARTICLES.md`, retrieved on write or edit of a source file. + +@CONSTITUTION.md + +## Startup Workflow + +Before making changes: + +1. **Read this file** completely. +2. **Confirm the active task** with the user or the agent's task list. +3. **Review recent commits** with `git log --oneline -5`. +4. **Ensure current branch is not `main`** — feature branches only. If on main, create one. + +## Working Rules + +- **One task at a time.** Finish before starting the next. +- **Conventional commits required.** The commit-msg hook enforces `type(scope): description`. Run `git commit` through the hook — do not bypass with `--no-verify`. +- **Verification required.** Run the verification commands before claiming done. +- **Stay in scope.** Don't modify files unrelated to the task. Scope reduction requires explicit user approval. +- **Leave clean state.** The next session must run verification immediately. + +## Amending the Constitution + +### File Split + +- **`CONSTITUTION.md` (Resident):** Conduct rules (Article V + Governance) loaded in every session. +- **`CONSTITUTION-ARTICLES.md` (Retrieved):** Domain, boundary, verification, and organization rules (Articles I–IV) loaded on write/edit of source code. + +### Writing a Rule + +Rules are fenced YAML blocks with: `id`, `title`, `gate`, `do`, `dont`, `harm`, `check` (and optional `example`, `scope`, `layers`). + +### Minting an ID + +ID format: `CONST-`. Pick the next free number in the family (never renumber to close gaps). + +| Letter | Family | Purpose | +|---|---|---| +| `G` | Governance | Invoking constitution or resolving priority | +| `E` | Enforcement | Gate design, execution, verification | +| `P` | Purity | Decision functions and side-effect isolation | +| `D` | Domain modelling | Domain types and constraints | +| `B` | Boundary | Core/shell boundaries, effects, adapters | +| `T` | Testing | Testing strategy, mutation, properties | +| `N` | Naming & structure | Module organization and naming | +| `W` | Work discipline | Task scope, bypass declarations, reviews | +| `S` | Subtraction | Code deletion and structural simplification | + +### Changing a Rule + +- Same obligation reworded / moved: **keep ID**. +- Obligation narrowed, widened, split, or deleted: **retire old ID forever** and mint new one. +## Surface Classes + +| Surface | Files | Rule | +|---|---|---| +| **Locked** | `AGENTS.md`, `.husky/_/`, verification scripts | Read and propose changes; do not edit to make verification pass. | +| **Editable** | `deno.json`, `deno.lock`, `commitlint.config.cjs`, `.gitignore`, `.husky/` (hooks only, not `_/`) | Edit freely within the active task. | +| **Human-controlled** | `CONSTITUTION.md`, `CONSTITUTION-ARTICLES.md`, `README.md`, merging to `main`, pushing, destructive ops | Propose changes; ask the user before acting. | + +## Definition of Done + +A task is done only when ALL of the following are true: + +- [ ] Target changes are applied. +- [ ] Verification commands ran and passed. +- [ ] Commit uses conventional format (`type(scope): description`). +- [ ] Evidence recorded via the runtime memory system and task list. +- [ ] No dirty files left in the working tree. + +## Verification Commands + +```bash +deno task test # both files: schema, coverage, ids, families, dangling citations +deno run --allow-read --allow-env --allow-run npm:@commitlint/cli@21 --from HEAD~1 +``` + +After a commit that deletes, splits, merges, or re-scopes a rule — not after every edit — also run the reassignment check against the revision before it: + +```bash +deno task test --against +``` + +### Anti-Bypass Rules + +- Run the **full command**, not parts in isolation. +- Evidence must be from the **current run**, not a prior session. +- **Any failure blocks done.** Do not bypass with `--no-verify`. +- Do not suppress, skip, or disable checks to make verification pass. + +### Hallucination Prevention + +- **Read before edit:** before editing a file, read it in the current session. Do not edit from memory. +- **Verify before claim:** before saying "done," the verification command must have run and its output recorded. +- **Search before write:** before writing code that calls a library API, read the actual API surface. Do not generate from training memory. + +## Multi-Agent Ownership + +When multiple agents work in the same repo: + +- Each agent owns a disjoint file/module set. +- An agent must claim a file before editing it. +- Agents may not recursively delegate to each other. +- The one-shot verification must pass before any agent claims done. + +## End of Session + +Before ending a session: + +1. Record current state, blockers, and next steps via the runtime memory system and task list. +2. Commit with a conventional-format message once work is in a safe state. +3. Leave the repo clean — `git status` should show nothing unexpected. + +## Escalation + +- **Constitution conflict**: `CONSTITUTION.md` is already in context — reread it there, not from disk. +- **Unclear requirements**: Ask the user. +- **Verification failure**: Record via memory, flag for review, do not bypass. +- **Scope ambiguity**: Re-read this file and the Definition of Done. diff --git a/repos/constitution/CLAUDE.md b/repos/constitution/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/repos/constitution/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/repos/constitution/CONSTITUTION-ARTICLES.md b/repos/constitution/CONSTITUTION-ARTICLES.md new file mode 100644 index 0000000..f9c650f --- /dev/null +++ b/repos/constitution/CONSTITUTION-ARTICLES.md @@ -0,0 +1,294 @@ +# Constitution — Articles + +Retrieved, not resident. `CONSTITUTION.md` is supreme and always in context; these four articles are its craft law, delivered when the work reaches the artifact each one governs. Article V (conduct) in `CONSTITUTION.md` binds here unchanged. + +Deliver this file on **write or edit** of a source file, never on read: an agent that greps, or works from a plan, never fires a read trigger. The trigger condition is the law's; the mechanism that fires it — a path-scoped rule, a pre-tool gate — is the consuming harness's, and belongs in that repo's `AGENTS.md`, never here. + +--- + +## Article I — The Pure Core + +```yaml +rules: + - id: CONST-P1 + title: Purity + gate: lint + do: each domain decision is a pure function — data in, a value or typed error out + dont: + - in a decision, do I/O, throw, read a clock, or use randomness + - return an effect handle from a decision — if it needs the runtime, move the boundary, not the purity + harm: logic untestable over all inputs, untrustworthy + check: lint — decisions import no I/O or effect-runtime; mutation + - id: CONST-D1 + title: Types Before Logic + gate: type-checker + do: define types before behavior; make illegal states unrepresentable so bad data fails to compile + dont: start from functions and add types after + harm: invalid data reaches runtime; tests multiply to cover what a type could forbid + check: type-checker rejects the illegal state; review + - id: CONST-D2 + title: Each Error Its Own Variant + gate: lint + do: give every distinct failure its own tagged variant + dont: distinguish failures by a boolean or string field + harm: callers can't branch on the real failure; distinct errors collapse into one case + check: lint; review — callers branch on the variant tag, never on a field value + - id: CONST-D3 + title: No Primitive Obsession + gate: lint + do: brand every domain-meaningful value (ids, amounts, codes) as its own type + dont: pass bare text or number in a domain-significant position + harm: values transposed or misused; the type says nothing about what they are + check: lint — no bare primitives in domain signatures + - id: CONST-D4 + title: Null Is Not a State — but absence is fine for optional data + gate: lint + do: + - model mutually-exclusive states as a tagged union — one variant per state, each carrying only its valid fields + - use a plain nullable for a value absent identically in every state + dont: + - encode a state by which fields are present + - wrap such a field in Option/Maybe to "fix" it — the wrapper renames the hole, not closes it + harm: a state machine hidden in a record; the compiler can't reject invalid field combinations — the question is never "null or Option" but "a value that may not exist, or a state in disguise" + check: lint — flags an optional that correlates with the discriminant, not plain optionals; review + example: + wrong: Order { status, shippedAt?, trackingId? } — state by presence; an Option wrapper is the same defect + right: Order = Pending { placedAt } | Shipped { placedAt, shippedAt, trackingId } + fine: Customer { name, middleName? } — genuinely optional; plain nullable, no wrapper + - id: CONST-P2 + title: The Pure Core Has One Path (Cyclomatic Complexity 1) + gate: lint + do: write each core decision as a single path — choice as exhaustive dispatch over a closed type (match a tagged union), iteration as map/fold; the core is an expression, not a procedure + dont: + - in the core, use if/else, switch, ?:, or &&/|| for control + - in the core, use for/while — repetition moves into map/fold + scope: binds the pure core (decision and workflow files); the ban is on the control-flow form, not branching — a core function reads as one path yet still decides and iterates; the shell sequences steps and carries no decisions, its only structure is the sandwich (CONST-B3); the gate runs on core files, not the shell + harm: every branch is an untested path where state silently diverges — the mutator reaches it, the suite does not + check: lint — cyclomatic complexity = 1 on core files (match, map, fold are calls, not control flow, so they hold at 1; if/switch/loops raise it) + example: + wrong: if (o.kind === "Shipped") ship(o) else hold(o) — two paths in a decision + right: match(o) { Shipped -> ship, Pending -> hold } — one exhaustive dispatch over a closed type + wrong_iteration: fold over the data with a for-loop in the core — iterating the core as a procedure + right_iteration: fold(xs, 0, add) — iterate as one expression (a shell loop is fine — that's the shell) +``` + +--- + +## Article II — The Boundary + +```yaml +rules: + - id: CONST-B1 + title: Functional Core, Imperative Shell + gate: review + do: split every module into a pure core (decisions) and a thin shell (I/O); pass plain serializable data across the seam + dont: let a boundary object (handler, adapter, middleware) make a decision — it only translates external ↔ domain + harm: decisions tangled with I/O can't be tested without mocks; bugs hide in the boundary + check: review — a boundary object that needs its own test suite has logic in it; move it to the core + - id: CONST-B2 + title: Effects Are Values + gate: lint + do: return effects as lazy values (descriptions), interpreted once at the edge; attach logging, metrics, tracing as decorators on the value + dont: + - put an eager async result (promise, future, task) on the public surface + - embed a cross-cutting concern in a decision + harm: an already-started result can't be held, retried, or swapped; embedded concerns can't be turned off or composed + check: lint — no eager async result on the public surface + example: + wrong: "getUser : UserId ->
" + right: "getUser : UserId -> Effect — a lazy value, interpreted once at the edge" + - id: CONST-B3 + title: The I/O Sandwich + gate: review + do: shape every outside interaction as read (impure) → transform (pure) → write (impure); the shell calls the core directly + dont: insert a layer that only passes work through without a read, transform, or write + harm: side effects leak into business logic; pass-through layers add coupling for nothing + check: review — pass-through delegation is the violation; the shell doing the read/transform/write, or sitting between transport and core, is not + example: + flow: | + read → decode → decide → shape → write + impure bread (read, write) around a thick pure filling (decode, decide, shape), no I/O between the pure steps. + read pull raw inputs — store, gateway, network, clock (impure) + decode validate raw → branded domain types (fail as data) (pure) + decide one decision over typed data → Decision | Error (pure) + shape build outputs and events from the Decision (pure) + write persist · emit · respond (impure) + wrong: read → decide → read → decide — I/O interleaved; the filling turns impure + edge: a later read that depends on an earlier decision — pre-fetch it, split into two sandwiches, or keep it openly in the shell; never fake a "pure core" around it + - id: CONST-B6 + title: The Sandwich Order Is Carried by Types + gate: type-checker + do: express an outside interaction as one phase chain — each phase's return type carries the required member the next phase's parameter demands — so the order is a consequence of the types and the compiler decides it + dont: + - hand-sequence the phases and state their order beside them; an order asserted in prose is decided by nothing + - give the phases a hierarchy — where a later phase's type is assignable to an earlier phase's parameter, an inversion still compiles + harm: an order nothing decides permits every permutation while reading as a guarantee, so the interleaved read that turns the filling impure — the defect CONST-B3 names — reaches production with the rule green + check: type-checker — composing the phases in the wrong order omits the required member, so the compiler names the phase that must come first; the sentence survives into the published declaration as that member's own name, which is what carries it into a consumer's compiler + example: + wrong: "write(decide(read(raw))) — hand-sequenced; every permutation type-checks, so the order is a comment" + right: "read : Raw -> ReadDone, decode : ReadDone -> DecodeDone, decide : DecodeDone -> DecideDone — decode cannot receive what read has not produced" + - id: CONST-B4 + title: Dependencies Point Inward + gate: lint + do: let the shell import the core; wire all implementations at one composition root + dont: let the core import the shell, the database, or the framework + harm: a decision layer chained to infrastructure can't be tested or replaced + check: import-graph lint + - id: CONST-B5 + title: Decode, Never Cast + gate: lint + do: turn outside data (bytes, serialized text, a foreign type) into a domain type via a decode returning a typed result + dont: + - assert type with an unchecked cast (`as`, `as unknown as`, `as any`) + - assert type with a suppression comment + harm: a shape nothing verified; everything downstream trusts a check that never ran + check: lint — no unchecked casts or suppression comments on outside data + example: + wrong: config := value as Config + right: "config := decode(value) : Result" + - id: CONST-P3 + title: Purity Is Per Function, Not Per Folder + gate: review + do: judge pure-versus-effectful by return type alone + dont: infer it from a folder, package, or "library versus application" + harm: a database-driver mislabeled "pure," a parser "impure," because of where it lives + check: review — return type decides; the lint behind CONST-P1 + example: + pure: "decide : Command -> Result" + effectful: "load : OrderId -> Effect — owns effects" +``` + +--- + +## Article III — Verification + +```yaml +rules: + - id: CONST-T8 + title: Test Public Functions Directly, Pure Logic with Mutation + gate: review + do: test the public API with real inputs and outputs; test internal calculation and branching logic with mutation tests (and properties only when CONST-T14 requires them); never write dedicated unit tests for code that only forwards calls between components + dont: + - write unit tests for intermediate helper functions that only pass data to other functions + - mix I/O code or adapters into the same mutation test run as pure calculation logic + - mock a dependency when only one real implementation exists + harm: unit-testing intermediate layers locks in private implementation details without catching real bugs; mocking real code gives false confidence; business rules stay tangled with I/O + check: review — every test calls either a public export or a pure decision function, never a private forwarding helper + - id: CONST-T14 + title: Properties Where the Surface Cannot Reach + gate: review + do: prove a pure decision with a property when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; the type is the generator + dont: + - cover the core with hand-picked example unit tests + - write a property for a decision already fully pinned from above just because the decision is important + harm: a green suite that tests only the cases you imagined; or a property farm that restates the public contract and dies with it + check: review — each authored property names the universal the public surface cannot reach + - id: CONST-T3 + title: Mutation Is the Measure + gate: mutation + do: gate a named, change-relevant mutated set at a perfect kill score; the set names the behavior it covers, and its scope is a cost decision, never a fault-majority claim; kill a survivor with a sharper property or by deleting the dead branch it exploits + dont: + - reach the number by a suppression comment + - reach the number by narrowing the mutated set after the fact + - reach the number by lowering the gate + - let an empty mutated set pass + - treat a raw mutation percentage as comparable across changes or codebases + harm: a score certifying tests that notice nothing; an empty or author-shrunk set passing vacuously + check: mutation gate (break = 100) on the declared mutated set; lint banning suppression, scope-narrowing, and an empty set + - id: CONST-T13 + title: Mutation Also Grades the Tests + gate: mutation + do: fail a run whose mutants all died if an authored property file defends nothing the rest of the suite does not; opt out in the mutation config, never by deleting the file the gate named + dont: + - treat a perfect mutant score as proof every test pulled its weight + - accuse a file that covered an unattributed kill + harm: toothless properties accumulate; deleting them to silence the gate removes the only named contract + check: mutation — the test-set verdict is part of the same run as the score + - id: CONST-T4 + title: Behavior Lives Where the Mutator Sees It + gate: lint + do: put any code that can be wrong (transform, check, branch) in a file the mutator covers + dont: place behavior in a declaration file (types, schemas, constant data), excluded from mutation + harm: a bug hidden behind a perfect score, in a file nothing mutates + check: lint — declaration files contain no behavior + - id: CONST-T9 + title: Pin the Published Contract Before You Delete a Path + gate: review + do: before removing or replacing a published operation, pin its observables (value, error variant, serialized document, process result) with examples or properties whose expected side is not the implementation under change; if the old operation still runs, compare old and new on the same published inputs until they agree, then delete old + dont: + - pin private functions + - derive expected values by running the implementation under change + - treat a mutation or property score as proof a deleted published capability still exists — those are blind to absence + - leave a persisted gold after the old path is gone unless the gold is externally authored, independently gated, and cheap to re-bless + harm: a rebuild silently drops a capability; same-session gold blesses the bug; a clean score after a delete is a silent regression + check: review — pins call only published names; each expected value names an independent source (spec clause, prior published major, second implementation, or a hand-written oracle next to the constructor) + - id: CONST-T10 + title: The Oracle Is Not the System Under Test + gate: review + do: every assertion has an oracle the SUT did not produce — a spec literal, a fixture not generated by importing the module, a law relating two views of the same value, or a second implementation; generated round-trip laws on a type cover what the type accepts and nothing it should reject, so a hand-written refusal survives beside them at any specifiable refusal boundary + dont: + - compute expected by calling the SUT + - assert collaborator call graphs + - treat generated accept-laws as full coverage of a refinement + harm: a green suite that cannot fail when the behavior is wrong; widening a refinement leaves generated laws green + check: review — plus sabotage (after green, break one core law and one published field; at least one test must go red) + - id: CONST-T11 + title: Snapshots and Differentials Are Published-Surface Oracles + gate: lint + do: snapshot only canonicalized published output; compare two implementations only of the same published operation (or a prior published major against current) + dont: + - snapshot or compare private helpers, mappers, or unexported modules + - snapshot a value small enough to be a property or a named example + harm: tests that fail on refactors callers cannot see and pass on contract breaks they can + check: lint — snapshot and differential fixtures are produced only through the package's published export map + - id: CONST-T12 + title: What a Test Does Comes from What It Calls, Not Its Filename + gate: lint + do: classify what a test is by what it imports and calls — public exports or pure logic under mutation — never by its folder, filename, or file extension + dont: decide which testing rules apply to a file based on its name or suffix + harm: renaming a test file secretly stops its rules from running while the test suite still looks complete + check: lint — no linter or test runner rules that pick tests by filename suffix + - id: CONST-E5 + title: A Gate's Key Is Recomputed, Never Reported + gate: review + do: + - key every gate on a recomputation from source bytes, a compiler verdict, or a rehash — never on a field the gated work's author supplied; when a gate reads a field, recompute that field in the same run + - treat a gate whose verdict the gated agent can produce or observe as unverified until an independent channel confirms it — an instrument the agent does not control, or review by someone who is not the gated agent + dont: + - accept a self-reported field, a presence flag, a metadata suffix, or a comment as evidence a property holds + - treat a mechanical gate's green as self-certifying + harm: a check keyed on author-supplied values passes everything and catches nothing, and the green then masks the broken invariant the gate exists to catch; an instrument correlated with the work under test can manufacture a verdict no single observer catches + check: review — each gate names the recomputation it runs and the independent channel that confirms its verdict +``` + +--- + +## Article IV — Organization + +```yaml +rules: + - id: CONST-N1 + title: Organized by What It Does + gate: review + do: organize by workflow and capability; keep code that changes together, together + dont: organize by what the system has (entities, technical layers) + harm: one change scattered across the tree + check: review — one change touches one capability subtree + - id: CONST-N2 + title: Names Scream the Domain + gate: lint + do: name files and folders for the job they do — a name must answer "of what?" + dont: + - use layer names (`core`, `shell`) + - use junk drawers (`util`, `service`, `manager`) + - use a suffix no rule keys on + harm: files no one can locate; meaningless buckets + check: filename lint — allowed suffixes; banned layer and junk-drawer names + - id: CONST-N3 + title: Fits in the Head + gate: review + do: give a module one responsibility; split it when a test needs elaborate setup (the signal it has several) + dont: accumulate unrelated concerns in one module + harm: modules no one can fully reason about; brittle, sprawling tests + check: review — fixture difficulty is the decomposition signal +``` diff --git a/repos/constitution/CONSTITUTION.md b/repos/constitution/CONSTITUTION.md new file mode 100644 index 0000000..e291ef9 --- /dev/null +++ b/repos/constitution/CONSTITUTION.md @@ -0,0 +1,85 @@ +# Constitution + +## Article V — Conduct + +```yaml +rules: + - id: CONST-G3 + title: Constitutional Violations Are Automatic P0 Failures + gate: review + do: treat every undeclared violation of this constitution during review as an automatic, non-negotiable P0 failure; reject the change unconditionally with zero appeals and no severity downgrades unless explicitly declared under CONST-W3 + dont: + - downgrade an undeclared constitutional breach to an advisory, P1, P2, or non-blocking finding + - treat a CONST-W3 declaration as optional prose — an explicit declaration in the change itself is the only legal waiver, and it must name the rule and the case + - accept a promise of follow-up repair or expedience plea to bypass an active rule without a CONST-W3 declaration + harm: constitutional rules decay into optional suggestions; agents negotiate away core architecture to ship faster; unblocked violations calcify into precedent + check: review — every undeclared constitutional violation is graded P0 and blocks approval unconditionally; any review that waives or downgrades an undeclared violation is rejected + - id: CONST-S1 + title: Depth Over Expedience + gate: review + do: fix the root cause; restructure when the design is wrong + dont: patch the symptom or bypass a boundary to ship faster + harm: the bug returns + check: review — the change names the root cause it fixes + - id: CONST-W1 + title: Scope Discipline + gate: review + do: execute accepted scope in full + dont: reduce scope mid-task because it grew complex, without the author's consent + harm: half-finished work; wasted effort second-guessing intent + check: review — delivered scope matches accepted scope + - id: CONST-S2 + title: First Principles Over Precedent + gate: review + do: justify a pattern by these principles — surrounding code is evidence of what exists, never of what is correct + dont: + - justify by "that is how it's done elsewhere" + - copy a neighbouring file as a template — code age grants no immunity + harm: unexamined defaults calcify into rules; one slop pattern seeds the next by imitation, and the average drifts down + check: review — a choice defended by precedent, or by the file next to it, is rejected + - id: CONST-S3 + title: API-First Discovery + gate: review + do: define the outside contract first, then derive use cases, decisions, and machinery beneath it; model only what a known requirement needs + dont: build a domain abstraction for a hypothetical future + harm: speculative structure that never pays off and constrains what comes after + check: review — every abstraction traces to a known requirement + - id: CONST-W2 + title: Challenge Before You Commit + gate: review + do: subject a large or irreversible choice to a deliberate challenge (another agent, a person, or rigorous self-examination), record it with the decision, judge it by the harm it names + dont: + - appeal to a tribunal or standing authority + - let a challenge become a clause quoted against a choice + harm: a costly, hard-to-reverse direction taken with no one trying to break it first + check: review — the challenge is recorded with the decision + - id: CONST-W3 + title: No Silent Bypass + gate: review + do: when you break a rule here — knowingly, or because it was wrong for this case — say so, in the open, in the change itself + dont: conceal a bypass + harm: two failures — the breach and the hiding of it; the next reader trusts a rule quietly broken + check: review — every rule breach is declared in the change that contains it + - id: CONST-S4 + title: Subtract Before You Add + gate: review + do: + - treat every line as a liability — removal is the default response to slop at every scale; adding is the exception you justify + - small — unify duplicates, make bad states unrepresentable, delete a branch instead of guarding it + - structural — when the root violates this document and breeds a bug class, rebuild the core (published contract pinned first (CONST-T9); decomposed into shippable milestones) rather than prune leaves off a rotten trunk + - distrust existing structure — assume rotten until it proves it conforms + dont: + - extend a copy-paste cluster with copy N+1 + - add a helper when removing or unifying one does the job + - patch around a rotten core to keep it alive + - treat code as sound because it compiles, is large, or is old + - mistake taste ("I'd write it differently") for rot + harm: the codebase only grows; rot survives every patch and regrows; each copied pattern seeds the next, and the average drifts down + check: review reads the net line delta — a refactor/improvement/chore that adds net lines states why and names what it deleted (features and their tests are exempt); a fix that leaves a named root violation standing is rejected; "rotten" names the invariant the core breaks; a structural rebuild ships a CONST-T9 pin on every published path it deletes + example: + wrong: add formatPhone() beside the three formatters already there + right: delete the three, keep one parameterised formatter + wrong_state: add a guard for the impossible state the record permits + right_state: delete the record; a tagged union makes the state unconstructable + +``` diff --git a/repos/constitution/LICENSE b/repos/constitution/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/repos/constitution/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/repos/constitution/README.md b/repos/constitution/README.md new file mode 100644 index 0000000..fc3f92c --- /dev/null +++ b/repos/constitution/README.md @@ -0,0 +1,124 @@ +# Constitution + +[![License: Apache-2.0](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE) +[![System F Software](https://img.shields.io/badge/systemfsoftware.com-constitution-black?style=flat-square)](https://systemfsoftware.com/constitution) +[![Rules: 34](https://img.shields.io/badge/rules-34%20in%20corpus-blue?style=flat-square)](CONSTITUTION.md) + +Shared engineering laws for repositories at [System F Software](https://systemfsoftware.com). + +It sets baseline requirements for clean code: a pure functional core behind a thin imperative shell, domain types before logic, mutation testing for decisions, and deleting code before writing more. Principles are stack-neutral, so they apply to any language. + +```mermaid +flowchart LR + S["systemfsoftware/constitution
Upstream Repository"] -->|git subtree| A[Consumer Repo A] + S -->|git subtree| B[Consumer Repo B] + S -->|git subtree| C[Consumer Repo C] + A -.symlink.-> S + B -.symlink.-> S + C -.symlink.-> S +``` + +--- + +## Two files, two roles + +The rules split into two files based on when an agent needs to see them: + +``` +constitution/ +├── CONSTITUTION.md # Resident: loaded on every run +└── CONSTITUTION-ARTICLES.md # On demand: retrieved when editing source files +``` + +| File | Delivery | Contents | How to load it | +| :--- | :--- | :--- | :--- | +| `CONSTITUTION.md` | **Always on** | Conduct (Article V) | Include in agent context on every turn (`@CONSTITUTION.md` in `AGENTS.md` or `CLAUDE.md`) | +| `CONSTITUTION-ARTICLES.md` | **On demand** | Articles I to IV (Pure Core, Boundaries, Testing, Project Layout) | Load via tool hook or path rule when editing source code (never on read) | + +Rules about conduct stay resident because nothing triggers them after a mistake happens. Craft rules (like how to structure a domain model or write a test) only need to load when someone touches source code. + +--- + +## Quick start + +Vendor the repository using `git subtree` and symlink both files to the project root: + +```bash +# 1. Fetch the remote into a local ref +git fetch https://github.com/systemfsoftware/constitution.git main:refs/remotes/vendor/constitution + +# 2. Add as a squashed subtree +git subtree add --prefix=vendor/constitution refs/remotes/vendor/constitution --squash \ + -m "chore: vendor shared constitution" + +# 3. Symlink both files to the repo root +ln -s vendor/constitution/CONSTITUTION.md CONSTITUTION.md +ln -s vendor/constitution/CONSTITUTION-ARTICLES.md CONSTITUTION-ARTICLES.md +``` + +If the repository is brand new, create an initial commit first (`git commit --allow-empty -m "init"`). + +### Connect to your agent harness + +1. Add `@CONSTITUTION.md` to `AGENTS.md` or `CLAUDE.md`. +2. Set up a path-scoped rule (`.claude/rules/` or `.cursor/rules/`) to provide `CONSTITUTION-ARTICLES.md` when editing source files. +3. Or add the marketplace and install the TTSR plugin to intercept violations during edits: + ```bash + omp plugin marketplace add systemfsoftware/constitution + omp plugin install constitution@systemfsoftware-marketplace + ``` + +--- + +## The articles + +| Article | File | Mode | Core rules | +| :--- | :--- | :--- | :--- | +| **I: Pure Core** | `CONSTITUTION-ARTICLES.md` | Retrieved | Pure decisions, explicit types, tagged error variants, no `null` states. | +| **II: Boundaries** | `CONSTITUTION-ARTICLES.md` | Retrieved | Functional core / imperative shell, values for effects, decode inputs rather than casting. | +| **III: Verification** | `CONSTITUTION-ARTICLES.md` | Retrieved | Observer-fit test placement, properties by narrow grant, mutation as the measure, independent oracles, pinned published contracts. | +| **IV: Organization** | `CONSTITUTION-ARTICLES.md` | Retrieved | Organize by domain responsibility, clear naming, keep modules small. | +| **V: Conduct** | `CONSTITUTION.md` | **Always on** | Zero-appeal P0 review enforcement, fix root causes, challenge decisions before committing, remove code before adding. | + +--- + +## Pulling updates + +Pull upstream changes into the subtree without changing existing symlinks: + +```bash +git subtree pull --prefix=vendor/constitution https://github.com/systemfsoftware/constitution.git main --squash \ + -m "chore: update shared constitution" +``` + +--- + +## Machine validation + +Every rule is defined in structured YAML: + +```yaml +- id: CONST-S4 + title: Subtract Before You Add + gate: review + do: treat every line as a liability — removal is the default response to slop + dont: extend a copy-paste cluster; patch around a rotten core + harm: the codebase only grows; rot survives every patch and regrows + check: review reads the net line delta; fixes that leave root violations are rejected +``` + +Run the validator to check rule IDs, schema compliance, and references across both files: + +```bash +deno task test +``` + +--- + +## Contributing + +Amendments need a written explanation, a version bump, and updates to consuming repos. See [AGENTS.md](AGENTS.md) for commit standards and testing guidelines. + +## License + +[Apache-2.0](LICENSE) (c) 2026 Ryan Lee. diff --git a/repos/constitution/commitlint.config.cjs b/repos/constitution/commitlint.config.cjs new file mode 100644 index 0000000..84dcb12 --- /dev/null +++ b/repos/constitution/commitlint.config.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/repos/constitution/deno.json b/repos/constitution/deno.json new file mode 100644 index 0000000..d112f13 --- /dev/null +++ b/repos/constitution/deno.json @@ -0,0 +1,10 @@ +{ + "version": "0.1.0", + "tasks": { + "test": "deno run --allow-read=CONSTITUTION.md,CONSTITUTION-ARTICLES.md --allow-run=git scripts/validate-constitution.ts", + "prepare": "git config core.hooksPath .husky" + }, + "imports": { + "@std/yaml": "jsr:@std/yaml@^1.2.0" + } +} diff --git a/repos/constitution/deno.lock b/repos/constitution/deno.lock new file mode 100644 index 0000000..e264451 --- /dev/null +++ b/repos/constitution/deno.lock @@ -0,0 +1,16 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/yaml@^1.2.0": "1.2.0" + }, + "jsr": { + "@std/yaml@1.2.0": { + "integrity": "20beb41e4983ba3437dbefac62b14061ab058e8a187596f19d28ff9035f6e6cf" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/yaml@^1.2.0" + ] + } +} diff --git a/repos/constitution/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md b/repos/constitution/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md new file mode 100644 index 0000000..c6acc20 --- /dev/null +++ b/repos/constitution/docs/plans/2026-08-29-2245-refactor-article-iii-verification-plan.md @@ -0,0 +1,432 @@ +--- +title: Article III Verification Doctrine Rehaul - Plan +type: refactor +date: 2026-08-29 +supersedes: docs/plans/2026-08-29-2210-refactor-article-iii-verification-plan.md +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-plan-bootstrap +execution: code +--- + +# Article III Verification Doctrine Rehaul - Plan + +## Goal Capsule + +- **Objective:** The constitution's verification law binds only what measured evidence supports — oracle independence, observer-fit test placement, change-relevant mutation over a named non-empty set, published-contract pinning before deletion, and recomputed gate keys — and no longer prescribes the Testing Trophy, mandatory mocked composition, blanket property coverage of the core, or characterization as a standing duty. The corpus gate is Deno. A reader outside this repo can confirm it: `pnpm test` passes with 33 rules across both corpus files, and `pnpm test --against ` run after the corpus commit names exactly CONST-T1, CONST-T2, and CONST-T5 as vacated with no id reassigned. +- **Means:** The corpus gate is rewritten as a Deno script that implements the behavior the repo's own documentation specifies (vacated-id and uncompared-file reporting); Article III is replaced (vacate T1/T2/T5, reword T3, keep T4, mint T8–T14 plus CONST-E5); CONST-S4 is repointed to CONST-T9; AGENTS.md and README.md prose is reconciled (KTD1–KTD5; normative rule text in Appendix A). +- **Authority:** The H1–H12 verdict dossier and the org-mechanism specification (both delivered in the invoking conversation) govern doctrine conflicts. The corpus's own id-surgery table in AGENTS.md governs id handling. Ecosystem tooling law: Deno only for code (software-wiki harness law). The current corpus at `origin/main` (28 rules: 7 resident Article V + 21 retrieved) is the base being amended, not evidence of correctness (CONST-S2). +- **Execution profile:** One feature branch; four conventional commits in order — plan checkpoint, validator, corpus, prose — pushed together in one PR; no consumer-repo changes. +- **Stop conditions:** A gate failure not explained by the amendment's own edits stops the run. An edit whose find-pattern misses (grep still finds the old text after the edit) stops the run for repair. Implementation-time evidence contradicting a settled decision stops the run. +- **Tail ownership:** Branch shipping, PR, and CI watch belong to the calling pipeline. The org tree's watchdog and mutation-config updates belong to that repo after its next vendor pull. + +--- + +## Product Contract + +### Summary + +Replace the Python corpus gate with a Deno script implementing the full documented behavior — schema, coverage, families, gates, dangling citations, cross-revision reassignment, and vacated-id/uncompared-file reporting. Rewrite Article III of `CONSTITUTION-ARTICLES.md` so the verification doctrine matches the evidence: observer-fit placement instead of trophy widths, a narrow grant for property tests instead of a blanket mandate, mutation gated on a named change-relevant set whose run also grades the test files, published-contract pinning instead of characterization, an independent-oracle requirement with a refusal complement, and one enforcement rule (CONST-E5) binding every gate's key to a recomputation. Repoint CONST-S4's rebuild pin from the vacated CONST-T5 to the new CONST-T9, and reconcile AGENTS.md and README.md so no prose teaches the vacated doctrine or a false rule count. + +### Problem Frame + +The org's enforcing tree already implements observer-fit placement, toothless-property detection, mutation of named sets, a contract lane driven from outside the process, and import-keyed test classification. The constitution still describes a Testing Trophy with mocked composition as the widest band and mandatory characterization before every rebuild. Agents obey `check:` lines, so the stale doctrine is not inert prose — it actively instructs behavior the enforcing tree refuted. The dossier (H1–H12, 73 primary sources) adjudicates the doctrine: same-session test generation is tautology-prone without an independent oracle (H1), the two-surface count is underdetermined (H2), decision-only mutation over-claims observer-fit (H5), a decision-layer fault-majority is false as a census (H6), and gates bind through accountability while advisory surfacing is inert (H12). The constitution must state principles at that altitude and leave mechanism binding to consumer trees, because consumer repos in several languages vendor this file. + +The amendment prunes refuted doctrine and replaces it with a larger instruction surface: Article III grows from 5 to 9 rules and the corpus from 28 to 33. That growth is deliberate — each minted rule names an observer, oracle, or gate discipline the old five could not express, and the refuted doctrine is gone from every line the nine now occupy. The no-retirement id rule means each mint is permanent; that cost is accepted per rule, not in aggregate. + +### Key Decisions + +- **Vacate CONST-T1 and CONST-T5 rather than reword them.** The trophy and the characterization duty are refuted doctrine, not rewordable ones; their numbers stay vacant forever per the corpus's own id-surgery rule. (session-settled: user-directed — chosen over keeping trophy and characterization as reworded doctrine: the org tree's measured mechanisms and the dossier refuted both.) Governs R2, R3, R20. +- **The dossier adjudicates doctrine conflicts.** Where the org tree's current mechanisms and the evidence disagree, the evidence wins: mutation scope is change-relevant, not decision-only (H5); the organizing principle is oracle independence, not a surface count (H2). (session-settled: user-directed — chosen over encoding the org repo's mechanisms verbatim: H5 refuted decision-only scoping as observer-fit and H2 refuted the exhaustive two-surface claim.) Governs R5, R8. +- **CONST-E5 ships in this amendment.** The unforgeable-gate rule is in scope now, not deferred. (session-settled: user-directed — chosen over deferring gate-verification doctrine to a later amendment: user directed inclusion when the deferral was presented as default.) Governs R14. +- **Principles only — no tool names, no consumer names, no filename suffixes in rule YAML.** Mutation is an instrument class; how a tree scopes its mutant set is that tree's config. (session-settled: user-directed — chosen over naming the enforcing stack as exemplar: consumer repos in other languages vendor this file, and a named first consumer turns a house rule into law.) Governs R19. + +### Requirements + +**Corpus gate (Deno)** + +- R1. `scripts/validate-constitution.py` is deleted; a Deno script at `scripts/validate-constitution.ts` implements the same gate — the two-file corpus contract, fenced-YAML coverage accounting, the rule schema (required, optional, and gate fields), the family registry, duplicate-id detection, dangling-citation resolution across both files, and the `--against` cross-revision reassignment check — plus the vacated-id and uncompared-file reporting on the success line that AGENTS.md and `docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md` already specify. Exit codes: 0 clean, 1 with named defects, 3 unmeasurable. `pnpm test` invokes the script directly; the shebang carries the permission flags (read limited to the two corpus files, run limited to `git`); the `@std/yaml` dependency is declared in `deno.json` imports. + +**Article III surgery** + +- R2. CONST-T1 is removed from `CONSTITUTION-ARTICLES.md`; the number is never reused; no corpus text prescribes trophy layers, widths, or mocked composition as doctrine. +- R3. CONST-T5 is removed; "characterization" appears nowhere in the corpus as a standing duty. +- R4. CONST-T2 is removed; its blanket property mandate does not survive in any reworded rule (see KTD1 for why the replacement takes a fresh id and a fresh title). +- R5. CONST-T3 keeps its id and title "Mutation Is the Measure" with a reworded body: the gate covers a named, non-empty, change-relevant mutated set; the `dont` list covers suppression, after-the-fact narrowing, gate-lowering, an empty set, and raw cross-project percentage comparison. +- R6. CONST-T4's text stands byte-identical. +- R7. CONST-T14 "Properties Where the Surface Cannot Reach" is minted: a property is earned when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; a `dont` forbids properties that restate a decision already pinned from above. +- R8. CONST-T8 "The Observer Must See the Fault Class" is minted: each behavior is enrolled in the observer that can see its failure class; `dont` entries forbid orchestrator suites, mixed mutant populations, and substitutes whose expected value derives from the single implementation they replace. +- R9. CONST-T13 "Mutation Also Grades the Tests" is minted: a run whose mutants all died fails when an authored property file defends nothing the rest of the suite does not; opting out happens in the mutation config, never by deleting the named file. +- R10. CONST-T9 "Pin the Published Contract Before You Delete a Path" is minted: observables are pinned with an expected side independent of the implementation under change; a persisted gold survives a rewrite only when externally authored, independently gated, and cheap to re-bless. +- R11. CONST-T10 "The Oracle Is Not the System Under Test" is minted: every assertion draws on an oracle the SUT did not produce, and a hand-written refusal survives beside generated accept-laws at any specifiable refusal boundary. +- R12. CONST-T11 "Snapshots and Differentials Are Published-Surface Oracles" is minted: snapshot and differential fixtures are produced only through the package's published export map. +- R13. CONST-T12 "Altitude Is What the Test Calls" is minted: a test's observer is decided by what it imports and invokes, never by filename, folder, or suffix; the check is lint. +- R14. CONST-E5 "A Gate's Key Is Recomputed, Never Reported" is minted at the end of Article III's rules block (retrieved placement — see KTD3): gates key on recomputation from source bytes, a compiler verdict, or a rehash; a gate whose verdict the gated agent can produce or observe is unverified until an independent channel confirms it. + +**Resident law** + +- R15. CONST-S4's structural-rebuild bullet cites CONST-T9 instead of CONST-T5, and its `check` clause requires a CONST-T9 pin on every published path a rebuild deletes. No other S4 text changes. + +**Prose reconciliation** + +- R16. AGENTS.md's file-choice paragraph cites CONST-T14 as its review-gated artifact-announced testing example instead of CONST-T2; the "28 of 34 rules" count sentence is recomputed against the amended corpus (33 rules) and restated with measured numbers; its vacated-id reporting claim is now backed by the gate's real behavior. +- R17. README.md's Article III row describes the new doctrine without naming the Testing Trophy, and the "Rules: 34" badge is recomputed to 33. + +**Refusals** + +- R19. No rule YAML names a tool, a consumer repository, a language stack, or a filename suffix. +- R20. No Testing Trophy diagram, width metaphor, or layer table survives anywhere in the corpus. + +### Success Criteria + +- `pnpm test` exits 0 and its success line reports the post-amendment corpus (33 rules across both files). +- After the corpus commit, `pnpm test --against ` exits 0, prints no reassignment, and names exactly CONST-T1, CONST-T2, and CONST-T5 as vacated. +- Against a pre-#9 revision, the same gate names exactly CONST-E1, CONST-E2, CONST-E3, CONST-E4, CONST-G1, and CONST-G2 as vacated. +- A probe that adds a citation to a vacated id makes the gate fail naming the dangling citation; the probe is reverted after the demonstration. +- A grep sweep over the corpus, AGENTS.md, README.md, and `scripts/` finds no reference to CONST-T1, CONST-T2, CONST-T5, "Testing Trophy", or characterization-as-duty. + +### Scope Boundaries + +In scope: `CONSTITUTION-ARTICLES.md`, `CONSTITUTION.md`, `scripts/validate-constitution.py` (deleted), `scripts/validate-constitution.ts` (new), `deno.json` (new), `package.json` (test script), `AGENTS.md`, `README.md`, and this plan. + +### Deferred to Follow-Up Work + +- The org tree's `WATCHDOG.md` police-checks "T1 = trophy, T5 = pin with characterization" (measured in the invoking conversation); it enforces vacated obligations after this amendment and must be updated in that repo after its next vendor pull. Other consumer repos are unmeasured; vacancy failure at their pulls is loud by design. A tracker ticket for this follow-up is filed by the shipping pipeline. +- Restructuring AGENTS.md's measured-count prose so future amendments drift less is a separate docs decision. + +Outside this amendment's identity: validator schema, family registry, or gate registration changes (none needed — E and T are registered, `review`/`lint`/`mutation` are registered gates); renumbering to close id gaps (forbidden by the corpus's own rule); restoring the Application section or the Preamble that commits #9/#10 removed. + +--- + +## Planning Contract + +### Key Technical Decisions + +- KTD1. **CONST-T2 is vacated; its replacement is minted as CONST-T14 with a distinct title.** The narrowed grant forbids what the old `do` required — blanket property coverage of the core violates the new `dont` — so the obligation narrowed, and the corpus's id-surgery table assigns a narrowed obligation a new id. The `--against` reassignment check compares titles, and only for ids present at both revisions, so it is structurally blind to a body-narrowing under a kept id; id discipline for body-level obligation change is therefore review-only, which is exactly why the narrowed grant takes a fresh id and a fresh title ("Properties Where the Surface Cannot Reach") — no reader can mistake the new rule for a light edit of the old. AGENTS.md's prose citation of CONST-T2 re-points to CONST-T14 (R16), which is likewise review-gated and artifact-announced. +- KTD2. **CONST-T3 keeps its id and title.** The reword adds only loophole-closers (empty set, after-the-fact narrowing, raw-percentage comparison) and renames the gated set from "the core" to a named change-relevant set; nothing the old text required is now forbidden, so an agent compliant with the old text still complies. Same obligation, sharper instrument hygiene. +- KTD3. **CONST-E5 is retrieved law at the end of Article III, not resident law.** Commit ebd9b10 (#9) removed the resident Application section — CONST-G1/E1–E4/G2 — with the rationale that gate accumulation had produced cargo-culted checks; recreating a resident enforcement block for one rule reopens exactly that. The resident file is now conduct-only by direction of #9, and CONST-E5 is not conduct. It belongs where the work announces it: an agent authoring or judging a gate holds a verification artifact, which is Article III's retrieval trigger. Family follows about-ness, not placement — the corpus's own precedent is CONST-P3 (purity) living in Article II. E5 is the next free E number because E1–E4, though deleted in #9, are permanently vacant. Grounding: this repo's vacuous-pass learning ("never key a gate on a value its own author supplies") and dossier H12 (enforced gates bind, advisory surfacing is inert; an instrument correlated with the work can manufacture a false verdict). The rule stops at accountability — a recomputed key plus an independent confirmation channel — because binding by a machine gate an agent merely observes is unmeasured (dossier open question Q6). +- KTD4. **Numbering follows the settled mint set; the bundle is one doctrine revision.** CONST-T8 through CONST-T13 per the specification; CONST-T14 for the property grant; CONST-E5 in the E family. T6 and T7 remain vacant alongside T1, T2, and T5; gaps are free and a citation into a gap fails loudly. The eight mints ship as one amendment deliberately: the rules are mutually dependent — T9's pinning assumes T10's oracle independence, T11's snapshots and T12's altitude both key on T8's observer placement, and CONST-E5 disciplines the gates the T-family names — so splitting them would mint rules that cite rules that do not exist yet. +- KTD5. **Four commits, pushed together as one PR: plan checkpoint, validator, corpus, prose.** The Deno gate lands before the corpus commit so the amendment it judges is verified by a gate that already reports vacancies; the corpus commit carries both constitution files; prose lands last. The intermediate states exist only on the local branch for minutes — CI and consumer repos see only the PR head. The corpus/prose divergence window never publishes, and each layer stays auditable on its own. +- KTD6. **The gate is Deno, not Python.** Ecosystem tooling law is Deno-only for code, and the user directed it for this run. The rewrite is a port, not a redesign: every check the Python gate made is preserved, and the one addition is the vacated-id/uncompared-file reporting the repo's documentation already specifies (AGENTS.md's gate paragraph; the vacuous-pass learning's success-line shape). The `@std/yaml` parser is declared in `deno.json` and imported by bare name; the shebang carries the least-privilege flags so callers invoke the script directly. + +### Id surgery map + +| Action | Id | Title | +|---|---|---| +| Vacate | CONST-T1 | The Testing Trophy | +| Vacate | CONST-T2 | Properties Over Examples (blanket mandate) | +| Vacate | CONST-T5 | Pin Behavior Before You Rebuild | +| Keep id, reword body | CONST-T3 | Mutation Is the Measure | +| Keep, byte-identical | CONST-T4 | Behavior Lives Where the Mutator Sees It | +| Mint | CONST-T8 | The Observer Must See the Fault Class | +| Mint | CONST-T9 | Pin the Published Contract Before You Delete a Path | +| Mint | CONST-T10 | The Oracle Is Not the System Under Test | +| Mint | CONST-T11 | Snapshots and Differentials Are Published-Surface Oracles | +| Mint | CONST-T12 | Altitude Is What the Test Calls | +| Mint | CONST-T13 | Mutation Also Grades the Tests | +| Mint | CONST-T14 | Properties Where the Surface Cannot Reach | +| Mint | CONST-E5 | A Gate's Key Is Recomputed, Never Reported | + +Base: 28 rules (CONSTITUTION.md 7 + CONSTITUTION-ARTICLES.md 21). Post-amendment: 28 − 3 + 8 = 33. + +### Assumptions + +- The org tree's enforcing mechanisms are taken as specified in the invoking conversation; they were measured in that repo and are not re-verified from here. +- The dossier's adjudications are taken as the doctrine authority as delivered; primary-source re-derivation is out of scope for this amendment. +- CONST-T2's vacancy (KTD1) is resolved by the corpus's own id-surgery table after the question was declined in dialogue; the user redirected nothing when authorizing autonomous shipping. +- The two inference-grade `harm` fields (the empty-set guard's catching behavior, machine-gate binding on an observing agent) are stated as rationale, not measured findings, and the rules that carry them are framed as guards and accountability requirements. +- The locked-surface guidance for verification scripts is overridden for the gate file itself by direct user direction (Python out, Deno in); the override is declared in the validator commit body. + +### Risks & Dependencies + +- Consumer repos citing CONST-T1, CONST-T2, or CONST-T5 fail loudly at their next vendor pull. That is the intended vacancy behavior — a citation into a gap resolves to nothing, which the gate surfaces by name. One consumer is known to cite them (the org tree's `WATCHDOG.md`, per the invoking conversation); its repair is the deferred follow-up, not a shim here. +- AGENTS.md's measured-count prose will drift again on the next amendment. Accepted; the amendment checklist already requires recomputation on delete/split/merge/re-scope commits. +- The first `pnpm test` run after the Deno rewrite fetches `@std/yaml` from JSR (as the old command fetched PyPI); subsequent runs are cached. + +### System-Wide Impact + +Every consumer repo's agents receive the amended doctrine at their next vendor pull; the constitution repo's own harness (AGENTS.md guidance, README's doctrine table and badge) changes with it. The gate moves from Python to Deno — consumer repos that invoke the vendored validator directly inherit a Deno dependency; those using `pnpm test` inherit it transparently. + +--- + +## Implementation Units + +### U5. Deno corpus gate + +**Goal:** The corpus gate is a Deno script implementing the full documented behavior, including vacated-id and uncompared-file reporting; the Python file is gone. + +**Requirements:** R1 + +**Dependencies:** none (executes first) + +**Files:** `scripts/validate-constitution.ts` (new), `scripts/validate-constitution.py` (deleted), `deno.json` (new), `package.json` (test script) + +**Approach:** + +1. Read the Python gate in full and port every check: two-file coverage, fenced-block extraction and per-block parse errors, declared-vs-parsed id accounting, schema fields, gate values, family registry, duplicate ids, `do`/`dont` string-or-string-list shape, `example` map-of-strings shape, cross-file dangling citations, `--against` title-keyed reassignment. +2. Add the success-line reporting: ids vacated since the revision, and corpus paths absent at it — neither fails the run; both are named. +3. Declare `@std/yaml` in `deno.json` imports; point `package.json`'s `test` script at the script path; carry permissions in the shebang (`--allow-read` limited to the two corpus files, `--allow-run=git`); mark the file executable. + +**Patterns to follow:** the reporting shape in the vacuous-pass learning ("what cannot be failed must be reported"); the Python gate's error-message voice. + +**Test scenarios:** + +- Red-first: before the rewrite, `pnpm test --against ebd9b10~1` (captured) prints only `no id reassigned` — six vacated ids unreported. +- After the rewrite, against `ebd9b10~1`: names exactly CONST-E1, CONST-E2, CONST-E3, CONST-E4, CONST-G1, CONST-G2 as vacated; exit 0. +- Against the pre-split revision `28c79e6~1`: names `CONSTITUTION-ARTICLES.md` as not compared; exit 0. +- Against the current HEAD: names nothing vacated; exit 0. +- Without `--against`: output shape unchanged from the Python gate's, exit 0. +- Dangling-citation fixture: a temporary citation to a vacated id fails the run naming it. + +**Verification:** the six scenarios pass; `deno lint` on the new script is clean. + +### U1. Article III surgery + +**Goal:** Article III states observer-fit, oracle-independence, mutation, pinning, and gate-key doctrine; the trophy and the characterization duty are gone. + +**Requirements:** R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12, R13, R14, R19, R20 + +**Dependencies:** none on U5 (independent content; ordering matters only for U4's evidence) + +**Files:** `CONSTITUTION-ARTICLES.md` + +**Approach:** + +1. Read the current Article III block before editing. +2. Replace the rules block with the Appendix A block verbatim: CONST-T8, CONST-T14, CONST-T3, CONST-T13, CONST-T4, CONST-T9, CONST-T10, CONST-T11, CONST-T12, then CONST-E5 as the terminal rule. CONST-T4's entry stays byte-identical to the current text. +3. Article III's heading and the surrounding article structure do not change. + +**Patterns to follow:** the existing rule YAML schema (`id`, `title`, `gate`, `do`, `dont`, `harm`, `check`); gate values limited to the registered set. + +**Test scenarios:** (all run at the corpus-commit boundary, after U2's S4 repoint — the dangling T5 citation in CONSTITUTION.md keeps the gate red until then) + +- Validator schema: `pnpm test` exits 0; the success line reports 33 rules. +- Dangling-citation probe: temporarily add a citation to CONST-T5 in a corpus file; the gate fails naming the dangling citation; revert the probe and confirm green. +- Stale-doctrine sweep: a grep over the corpus for `CONST-T1`, `CONST-T2`, `CONST-T5`, `Testing Trophy`, and `characterization` returns nothing. + +**Verification:** the three scenarios pass at the corpus-commit boundary. + +### U2. S4 repoint + +**Goal:** CONST-S4 pins structural rebuilds with CONST-T9 instead of the vacated CONST-T5. + +**Requirements:** R15 + +**Dependencies:** U1 (the citation target must exist) + +**Files:** `CONSTITUTION.md` + +**Approach:** + +1. Read the current CONST-S4 block before editing and confirm both find-patterns are present verbatim. +2. In the structural-rebuild bullet, replace "behaviour pinned with characterization tests first, CONST-T5" with "published contract pinned first (CONST-T9)". +3. In the `check` clause, replace "a structural rebuild ships its characterization tests" with "a structural rebuild ships a CONST-T9 pin on every published path it deletes". +4. If either find-pattern misses, stop for amendment repair — do not approximate the edit. + +**Patterns to follow:** the resident Article V rule shape; no other S4 field changes. + +**Test scenarios:** + +- A grep of `CONSTITUTION.md` for CONST-T5 returns nothing; a grep for CONST-T9 returns the S4 citation. +- `pnpm test` exits 0 at the corpus-commit boundary (with U1). + +**Verification:** both scenarios pass. + +### U3. Harness prose reconciliation + +**Goal:** The harness and front-page prose teach the amended doctrine, cite only live rules, and carry measured numbers. + +**Requirements:** R16, R17 + +**Dependencies:** U1, U2, U5 (prose must match final rule text, ids, and gate behavior) + +**Files:** `AGENTS.md`, `README.md` + +**Approach:** + +1. In AGENTS.md's file-choice paragraph, replace the CONST-T2 example citation with CONST-T14. +2. In AGENTS.md's gate-explanation paragraph, recompute the inbound-citation count and the corpus total (33) against the amended files and restate the sentence with the measured numbers; its vacated-reporting claim now names real behavior. +3. In README.md's doctrine table, rewrite the Article III row to describe observer-fit placement, properties by narrow grant, and mutation as the measure; recompute the "Rules" badge from 34 to 33. + +**Patterns to follow:** AGENTS.md's existing measured-claim style; README.md's table-row and badge shapes. + +**Test scenarios:** + +- Repo-wide grep sweep (excluding `.git`, lock files, and this plan) for CONST-T1, CONST-T2, CONST-T5, "Testing Trophy", and characterization-as-duty returns nothing. +- The recomputed AGENTS.md and README numbers match fresh counts run at implementation time, not numbers carried from this plan. + +**Verification:** both scenarios pass. + +### U4. Verification battery and commits + +**Goal:** Evidence is recorded and the work lands as four conventional commits. + +**Requirements:** all + +**Dependencies:** U5, U1, U2, U3 + +**Files:** none new + +**Approach:** + +1. Run the full Verification Contract in order. +2. Commit the plan checkpoint (`docs(plans):`) first; then the Deno gate (`feat(scripts)!:`) with the locked-surface override declared in the body; then the corpus change (U1 + U2, `refactor(constitution)!:`); then the prose (U3, `docs:`). +3. Run commitlint over all four commits. + +**Test scenarios:** + +- All four commits pass `pnpm exec commitlint --from HEAD~4`. +- The working tree is clean after the commits. + +**Verification:** the Verification Contract's exit criteria all hold. + +--- + +## Verification Contract + +| Check | Command / method | Exit criterion | +|---|---|---| +| Corpus gate | `pnpm test` | Exit 0; success line reports 33 rules across both corpus files | +| Vacancy gate | `pnpm test --against ` | Exit 0; no reassignment; names exactly CONST-T1, CONST-T2, CONST-T5 vacated | +| Backfill fixture | `pnpm test --against ebd9b10~1` | Exit 0; names exactly the six #9-removed ids vacated | +| Uncompared fixture | `pnpm test --against 28c79e6~1` | Exit 0; names `CONSTITUTION-ARTICLES.md` not compared | +| Dangling-citation probe | temporary citation to a vacated id, then `pnpm test` | Fails naming the citation; probe reverted; gate green again | +| Stale-doctrine sweep | grep corpus, AGENTS.md, README.md, `scripts/` for vacated ids and retired doctrine names | Zero matches | +| Commit format | `pnpm exec commitlint --from HEAD~4` | Exit 0 for all four commits | +| Clean state | `git status --porcelain` | Empty after commits | + +--- + +## Definition of Done + +- All requirements R1–R20 hold in the tree. +- The Verification Contract passes in full, in the current session. +- The four commits (plan, validator, corpus, prose) are separate, conventional, and pushed together in one PR. +- No consumer-repo file is touched; the org-tree watchdog follow-up remains deferred with its tracker ticket filed. +- Working tree is clean. + +--- + +## Appendix A — Article III replacement block + +Normative payload for U1. YAML block content, replacing the current Article III `rules:` sequence: + +```yaml +rules: + - id: CONST-T8 + title: The Observer Must See the Fault Class + gate: review + do: enroll each behavior in the observer that can see its failure class — a pure decision is read by properties and by mutation of that decision; a published operation is read by tests that call only exported names; a shell that only translates is read by those exported-name tests, not by a suite of its own + dont: + - give the orchestrator its own test suite — extract the decision or live with the exported-name test + - enroll adapters, codecs, or wiring in the same mutant set as decisions + - treat a substitute whose expected value derives from the single implementation it replaces as an observer + harm: the wrong instrument reports coverage and measures nothing; mocked composition certifies wiring; a fat middle grows where decisions and I/O stay tangled + check: review — every new test names the surface it binds (decision or published export) and does not import a non-exported symbol to assert it + - id: CONST-T14 + title: Properties Where the Surface Cannot Reach + gate: review + do: prove a pure decision with a property when a universal over generated input, or a refusal no generated law can express, cannot be reached from the published surface; the type is the generator + dont: + - cover the core with hand-picked example unit tests + - write a property for a decision already fully pinned from above just because the decision is important + harm: a green suite that tests only the cases you imagined; or a property farm that restates the public contract and dies with it + check: review — each authored property names the universal the public surface cannot reach + - id: CONST-T3 + title: Mutation Is the Measure + gate: mutation + do: gate a named, change-relevant mutated set at a perfect kill score; the set names the behavior it covers, and its scope is a cost decision, never a fault-majority claim; kill a survivor with a sharper property or by deleting the dead branch it exploits + dont: + - reach the number by a suppression comment + - reach the number by narrowing the mutated set after the fact + - reach the number by lowering the gate + - let an empty mutated set pass + - treat a raw mutation percentage as comparable across changes or codebases + harm: a score certifying tests that notice nothing; an empty or author-shrunk set passing vacuously + check: mutation gate (break = 100) on the declared mutated set; lint banning suppression, scope-narrowing, and an empty set + - id: CONST-T13 + title: Mutation Also Grades the Tests + gate: mutation + do: fail a run whose mutants all died if an authored property file defends nothing the rest of the suite does not; opt out in the mutation config, never by deleting the file the gate named + dont: + - treat a perfect mutant score as proof every test pulled its weight + - accuse a file that covered an unattributed kill + harm: toothless properties accumulate; deleting them to silence the gate removes the only named contract + check: mutation — the test-set verdict is part of the same run as the score + - id: CONST-T4 + title: Behavior Lives Where the Mutator Sees It + gate: lint + do: put any code that can be wrong (transform, check, branch) in a file the mutator covers + dont: place behavior in a declaration file (types, schemas, constant data), excluded from mutation + harm: a bug hidden behind a perfect score, in a file nothing mutates + check: lint — declaration files contain no behavior + - id: CONST-T9 + title: Pin the Published Contract Before You Delete a Path + gate: review + do: before removing or replacing a published operation, pin its observables (value, error variant, serialized document, process result) with examples or properties whose expected side is not the implementation under change; if the old operation still runs, compare old and new on the same published inputs until they agree, then delete old + dont: + - pin private functions + - derive expected values by running the implementation under change + - treat a mutation or property score as proof a deleted published capability still exists — those are blind to absence + - leave a persisted gold after the old path is gone unless the gold is externally authored, independently gated, and cheap to re-bless + harm: a rebuild silently drops a capability; same-session gold blesses the bug; a clean score after a delete is a silent regression + check: review — pins call only published names; each expected value names an independent source (spec clause, prior published major, second implementation, or a hand-written oracle next to the constructor) + - id: CONST-T10 + title: The Oracle Is Not the System Under Test + gate: review + do: every assertion has an oracle the SUT did not produce — a spec literal, a fixture not generated by importing the module, a law relating two views of the same value, or a second implementation; generated round-trip laws on a type cover what the type accepts and nothing it should reject, so a hand-written refusal survives beside them at any specifiable refusal boundary + dont: + - compute expected by calling the SUT + - assert collaborator call graphs + - treat generated accept-laws as full coverage of a refinement + harm: a green suite that cannot fail when the behavior is wrong; widening a refinement leaves generated laws green + check: review — plus sabotage: after green, break one core law and one published field; at least one test must go red + - id: CONST-T11 + title: Snapshots and Differentials Are Published-Surface Oracles + gate: lint + do: snapshot only canonicalized published output; compare two implementations only of the same published operation (or a prior published major against current) + dont: + - snapshot or compare private helpers, mappers, or unexported modules + - snapshot a value small enough to be a property or a named example + harm: tests that fail on refactors callers cannot see and pass on contract breaks they can + check: lint — snapshot and differential fixtures are produced only through the package's published export map + - id: CONST-T12 + title: Altitude Is What the Test Calls + gate: lint + do: decide a test's observer from what it imports and invokes — published names or a decision under mutation — never from a filename, folder, or suffix + dont: key which doctrine applies to a test on a label the author can rename + harm: a rename silently un-enrolls the file from its observer; the absence reads as coverage + check: lint — no rule that selects tests by filename suffix + - id: CONST-E5 + title: A Gate's Key Is Recomputed, Never Reported + gate: review + do: + - key every gate on a recomputation from source bytes, a compiler verdict, or a rehash — never on a field the gated work's author supplied; when a gate reads a field, recompute that field in the same run + - treat a gate whose verdict the gated agent can produce or observe as unverified until an independent channel confirms it — an instrument the agent does not control, or review by someone who is not the gated agent + dont: + - accept a self-reported field, a presence flag, a metadata suffix, or a comment as evidence a property holds + - treat a mechanical gate's green as self-certifying + harm: a check keyed on author-supplied values passes everything and catches nothing, and the green then masks the broken invariant the gate exists to catch; an instrument correlated with the work under test can manufacture a verdict no single observer catches + check: review — each gate names the recomputation it runs and the independent channel that confirms its verdict +``` + +--- + +## Appendix B — CONST-S4 edits + +Normative payload for U2, verified verbatim against the current corpus. Two edits, complete and final: + +- Structural-rebuild bullet: "behaviour pinned with characterization tests first, CONST-T5" becomes "published contract pinned first (CONST-T9)". +- Check clause: "a structural rebuild ships its characterization tests" becomes "a structural rebuild ships a CONST-T9 pin on every published path it deletes". + +--- + +## Sources & Research + +- H1–H12 verdict dossier (delivered in the invoking conversation): doctrine authority. Load-bearing verdicts: H1 (same-session contamination), H2 (oracle independence over surface count), H5 (change-relevant mutation; decision-only refuted; empty-set guard load-bearing), H7 (refusal complement), H8 (seam authority), H11 (external-gated gold), H12 (gates bind; accountability reading). Independently spot-confirmed against primary literature (arXiv:2607.05139, arXiv:2410.21136, EMSE'22 commit-relevant mutants). +- Org-mechanism specification (delivered in the invoking conversation): the enforcing tree's mechanisms, the mint set CONST-T8 through CONST-T13, and the measured consumer citation (org tree WATCHDOG.md). +- Python gate `scripts/validate-constitution.py`, read in full: the port source — schema fields, family registry, cross-file citation resolution, title-keyed `--against` reassignment over ids present at both revisions. +- AGENTS.md: id-surgery table (obligation identity), family registry, file-choice rule, the gate-explanation paragraph whose counts R16 recomputes and whose vacated-reporting claim the Deno gate makes true. +- `docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md`: grounding for CONST-E5 and the `--against` reporting shape — a gate keyed on author-supplied values certifies nothing; what cannot be failed must be reported. +- Tooling: Deno-only ecosystem law (software-wiki harness convention); `@std/yaml` (`jsr:@std/yaml`, single-document `parse`, safe for untrusted input — docs.deno.com/runtime/reference/std/yaml). +- Corpus base: `origin/main` at dd5722a (28 rules; commits ebd9b10/#9 and dd5722a/#10 removed the Application section and the Preamble). diff --git a/repos/constitution/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md b/repos/constitution/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md new file mode 100644 index 0000000..287e44d --- /dev/null +++ b/repos/constitution/docs/plans/2026-08-30-0100-feat-omp-constitution-ttsr-plugin-plan.md @@ -0,0 +1,49 @@ +--- +title: "feat(plugin): create OMP constitution TTSR rules plugin" +date: 2026-08-30 +category: feature +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +execution: code +product_contract_source: ce-plan +--- + +# Goal Capsule + +Add a standard OMP plugin package (`@systemfsoftware/omp-plugin-constitution` under `packages/omp-plugin/` or `plugins/constitution/`) that defines Time-Traveling Stream Rules (TTSR) in markdown (`rules/*.md`). The rules trigger on edits/writes to source code and tests, enforcing strict adherence to the Supreme Engineering Constitution (`CONSTITUTION.md` and `CONSTITUTION-ARTICLES.md` Articles I–V). Any violation in review or source generation is flagged immediately with automatic P0 severity and actionable remediation. + +# Product Contract + +## 1. Scope and Objective +- Package an OMP plugin adhering to Claude/OMP plugin discovery specifications (`.omp-plugin/plugin.json`, `rules/`, `package.json`, `README.md`). +- Define targeted TTSR stream rules (`rules/*.md`) covering: + 1. `constitution-pure-core.md`: Enforces Article I (purity, tagged error variants, branded types, cyclomatic complexity 1, no primitive obsession, no boolean/status string errors, no null states). + 2. `constitution-boundary.md`: Enforces Article II (functional core / imperative shell, effects as lazy values, I/O sandwich order, inward dependencies, decode never unchecked cast `as any`/`as unknown`). + 3. `constitution-verification.md`: Enforces Article III (public export API testing, mutation testing on decisions, narrow property grants, independent oracles, no mocked intermediate single-implementation glue code, no tautological/characterization tests). + 4. `constitution-conduct-and-review.md`: Enforces Article V & Governance (automatic P0 on constitutional breaches with zero appeals unless declared under CONST-W3, root cause over expedient patch, subtract before add, scope discipline). +- Each rule carries frontmatter with `condition`, `scope` (`tool:edit(...)`, `tool:write(...)`, `tool:ast_edit(...)`), and `interruptMode: tool-only` or `always`. +- Update project manifest (`package.json`) and root test validation to ensure all rules are valid YAML frontmatter and tested against `omp ttsr test` (or regex validator). + +## 2. Requirements & Invariants +- **R1 (Plugin Discovery)**: Plugin manifest `.omp-plugin/plugin.json` and `.claude-plugin/plugin.json` properly identify the plugin name, version, and author metadata. +- **R2 (TTSR Rule Format)**: Every rule file in `rules/` has valid YAML frontmatter with `description`, `condition`, `scope`, and markdown instructions quoting the exact rule IDs (`CONST-P1`, `CONST-D1`, `CONST-B1`, `CONST-T8`, `CONST-G3`, etc.). +- **R3 (Zero Hallucination / Grounded)**: Rule text cites exact IDs and principles from the repository's `CONSTITUTION.md` and `CONSTITUTION-ARTICLES.md`. +- **R4 (Verification)**: `pnpm test` (and any new plugin test scripts) must execute cleanly, validating all YAML blocks across the repository. + +# Implementation Units + +- [ ] **U1: Scaffold OMP Plugin Manifest & Directory Structure** + - **Goal**: Create the plugin layout under `plugins/constitution/` or root plugin metadata with `.omp-plugin/plugin.json`, `.claude-plugin/plugin.json`, and `package.json`. + - **Files**: `plugins/constitution/.omp-plugin/plugin.json`, `plugins/constitution/.claude-plugin/plugin.json`, `plugins/constitution/package.json`, `plugins/constitution/README.md`. + +- [ ] **U2: Implement Article I & II TTSR Rules (Core & Boundary)** + - **Goal**: Author `rules/constitution-pure-core.md` and `rules/constitution-boundary.md` with precise regex and ast conditions for unchecked casts (`as any`, `as unknown`), eager async promises on domain signatures, mutable control flow in pure core, and missing error tagging. + - **Files**: `plugins/constitution/rules/constitution-pure-core.md`, `plugins/constitution/rules/constitution-boundary.md`. + +- [ ] **U3: Implement Article III & V TTSR Rules (Verification & Review P0)** + - **Goal**: Author `rules/constitution-verification.md` and `rules/constitution-conduct-review.md` covering mocked single implementations, tautological assertions, private helper unit tests, and the mandatory P0 automatic review rejection. + - **Files**: `plugins/constitution/rules/constitution-verification.md`, `plugins/constitution/rules/constitution-conduct-review.md`. + +- [ ] **U4: Wire Validation, Test Battery, and Documentation** + - **Goal**: Update root `scripts/validate-constitution.ts` (or add plugin test scripts) to validate the plugin rules, update `README.md` and `AGENTS.md` to document the OMP plugin installation and usage. + - **Files**: `scripts/validate-constitution.ts`, `README.md`, `AGENTS.md`, `package.json`. diff --git a/repos/constitution/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md b/repos/constitution/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md new file mode 100644 index 0000000..9796243 --- /dev/null +++ b/repos/constitution/docs/solutions/architecture-patterns/the-vacuous-pass-gate-input-sets.md @@ -0,0 +1,152 @@ +--- +title: A gate can go green because it stopped looking +date: 2026-08-18 +category: architecture-patterns +module: constitution corpus validator +problem_type: architecture_pattern +component: tooling +severity: high +applies_when: + - A gate's input set is named in the gate rather than derived from the tree + - A single-artifact check is widened to cover several artifacts + - A merge-blocking check reports a count it computed itself +tags: [gate-design, vacuous-pass, verification, fail-closed, known-bad-fixture] +--- + +# A gate can go green because it stopped looking + +## Context + +The constitution's format gate validated one markdown file whose path was a module-level +constant. The law was then split across two files. The gate's logic was untouched and +entirely correct — it parsed, it compared declared rule ids against parsed rule ids, it +found no discrepancy, it exited 0. It was reporting on a third of the rules. + +Nothing in the output distinguished that from a healthy corpus. The same run over the +whole corpus and the run over a third of it print the same shape, differing only in a +number nobody had a baseline for. + +Two further shapes of the same defect surfaced during review of the fix, both after the +input set had been widened to a tuple of paths: + +- A path present in the tuple but **absent from disk** was caught by a hard failure. A + path present, parsing cleanly, and declaring **zero rules** was not — emptying the + resident half to a preamble plus two `placeholder: true` blocks printed a valid line + with a smaller count and exited 0. +- The cross-revision arm tolerated a path that did not exist at the older revision, + because a newly created file legitimately has nothing to compare. That tolerance also + swallowed a path **renamed** in the same commit that re-scoped a rule: the older + revision had no such path, so the rule's retitle went uncompared and the run reported + no reassignment. + +## Guidance + +**Assert the corpus, not only the contents.** A check over a subset is +indistinguishable from a check over the whole unless the gate says which inputs it +measured. Three rules follow. + +1. **A missing input is a hard failure, never a smaller pass.** Absence of an expected + input is a defect in the gate's own configuration, and it must exit non-zero rather + than validate what remains. + +2. **Presence is not contribution.** An input that resolves, parses, and yields nothing + scores exactly like one that yields its half. Require every declared input to + contribute at least one unit of the thing being validated. This is a recomputation + from the bytes — *did this file produce a rule?* — not a number the author supplies. + +3. **What cannot be failed must be reported.** Some gaps are legal and failing them + would fire on correct work: a deliberate deletion, a genuinely new input with no + history. Name them on the success line — which inputs were not compared, which + identifiers vacated — so the reader sees the reduced coverage instead of inferring + full coverage from a green exit. + +**Never key a gate on a value its own author supplies.** The tempting fix for shape 1 is +a pinned expected count. That is a field the author writes, so the gate never runs on the +case it exists to catch. Prefer a key the gate recomputes: a digest over current bytes, a +compiler verdict, a re-derivation from the tree. + +**Prove the widening with a known-bad fixture.** A gate that has only ever been run +against a healthy input has demonstrated that it can print a success line. Run it against +an input that must fail, and read the failure text. Each defect above was found by a +fixture, not by reasoning about the code. + +## Why This Matters + +The failure is silent by construction and lands in the one place nobody reads twice: +output that already says everything is fine. Downstream, the green result is then cited as +evidence the invariant holds — so the gate does not merely fail to catch the defect, it +actively certifies its absence. + +The documentation compounds the harm. Prose asserting a guarantee the gate does not +implement ("a rule dropped in a move fails the gate") trains a maintainer to skip the +manual review the gate is not doing. Two independent reviewers caught that sentence here; +it had been written in the same change that widened the gate, by the author most convinced +the gate was now sound. + +## When to Apply + +- Widening any check from one artifact to several — the moment the input set becomes a + collection, it becomes a thing that can silently shrink. +- Any gate that prints a count it computed itself, with no baseline the reader can check. +- Any check whose failure path is reachable only by a state the repository never normally + reaches; that path has almost certainly never run. +- Reviewing a claim about what a gate enforces. Run the fixture; do not read the code and + agree with it. + +## Examples + +Before — a single named input. Correct, and silently correct about a third of the corpus +once the corpus grew: + +```python +PATH = "ONE_FILE.md" +text = open(PATH, encoding="utf-8").read() +# ... one file's worth of checking, exit 0 +``` + +After — the input set is a collection, absence is fatal, and emptiness is fatal: + +```python +PATHS = ("RESIDENT.md", "RETRIEVED.md") + +for p in PATHS: + try: + texts[p] = open(p, encoding="utf-8").read() + except FileNotFoundError: + fail([f"{p}: missing — half a corpus scores exactly like a whole one"]) + +# ... after parsing, every declared input must have produced something +for p in PATHS: + if p not in contributors: + errors.append(f"{p}: parses but declares no rule") +``` + +And what cannot be failed is stated rather than omitted: + +``` +valid: 34 rules across 6 yaml blocks in 2 files, 9 families; + no id reassigned since ; + not compared, absent at : RETRIEVED.md; + 1 id(s) vacated since : CONST-N3 +``` + +The fixture battery that found all of it — each must fail, and the failure text is the +artifact worth keeping: + +| Fixture | Required result | +|---|---| +| one corpus path absent from disk | fail, naming the path | +| one corpus path present but declaring nothing | fail, naming the path | +| an identifier duplicated across two paths | fail on the duplicate | +| a rule deleted while a citation to it survives | fail on the dangling citation | +| a malformed fence in the second path | fail, naming that path and a path-local block index | +| a rule deleted with nothing citing it | pass, and name the vacated identifier | +| a path renamed alongside a rule retitle | pass, and name the uncompared path | + +## Related + +- The gate discussed here is the constitution corpus validator invoked by the repository's + `test` script; its module docstring carries the same argument at the point of use. +- `CONST-E1` (Prefer the Gate) and `CONST-E3` (A Gate Earns Its Place) are the rules that + make a gate the final word and price its false-positive budget; this learning is the + counterweight — a gate that cannot fail is not enforcement, it is a certificate. diff --git a/repos/constitution/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md b/repos/constitution/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md new file mode 100644 index 0000000..c8c4acc --- /dev/null +++ b/repos/constitution/docs/solutions/architecture-patterns/ttsr-regex-rules-flag-translation-backreferences.md @@ -0,0 +1,31 @@ +--- +title: TTSR regex rules require explicit flag translation and generic backreferences +date: 2026-08-30 +category: architecture-patterns +module: omp-plugin-constitution +problem_type: architecture_pattern +component: tooling +severity: medium +applies_when: + - Authoring Time-Traveling Stream Rules (TTSR) markdown files with condition regexes + - Writing validators for stream rules with PCRE inline flags like `(?i)` or `(?ims)` + - Matching self-referential or tautological anti-patterns across tool streams +root_cause: tool_interface +tags: [ttsr, regex, omp, stream-rules, flag-translation, backreference] +--- + +# TTSR regex rules require explicit flag translation and generic backreferences + +## Context + +When creating an OMP plugin providing Time-Traveling Stream Rules (TTSR), rules are authored as Markdown documents with YAML frontmatter containing `condition` regex patterns and `scope` tool selectors. + +During implementation and review of the constitution plugin, two critical regex compilation and matching traps emerged: +1. **PCRE Inline Flags:** Authors frequently write `(?i)` or `(?ims)` in condition strings. Standard JavaScript / V8 `RegExp` engines reject leading `(?i)` group syntax as invalid groups (`SyntaxError: Invalid group`). The OMP capability layer translates leading PCRE flag groups (`/^\(\?([ims]+)\)/`) into native `RegExp` flags, but a custom validator that passes raw strings to `new RegExp(pattern)` fails valid rules, while failing to reject unsupported inline combinations (`(?imsu)` or mid-pattern flags). +2. **Literal vs Generic Anti-Pattern Matching:** Condition patterns targeting self-referential statements (e.g., asserting `expect(fn()).toBe(fn())`) fail completely when hardcoded to literal identifiers. To detect tautologies generically across all codebases without false negatives, condition regexes must use capturing groups and backreferences (`expect\(\s*(\w+)\s*\([^)]*\)\s*\)\.to(?:Be|Equal)\(\s*\1\s*\([^)]*\)\s*\)`). + +## Guidance + +- When validating TTSR condition regexes, implement the exact OMP inline flag translation (`/^\(\?([ims]+)\)/`) and reject unsupported flags explicitly before compiling. +- For anti-pattern rules that detect identical calls or operands on both sides of an operator, use regex backreferences (`\1`) rather than example identifiers. +- Validate the expected rule set strictly by exact filename or ID rather than loosely testing directory file counts. diff --git a/repos/constitution/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md b/repos/constitution/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md new file mode 100644 index 0000000..4ebe0db --- /dev/null +++ b/repos/constitution/docs/solutions/workflow-issues/plans-freeze-observations-about-a-moving-base.md @@ -0,0 +1,34 @@ +--- +title: Plans freeze observations about a moving base +date: 2026-08-30 +category: workflow-issues +module: constitution amendment workflow +problem_type: workflow_issue +component: development_workflow +severity: medium +applies_when: + - A plan names a base revision and its verification rows carry expectations measured at research time + - Commits land on the branch between plan freeze and the first implementation unit + - A fixture expectation in a plan contradicts what the gate prints at execution time +root_cause: stale planning baseline +tags: [plan-freeze, base-drift, fixtures, measured-expectations, review-findings] +--- + +# Plans freeze observations about a moving base + +## Context + +During the Article III verification amendment, the plan was researched and frozen against base `dd5722a`. Six commits landed on the branch before the implementation units started. By execution time, plan rows that named the pre-split tree ("exactly six vacated ids at this revision", "the uncompared-file clause fires at revision X") described a tree that no longer existed: the corpus split and corpus-commit had moved the vacated-id set to nine and moved the uncompared-file boundary to an earlier revision. The multi-agent review wave flagged the unsatisfiable rows; execution had already recorded the honest outputs by running the gate and keeping what it printed. + +The defect is not that the plan was wrong when written — it was measured against the base it named. The defect is that nothing re-derived the expectations when the base moved, and the plan's freeze rule (byte-identical after start) made correcting the rows in place the wrong move. + +## Guidance + +- At the first implementation unit, re-run every plan verification command against the current head. Record divergent outputs in the run record as measured facts; do not edit frozen plan rows to match. +- Derive fixture expectations at execution time from the gate's actual output. A number carried from research time is a hypothesis about the terminal; the run's own execution is the terminal. +- When a plan row and a measured output disagree, trust the measurement, note the divergence in the run record, and let the review (not the plan edit) carry the correction. Two unsatisfiable rows in this session were caught exactly this way — by a reviewer executing the named revision and comparing against the plan's text. +- Reviewers verify plan claims by execution against named revisions, not by re-reading the plan. That stance is what surfaced the drift; keep it in every review brief. + +## Applicability + +This applies to any planned change whose plan encodes observations about tree state (counts, absence boundaries, report shapes) rather than only intent. A plan that names only invariants and lets execution derive numbers has no rows to go stale. Where the plan must carry concrete expectations (fixtures for a gate), scope each expectation to the revision it was measured at, so a moved base invalidates the row loudly instead of silently. diff --git a/repos/constitution/plugins/constitution/.claude-plugin/plugin.json b/repos/constitution/plugins/constitution/.claude-plugin/plugin.json new file mode 100644 index 0000000..aa00a23 --- /dev/null +++ b/repos/constitution/plugins/constitution/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "@systemfsoftware/omp-plugin-constitution", + "version": "0.1.0", + "description": "OMP Plugin providing Time-Traveling Stream Rules (TTSR) enforcing strict adherence to the Supreme Engineering Constitution.", + "author": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "homepage": "https://github.com/systemfsoftware/constitution", + "repository": "https://github.com/systemfsoftware/constitution", + "license": "Apache-2.0", + "keywords": [ + "omp", + "plugin", + "constitution", + "rules", + "ttsr", + "architecture", + "clean-code" + ] +} diff --git a/repos/constitution/plugins/constitution/.omp-plugin/plugin.json b/repos/constitution/plugins/constitution/.omp-plugin/plugin.json new file mode 100644 index 0000000..aa00a23 --- /dev/null +++ b/repos/constitution/plugins/constitution/.omp-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "@systemfsoftware/omp-plugin-constitution", + "version": "0.1.0", + "description": "OMP Plugin providing Time-Traveling Stream Rules (TTSR) enforcing strict adherence to the Supreme Engineering Constitution.", + "author": { + "name": "systemfsoftware", + "url": "https://systemfsoftware.com" + }, + "homepage": "https://github.com/systemfsoftware/constitution", + "repository": "https://github.com/systemfsoftware/constitution", + "license": "Apache-2.0", + "keywords": [ + "omp", + "plugin", + "constitution", + "rules", + "ttsr", + "architecture", + "clean-code" + ] +} diff --git a/repos/constitution/plugins/constitution/README.md b/repos/constitution/plugins/constitution/README.md new file mode 100644 index 0000000..fbb7390 --- /dev/null +++ b/repos/constitution/plugins/constitution/README.md @@ -0,0 +1,29 @@ +# OMP Plugin — Supreme Engineering Constitution TTSR Rules + +Time-Traveling Stream Rules (TTSR) plugin for Oh My Pi (OMP) and Claude Code that enforces strict adherence to the [System F Software Engineering Constitution](https://github.com/systemfsoftware/constitution). + +## Overview + +This plugin installs active stream rules that intercept edits, writes, and reviews violating constitutional articles: +- **`rules/constitution-pure-core.md` (Article I):** Intercepts impure logic (exceptions, I/O in decisions), primitive obsession, procedural control flow (cyclomatic complexity > 1 in core), and unvalidated states. +- **`rules/constitution-boundary.md` (Article II):** Intercepts decisions in boundary adapters, eager async promises on public domain surfaces, unchecked type casts (`as any`), and interleaved I/O sandwiches. +- **`rules/constitution-verification.md` (Article III):** Intercepts mocks on single implementations, tautological/characterization tests, private helper unit tests, and dependent oracles. +- **`rules/constitution-conduct-review.md` (Article V & Governance):** Enforces mandatory **P0** automatic failure on all undeclared constitutional violations with zero appeals and zero waivers (`CONST-G3`). + +## Installation + +Install in OMP: + +```bash +omp plugin install @systemfsoftware/omp-plugin-constitution +``` + +Or link directly in local development: + +```bash +omp plugin link ./plugins/constitution +``` + +## License + +Apache-2.0 diff --git a/repos/constitution/plugins/constitution/rules/constitution-boundary.md b/repos/constitution/plugins/constitution/rules/constitution-boundary.md new file mode 100644 index 0000000..6ff81fb --- /dev/null +++ b/repos/constitution/plugins/constitution/rules/constitution-boundary.md @@ -0,0 +1,33 @@ +--- +description: "Fires on edits/writes that violate Article II (The Boundary) of the Constitution: decisions inside boundary handlers, eager async promises on public surfaces, unchecked type casts, or pass-through delegation layers." +condition: + - '(?\(\s*\(\s*(?:resolve|reject)' +scope: + - 'tool:edit(**/*.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:write(**/*.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:ast_edit(**/*.{ts,tsx,js,jsx,rs,py,go})' +interruptMode: tool-only +--- + +# Constitutional Interruption: Article II — The Boundary Violation + +You are authoring code that violates **Article II (The Boundary)** of the Supreme Engineering Constitution (`CONSTITUTION-ARTICLES.md`): + +1. **Functional Core, Imperative Shell (`CONST-B1`):** Boundary handlers, adapters, and middleware translate external data ↔ domain types. + - **DO NOT** make domain decisions inside boundary objects. Boundary layers that require complex logic testing belong in the pure core. + +2. **Effects Are Lazy Values (`CONST-B2`):** Return side-effects as lazy descriptions (`Effect`, IO values), interpreted once at the application edge. + - **DO NOT** put eager async results (e.g., bare `Promise`) directly on public domain surfaces. + +3. **The I/O Sandwich (`CONST-B3` & `CONST-B6`):** Outside interactions follow `read` (impure) → `transform` (pure) → `write` (impure). + - **DO NOT** interleave I/O inside transformation logic. + - **DO NOT** add useless pass-through layers that only forward calls without transforming, reading, or writing. + +4. **Dependencies Point Inward (`CONST-B4`):** The shell imports the core, never vice versa. Wire all implementations at one composition root. + +5. **Decode, Never Cast (`CONST-B5`):** Validate incoming serialized or foreign data into branded domain types with a typed decoder. + - **DO NOT** use unchecked type casting (`as any`, `as unknown`, `as Type`) or suppression comments (`@ts-ignore`, `@ts-expect-error`) to bypass validation. + +Remediate the boundary violation before re-issuing this write. diff --git a/repos/constitution/plugins/constitution/rules/constitution-conduct-review.md b/repos/constitution/plugins/constitution/rules/constitution-conduct-review.md new file mode 100644 index 0000000..5e1663c --- /dev/null +++ b/repos/constitution/plugins/constitution/rules/constitution-conduct-review.md @@ -0,0 +1,37 @@ +--- +description: "Fires during review or implementation when constitutional rules are treated as optional, bypassed without declared CONST-W3 justification, or downgraded from P0 severity." +condition: + - '(?i)(?:downgrade|waive|ignore|skip)\s+(?:the\s+)?constitution' + - '(?ims)severity\s*:\s*(?:P1|P2|P3|advisory)\b.{0,200}?\bCONST-[A-Z][0-9]+\b' + - '(?ims)\bCONST-[A-Z][0-9]+\b.{0,200}?\bseverity\s*:\s*(?:P1|P2|P3|advisory)\b' +scope: + - 'tool:edit(**/*)' + - 'tool:write(**/*)' + - 'tool:ast_edit(**/*)' +interruptMode: tool-only +--- + +# Constitutional Interruption: Constitutional Review & Conduct Violation + +You are attempting to waive, downgrade, or silently bypass the Supreme Engineering Constitution (`CONSTITUTION.md`): + +1. **Automatic P0 Severity on All Violations (`CONST-G3`):** + - Every undeclared violation of the constitution during review is an **automatic, non-negotiable P0 failure**. + - **ZERO APPEALS, NO SEVERITY DOWNGRADES, NO WAIVERS.** + - **DO NOT** downgrade a constitutional violation to an advisory, P1, P2, or non-blocking finding. + - **DO NOT** approve or merge any change while an undeclared constitutional violation exists. + +2. **No Silent Bypass (`CONST-W3`):** + - The **ONLY** legal exception to any rule is an explicit, declared bypass stated in the open, in the change itself, naming the exact rule ID and the specific reason why it was wrong or impossible for this case. + - Undeclared breaches cannot be waived by reviewer discretion. + +3. **Depth Over Expedience (`CONST-S1`):** + - Fix the root cause. Restructure when the design is wrong. Do not patch symptoms to ship faster. + +4. **Subtract Before You Add (`CONST-S4`):** + - Treat every line as a liability. Delete dead branches and replace complex records with unconstructable tagged unions before adding new abstractions. + +5. **First Principles Over Precedent (`CONST-S2`):** + - Neighboring code is evidence of what exists, never of what is correct. Justify all patterns by first principles. + +Block the change or declare the bypass explicitly under `CONST-W3`. diff --git a/repos/constitution/plugins/constitution/rules/constitution-pure-core.md b/repos/constitution/plugins/constitution/rules/constitution-pure-core.md new file mode 100644 index 0000000..857ec9e --- /dev/null +++ b/repos/constitution/plugins/constitution/rules/constitution-pure-core.md @@ -0,0 +1,33 @@ +--- +description: "Fires on edits/writes that violate Article I (The Pure Core) of the Constitution: impure decisions doing I/O or throwing, unvalidated primitives in domain models, unbranched boolean/string error distinctions, or procedural control flow." +condition: + - '(?i)throw\s+new\s+Error\(' + - '(?i)(?:class|interface)\s+\w+\s*\{[^}]*?\b(?:success|isError)\s*:\s*\b(?:true|false|boolean\b)' + - '(?i)type\s+\w+\s*=\s*\{[^}]*?\b(?:success|isError)\s*:\s*\b(?:true|false|boolean\b)' +scope: + - 'tool:edit(**/*.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:write(**/*.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:ast_edit(**/*.{ts,tsx,js,jsx,rs,py,go})' +interruptMode: tool-only +--- + +# Constitutional Interruption: Article I — The Pure Core Violation + +You are authoring code that violates **Article I (The Pure Core)** of the Supreme Engineering Constitution (`CONSTITUTION-ARTICLES.md`): + +1. **Pure Decisions (`CONST-P1`):** Every domain decision must be a pure function (data in, value or typed error out). + - **DO NOT** perform I/O, read system clocks, use randomness, or `throw` exceptions inside decision logic. + - Return errors as typed values. + +2. **Types Before Logic (`CONST-D1` & `CONST-D4`):** Make illegal states unrepresentable. + - **DO NOT** encode states by field presence (e.g. `Order { status, shippedAt?, trackingId? }`). Model mutually exclusive states as a tagged union. + - **DO NOT** use unchecked casts (`as any`, `as unknown`). + +3. **Distinct Tagged Errors (`CONST-D2`):** Give every failure its own tagged variant. + - **DO NOT** distinguish errors by a `success: boolean` flag or `message: string`. Callers must branch exhaustively on the variant tag. + +4. **No Primitive Obsession (`CONST-D3`):** Brand domain-significant numbers, IDs, and codes. + +5. **Single-Path Core (`CONST-P2`):** Pure decisions are expressions, not procedures (cyclomatic complexity = 1). Use exhaustive pattern matching / fold / map rather than procedural `if/else`, loops, or `switch` statements in pure decision modules. + +Remediate the violation before re-issuing this write. diff --git a/repos/constitution/plugins/constitution/rules/constitution-verification.md b/repos/constitution/plugins/constitution/rules/constitution-verification.md new file mode 100644 index 0000000..0815fa4 --- /dev/null +++ b/repos/constitution/plugins/constitution/rules/constitution-verification.md @@ -0,0 +1,42 @@ +--- +description: "Fires on edits/writes that violate Article III (Verification) of the Constitution: mocking the only real implementation, authoring tautological/characterization tests, testing intermediate private forwarding helpers, or computing expected test values by invoking the SUT." +condition: + - '(?i)(?:vi|jest)\.mock\(\s*["\x27]\.\.?/' + - '(?i)(?:sinon|td)\.replace\(\s*["\x27]\.\.?/' + - '(?i)expect\(\s*(\w+)\s*\([^)]*\)\s*\)\.to(?:Be|Equal|StrictEqual)\(\s*\1\s*\([^)]*\)\s*\)' + - '(?i)\bcharacterization\b' + - '(?i)\bgolden[\s_-]+master\b' +scope: + - 'tool:edit(**/*.{test,spec}.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:write(**/*.{test,spec}.{ts,tsx,js,jsx,rs,py,go})' + - 'tool:ast_edit(**/*.{test,spec}.{ts,tsx,js,jsx,rs,py,go})' +interruptMode: tool-only +--- + +# Constitutional Interruption: Article III — Verification Violation + +You are authoring tests that violate **Article III (Verification)** of the Supreme Engineering Constitution (`CONSTITUTION-ARTICLES.md`): + +1. **Test Public Exports Directly, Pure Logic with Mutation (`CONST-T8`):** + - Test the public API with real inputs and outputs. + - **DO NOT** write unit tests for intermediate helper functions that only pass data to other functions. + - **DO NOT** mock a dependency when only one real version of it exists. + - **DO NOT** mix adapters or I/O code into the same mutation test run as pure calculation logic. + +2. **Properties by Narrow Grant (`CONST-T14`):** + - Pure decisions earn property tests only when a universal invariant over generated inputs cannot be fully pinned from the public surface. + - **DO NOT** spray property tests across decisions already covered from the exported surface. + +3. **Mutation Is the Measure (`CONST-T3` & `CONST-T13`):** + - Gate change-relevant pure logic at a 100% mutation kill score over a named, non-empty set. + - Never suppress mutants or shrink the mutated set after the fact. + +4. **Independent Oracles (`CONST-T10` & `CONST-T9`):** + - Every test assertion must have an oracle the SUT did not produce (spec literal, second implementation, law relating two views). + - **DO NOT** compute expected values by calling the implementation under test. + - **DO NOT** write tautological / characterization tests that simply record current output and assert it. + +5. **Snapshots on Public Exports Only (`CONST-T11`):** + - Snapshot only canonicalized, published output formats through the package's published export map. + +Fix the test architecture before proceeding. diff --git a/repos/constitution/scripts/validate-constitution.ts b/repos/constitution/scripts/validate-constitution.ts new file mode 100755 index 0000000..bf507e0 --- /dev/null +++ b/repos/constitution/scripts/validate-constitution.ts @@ -0,0 +1,288 @@ +#!/usr/bin/env -S deno run --allow-read=CONSTITUTION.md,CONSTITUTION-ARTICLES.md --allow-run=git +/** + * Validate the constitution corpus against constitution-rule/v1. + * + * Gate for CONST-E1 applied reflexively: the constitution's own format must fail a + * command, not a cited clause. Validates every fenced ```yaml block against + * hardcoded schema fields (required_fields, optional_fields, gate_values). + * + * Coverage is checked before schema. A rule the parser never reaches cannot be + * validated, and an unterminated fence silently removes every rule after it from + * the block — so counting ids in the raw text and comparing against ids parsed + * out of blocks is the only way this gate can report on what it did NOT see. + * Without that comparison a green run means "no rule I happened to parse was + * malformed", which is not the claim the gate is making. + * + * The corpus is two files — the resident law and the retrieved articles — and the + * union is the unit every check runs over. Ids are unique across it and citations + * resolve across it: CONST-S4 cites CONST-T9, which lives in the other file. Point + * this at one file and the coverage comparison above still passes, on a third of + * the rules, which is precisely the vacuous pass it exists to prevent. A file that + * is merely absent is not the only shape of that pass: a file present and parsing + * but declaring no rule scores identically, so every path in PATHS must contribute + * at least one rule of its own. + * + * There is no backwards compatibility and no retirement ledger. A deleted rule + * leaves its number vacant and a citation to it resolves to nothing, which is a + * loud failure and needs no gate. Vacancy is named, not enforced: --against lists + * ids vacated since the revision and corpus files absent at it on the success + * line, because a green line that silently measured less is the vacuous pass this + * gate exists to prevent. The one identifier defect that is NOT loud is an id that + * survives while its rule changes underneath it: every citation keeps resolving, + * to the wrong rule. No single revision can see that, so `--against ` + * recomputes it from git. + * + * Exit 0 clean, 1 with a named defect list, 3 unmeasurable — no identifiers matched + * at all, reported distinctly because an id pattern that matches nothing scores a + * healthy corpus and an id-free one identically. + */ +import { parse } from "@std/yaml"; + +const PATHS = ["CONSTITUTION.md", "CONSTITUTION-ARTICLES.md"] as const; + +const ID_RE = /^CONST-[A-Z]\d+$/; +const ID_IN_TEXT_RE = /^\s*- id:\s*(\S+)\s*$/gm; +const TITLE_IN_TEXT_RE = /^\s*- id:\s*(\S+)\s*\n\s*title:\s*(.+?)\s*$/gm; +const CITE_RE = /\bCONST-[A-Z]\d+\b/g; + +// A family letter names what a rule is ABOUT, never where it sits. Adding a +// letter here is half the change; the other half is the registry in AGENTS.md. +const FAMILIES: Record = { + "G": "Governance", + "E": "Enforcement", + "P": "Purity", + "D": "Domain modelling", + "B": "Boundary", + "T": "Testing", + "N": "Naming & structure", + "W": "Work discipline", + "S": "Subtraction", +}; + +const REQUIRED_FIELDS = ["id", "title", "gate", "do", "dont", "harm", "check"]; +const OPTIONAL_FIELDS = ["scope", "example", "layers"]; +const GATE_VALUES: Record = { + "lint": true, + "type-checker": true, + "mutation": true, + "review": true, +}; +const KNOWN_FAMILIES = Object.keys(FAMILIES).sort().join(", "); +const KNOWN_GATES = Object.keys(GATE_VALUES).sort().join(", "); + +type Rule = Record; + +function fail(errors: string[]): never { + for (const e of errors) console.log(`FAIL ${e}`); + Deno.exit(1); +} + +function titlesFrom(text: string): Map { + const titles = new Map(); + for (const m of text.matchAll(TITLE_IN_TEXT_RE)) titles.set(m[1], m[2]); + return titles; +} + +const againstIndex = Deno.args.indexOf("--against"); +const againstEquals = Deno.args.find((a) => a.startsWith("--against=")); +const against = againstIndex >= 0 + ? Deno.args[againstIndex + 1] + : againstEquals?.slice("--against=".length); +const againstRequested = against !== undefined || againstIndex >= 0 || + againstEquals !== undefined; +if (againstRequested && (against === undefined || against.length === 0)) { + fail([ + "--against requires a revision (form: --against or --against=) — a silently skipped comparison is the vacuous pass this gate exists to prevent", + ]); +} +const strayAgainst = Deno.args.find((a) => + a.startsWith("--against") && a !== "--against" && !a.startsWith("--against=") +); +if (strayAgainst !== undefined) { + fail([`unknown flag '${strayAgainst}' — did you mean --against ?`]); +} + +const errors: string[] = []; + +const texts: Record = {}; +for (const p of PATHS) { + try { + texts[p] = await Deno.readTextFile(p); + } catch { + fail([ + `${p}: missing — the corpus is both files, and half a corpus scores exactly like a whole one`, + ]); + } +} + +const blocks: Array<{ path: string; index: number; body: string }> = []; +for (const [p, t] of Object.entries(texts)) { + const found = [...t.matchAll(/```yaml\n([\s\S]*?)```/g)]; + if (found.length === 0) { + errors.push(`${p}: no fenced yaml rule blocks found`); + } + found.forEach((m, j) => blocks.push({ path: p, index: j, body: m[1] })); +} +if (blocks.length === 0) { + fail([...errors, "no fenced yaml rule blocks found in any corpus file"]); +} + +const rules: Rule[] = []; +for (const b of blocks) { + try { + const doc = parse(b.body) as { rules?: Rule[] } | null; + rules.push(...(doc?.rules ?? [])); + } catch (e) { + errors.push( + `${b.path} block ${b.index}: YAML parse error: ${(e as Error).message}`, + ); + } +} + +const parsedIds = rules.map((r) => String(r.id)); +const declaredIds = Object.values(texts).flatMap((t) => + [...t.matchAll(ID_IN_TEXT_RE)].map((m) => m[1]) +); +const parsedIdSet = new Set(parsedIds); +const uncovered = declaredIds.filter((i) => !parsedIdSet.has(i)); +if (uncovered.length > 0) { + errors.push( + `${uncovered.length} rule(s) declared in the corpus but never parsed into a yaml block: [${uncovered.join(", ")}] — check for an unterminated \`\`\`yaml fence`, + ); +} + +if (declaredIds.length === 0) { + console.log( + "UNMEASURABLE: no rule identifiers matched — the corpus is empty, or the id syntax moved", + ); + Deno.exit(3); +} + +const seen = new Set(); +for (const r of rules) { + const rid = String(r.id ?? ""); + for (const f of REQUIRED_FIELDS) { + if (!(f in r)) errors.push(`${rid}: missing required field '${f}'`); + } + const unknown = Object.keys(r).filter((k) => + !REQUIRED_FIELDS.includes(k) && !OPTIONAL_FIELDS.includes(k) + ); + if (unknown.length > 0) { + errors.push(`${rid}: unknown fields [${unknown.sort().join(", ")}]`); + } + if (!ID_RE.test(rid)) { + errors.push(`${rid}: id does not match ${ID_RE.source}`); + } else if (!Object.hasOwn(FAMILIES, rid["CONST-".length])) { + errors.push( + `${rid}: family '${rid["CONST-".length]}' is not registered — known families are [${KNOWN_FAMILIES}]`, + ); + } + if (seen.has(rid)) errors.push(`${rid}: duplicate id`); + seen.add(rid); + if (typeof r.gate !== "string" || !Object.hasOwn(GATE_VALUES, r.gate)) { + errors.push(`${rid}: gate '${String(r.gate)}' not in [${KNOWN_GATES}]`); + } + for (const f of ["do", "dont"] as const) { + const v = r[f]; + const shaped = typeof v === "string" || + (Array.isArray(v) && v.every((x) => typeof x === "string")); + if (!shaped) errors.push(`${rid}: '${f}' must be a string or list of strings`); + } + const ex = r.example; + if (ex !== undefined && ex !== null) { + const shaped = typeof ex === "object" && !Array.isArray(ex) && + Object.values(ex).every((v) => typeof v === "string"); + if (!shaped) errors.push(`${rid}: 'example' must be a map of strings`); + } +} + +const cites: Record> = {}; +for (const [p, t] of Object.entries(texts)) { + cites[p] = new Set([...t.matchAll(CITE_RE)].map((m) => m[0])); +} +const allCited = new Set(); +for (const s of Object.values(cites)) { + for (const c of s) allCited.add(c); +} +for (const cited of [...allCited].sort()) { + if (seen.has(cited)) continue; + for (const p of PATHS) { + if (cites[p]?.has(cited)) { + errors.push(`dangling citation: '${cited}' is cited in ${p} but names no rule`); + } + } +} + +async function checkAgainst( + rev: string, + errors: string[], + liveTitles: Map, +): Promise<{ vacated: string[]; uncompared: string[] }> { + const oldTitles = new Map(); + const uncompared: string[] = []; + try { + const results = await Promise.all(PATHS.map(async (p) => { + const cmd = new Deno.Command("git", { + args: ["show", `${rev}:${p}`], + stdout: "piped", + stderr: "piped", + }); + const out = await cmd.output(); + if (!out.success) return { p, absent: true, titles: [] }; + return { p, absent: false, titles: [...titlesFrom(new TextDecoder().decode(out.stdout))] }; + })); + for (const r of results) { + if (r.absent) { + uncompared.push(r.p); + continue; + } + for (const [rid, title] of r.titles) oldTitles.set(rid, title); + } + } catch (e) { + errors.push(`--against ${rev}: git is not runnable (${(e as Error).message})`); + return { vacated: [], uncompared: [] }; + } + + if (oldTitles.size === 0) { + errors.push( + `--against ${rev}: no rules found in any corpus file at that revision — wrong rev, or every file was renamed`, + ); + return { vacated: [], uncompared: [] }; + } + + for (const [rid, oldTitle] of oldTitles) { + const live = liveTitles.get(rid); + if (live !== undefined && live !== oldTitle) { + errors.push( + `reassigned id: '${rid}' named "${oldTitle}" at ${rev} and names "${live}" now — every citation to it resolves to a different rule`, + ); + } + } + + const vacated = [...oldTitles.keys()].filter((rid) => !liveTitles.has(rid)) + .sort(); + return { vacated, uncompared: uncompared.sort() }; +} + +let vacated: string[] = []; +let uncompared: string[] = []; +if (against !== undefined) { + const liveTitles = new Map(); + for (const t of Object.values(texts)) { + for (const [rid, title] of titlesFrom(t)) liveTitles.set(rid, title); + } + ({ vacated, uncompared } = await checkAgainst(against, errors, liveTitles)); +} + +if (errors.length > 0) fail(errors); + +const suffix = against !== undefined ? `; no id reassigned since ${against}` : ""; +console.log( + `valid: ${rules.length} rules across ${blocks.length} yaml blocks in ${Object.keys(texts).length} files, ${Object.keys(FAMILIES).length} families${suffix}`, +); +if (against !== undefined && uncompared.length > 0) { + console.log(` not compared, absent at ${against}: ${uncompared.join(", ")}`); +} +if (against !== undefined && vacated.length > 0) { + console.log(` ${vacated.length} id(s) vacated since ${against}: ${vacated.join(", ")}`); +} diff --git a/scripts/codegraph-worktree-mcp.ts b/scripts/codegraph-worktree-mcp.ts new file mode 100755 index 0000000..5d3c420 --- /dev/null +++ b/scripts/codegraph-worktree-mcp.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run --allow-env + +function sanitizeInstance(name: string): string { + let s = name.replace(/[^A-Za-z0-9_.-]/g, '-').replace(/--/g, '-') + s = s.replace(/^[-_.]+/, '').replace(/[-_.]+$/, '') + return s || 'root' +} + +async function commandExists(cmd: string): Promise { + const c = new Deno.Command('which', { args: [cmd], stdout: 'null', stderr: 'null' }) + const { code } = await c.output() + return code === 0 +} + +async function provisionMcp(worktreePath: string): Promise { + try { + await Deno.stat(worktreePath) + } catch { + console.error(`codegraph-worktree-mcp: no such dir: ${worktreePath}`) + Deno.exit(1) + } + + let cg = '' + if (await commandExists('codegraph')) cg = 'codegraph' + else if (Deno.env.get('HOME')) { + const p = `${Deno.env.get('HOME')}/.local/bin/codegraph` + try { + await Deno.stat(p) + cg = p + } catch { /* not found */ } + } + if (!cg) { + console.error( + 'codegraph-worktree-mcp: codegraph CLI not found on PATH or at $HOME/.local/bin/codegraph — install omp-infra-bootstrap first', + ) + Deno.exit(1) + } + + console.log(`codegraph-worktree-mcp: ensuring instance for ${worktreePath}`) + { + const cmd = new Deno.Command(cg, { + args: [], + cwd: worktreePath, + stdout: 'null', + stderr: 'null', + }) + const { code } = await cmd.output() + if (code !== 0) { + console.error('codegraph-worktree-mcp: instance install failed') + Deno.exit(1) + } + } + + const base = worktreePath.replace(/\/+$/, '').split('/').at(-1) ?? 'root' + const instance = sanitizeInstance(base) + const socket = `${ + Deno.env.get('HOME') + }/.local/share/containers/storage/volumes/codegraph-${instance}-data/_data/codegraph.sock` + const mcpFile = `${worktreePath}/.mcp.json` + + let cfg: Record = {} + try { + const raw = await Deno.readTextFile(mcpFile) + cfg = JSON.parse(raw) + } catch (e) { + if (e instanceof SyntaxError) { + console.error(`codegraph-worktree-mcp: existing ${mcpFile} unreadable (${e}); overwriting`) + cfg = {} + } else if (!(e instanceof Deno.errors.NotFound)) { + // other IO error, overwrite + cfg = {} + } + } + const servers = (cfg['mcpServers'] as Record | undefined) ?? {} + servers['codegraph'] = { + type: 'stdio', + command: 'socat', + args: ['STDIO', `UNIX-CONNECT:${socket}`], + enabled: true, + } + cfg['mcpServers'] = servers + await Deno.writeTextFile(mcpFile, JSON.stringify(cfg, null, 2) + '\n') + console.log(`codegraph-worktree-mcp: ${mcpFile} -> ${socket}`) +} + +if (import.meta.main) { + const wt = Deno.args[0] ?? Deno.cwd() + await provisionMcp(wt) +} diff --git a/scripts/convert-to-relative-paths.ts b/scripts/convert-to-relative-paths.ts new file mode 100755 index 0000000..7702558 --- /dev/null +++ b/scripts/convert-to-relative-paths.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write +import { relative } from '@std/path' + +async function convertToRelativePaths(sharedRoot: string): Promise { + let count = 0 + for await (const entry of Deno.readDir(sharedRoot)) { + if (!entry.isDirectory) continue + const gitFile = `${sharedRoot}/${entry.name}/.git` + let stat: Deno.FileInfo + try { + stat = await Deno.stat(gitFile) + } catch { + continue + } + if (!stat.isFile) continue + const line = (await Deno.readTextFile(gitFile)).split('\n')[0] ?? '' + if (!line.startsWith('gitdir:')) continue + const absPath = line.slice('gitdir:'.length).trim() + if (!absPath.startsWith('/')) continue + const worktreePath = `${sharedRoot}/${entry.name}` + let rel: string + try { + rel = relative(worktreePath, absPath) + } catch { + continue + } + await Deno.writeTextFile(gitFile, `gitdir: ${rel}\n`) + console.log(` ${worktreePath}/.git -> ${rel}`) + count++ + } + console.log(`converted ${count} worktree .git files`) + return count +} + +if (import.meta.main) { + const root = Deno.args[0] ?? Deno.cwd() + try { + await Deno.stat(root) + } catch { + console.error(`error: ${root} is not a directory`) + Deno.exit(1) + } + await convertToRelativePaths(root) +} diff --git a/scripts/copy-codegraph.ts b/scripts/copy-codegraph.ts new file mode 100755 index 0000000..85504e5 --- /dev/null +++ b/scripts/copy-codegraph.ts @@ -0,0 +1,231 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run --allow-env +import { resolvePrimaryRepo } from './lib/git.ts' + +async function commandExists(cmd: string): Promise { + try { + const c = new Deno.Command('which', { args: [cmd], stdout: 'null', stderr: 'null' }) + const { code } = await c.output() + return code === 0 + } catch { + return false + } +} + +async function runCodegraphMcp(worktreePath: string): Promise { + const script = new URL('./codegraph-worktree-mcp.ts', import.meta.url).pathname + const cmd = new Deno.Command(Deno.execPath(), { + args: [ + 'run', + '--allow-read', + '--allow-write', + '--allow-run', + '--allow-env', + script, + worktreePath, + ], + stdout: 'piped', + stderr: 'piped', + }) + const { code, stderr } = await cmd.output() + if (code !== 0) { + const msg = new TextDecoder().decode(stderr).trim() + console.log(`copy-codegraph: codegraph MCP provisioning skipped (rc=${code}) ${msg}`) + } +} + +async function trySqliteBackup(src: string, dst: string): Promise { + if (!await commandExists('sqlite3')) return false + const cmd = new Deno.Command('sqlite3', { + args: [src, `.backup '${dst}'`], + stdout: 'null', + stderr: 'null', + }) + const { code } = await cmd.output() + if (code !== 0) return false + try { + const s = await Deno.stat(dst) + return s.size > 0 + } catch { + return false + } +} + +async function tryReflink(src: string, dst: string): Promise { + const cmd = new Deno.Command('cp', { + args: ['--reflink=always', src, dst], + stdout: 'null', + stderr: 'null', + }) + const { code } = await cmd.output() + return code === 0 +} + +async function tryCopy(src: string, dst: string): Promise { + try { + await Deno.copyFile(src, dst) + return true + } catch { + return false + } +} + +async function isSqliteOk(path: string): Promise { + if (!await commandExists('sqlite3')) return true // daemon will rebuild if corrupt + const cmd = new Deno.Command('sqlite3', { + args: [path, 'PRAGMA quick_check;'], + stdout: 'piped', + stderr: 'null', + }) + const { stdout } = await cmd.output() + const out = new TextDecoder().decode(stdout) + return out.includes('ok') +} + +async function copyCodegraph(worktreePath: string, primaryPath: string | null): Promise { + await runCodegraphMcp(worktreePath) + + const srcDb = primaryPath ? `${primaryPath}/.codegraph/codegraph.db` : '' + const dstDir = `${worktreePath}/.codegraph` + const dstDb = `${dstDir}/codegraph.db` + + if (!primaryPath || primaryPath === worktreePath) { + console.log( + 'copy-codegraph: no separate primary worktree, skipping warm copy (codegraph init will build fresh)', + ) + } else { + let srcExists = false + try { + const s = await Deno.stat(srcDb) + srcExists = s.size > 0 + } catch { + srcExists = false + } + if (!srcExists) { + console.log( + `copy-codegraph: no primary index at ${srcDb}, skipping warm copy (codegraph init will build fresh)`, + ) + } else { + let dstExists = false + try { + const s = await Deno.stat(dstDb) + dstExists = s.size > 0 + } catch { + dstExists = false + } + if (dstExists) { + console.log('copy-codegraph: worktree already has an index, skipping warm copy') + } else { + await Deno.mkdir(dstDir, { recursive: true }) + // sweep partials + for await (const e of Deno.readDir(dstDir)) { + if (e.name.startsWith('codegraph.db.partial.')) { + try { + await Deno.remove(`${dstDir}/${e.name}`) + } catch { /* ignore */ } + } + } + const tmpDb = `${dstDb}.partial.${Deno.pid}` + let mechanism = '' + // try sqlite backup + if (await trySqliteBackup(srcDb, tmpDb)) { + mechanism = 'sqlite backup, consistent snapshot' + } else { + try { + await Deno.remove(tmpDb) + } catch { /* ignore */ } + if (await tryReflink(srcDb, tmpDb)) { + mechanism = 'reflink, no data moved' + if (!await isSqliteOk(tmpDb)) { + console.log( + 'copy-codegraph: integrity check failed on copied index, dropping it (codegraph init will rebuild fresh)', + ) + try { + await Deno.remove(tmpDb) + } catch { /* ignore */ } + mechanism = '' + } + } else { + try { + await Deno.remove(tmpDb) + } catch { /* ignore */ } + if (await tryCopy(srcDb, tmpDb)) { + mechanism = 'full byte copy, no reflink on this filesystem' + if (!await isSqliteOk(tmpDb)) { + console.log( + 'copy-codegraph: integrity check failed on copied index, dropping it (full init will rebuild)', + ) + try { + await Deno.remove(tmpDb) + } catch { /* ignore */ } + mechanism = '' + } + } else { + console.log( + 'copy-codegraph: every copy mechanism failed, skipping DB copy (codegraph init will build fresh)', + ) + } + } + } + if (mechanism) { + try { + const s = await Deno.stat(tmpDb) + if (s.size > 0) { + await Deno.rename(tmpDb, dstDb) + console.log(`copy-codegraph: index warm-started (${mechanism})`) + } + } catch { + if (mechanism) { + console.log( + 'copy-codegraph: index copied but rename failed, dropping it (codegraph init will build fresh)', + ) + } + try { + await Deno.remove(tmpDb) + } catch { /* ignore */ } + } + } + } + } + } + + // codegraph init + let cgBin: string | null = null + if (await commandExists('codegraph')) cgBin = 'codegraph' + else { + try { + await Deno.stat(`${Deno.env.get('HOME') ?? ''}/.local/bin/codegraph`) + cgBin = `${Deno.env.get('HOME')}/.local/bin/codegraph` + } catch { /* not found */ } + } + if (cgBin) { + const cmd = new Deno.Command(cgBin, { + args: ['init'], + cwd: worktreePath, + stdout: 'null', + stderr: 'null', + }) + const { code } = await cmd.output() + if (code !== 0) { + console.log(`copy-codegraph: codegraph init failed (rc=${code}) — index left to the daemon`) + } + } else { + console.log('copy-codegraph: codegraph CLI not found, skipping init (daemon will index fresh)') + } +} + +if (import.meta.main) { + const worktreePath = Deno.args[0] + if (!worktreePath) { + console.error('worktree_path required') + Deno.exit(1) + } + let primary: string | null = Deno.args[1] ?? null + if (!primary) { + try { + primary = await resolvePrimaryRepo(worktreePath) + } catch { + primary = null + } + } + await copyCodegraph(worktreePath, primary) +} diff --git a/scripts/generate-artifacts.ts b/scripts/generate-artifacts.ts new file mode 100755 index 0000000..7e186df --- /dev/null +++ b/scripts/generate-artifacts.ts @@ -0,0 +1,39 @@ +#!/usr/bin/env -S deno run --allow-read --allow-run +import { exists } from '@std/fs' + +async function generateArtifacts(worktreePath: string): Promise { + console.log('generate-artifacts: generating build artifacts...') + let cmd: Deno.Command + if (await exists(`${worktreePath}/pnpm-lock.yaml`)) { + cmd = new Deno.Command('corepack', { args: ['pnpm', 'build'], cwd: worktreePath }) + } else if (await exists(`${worktreePath}/package-lock.json`)) { + cmd = new Deno.Command('npm', { args: ['run', 'build'], cwd: worktreePath }) + } else if (await exists(`${worktreePath}/yarn.lock`)) { + cmd = new Deno.Command('yarn', { args: ['build'], cwd: worktreePath }) + } else if (await exists(`${worktreePath}/bun.lock`)) { + cmd = new Deno.Command('bun', { args: ['run', 'build'], cwd: worktreePath }) + } else if (await exists(`${worktreePath}/Cargo.toml`)) { + cmd = new Deno.Command('cargo', { args: ['build'], cwd: worktreePath }) + } else if (await exists(`${worktreePath}/go.mod`)) { + cmd = new Deno.Command('go', { args: ['build', './...'], cwd: worktreePath }) + } else { + console.log('generate-artifacts: no recognized build system, skipping') + console.log('generate-artifacts: done') + return + } + const { code } = await cmd.output() + if (code !== 0) { + console.error(`generate-artifacts: build exited ${code}`) + Deno.exit(code) + } + console.log('generate-artifacts: done') +} + +if (import.meta.main) { + const p = Deno.args[0] + if (!p) { + console.error('worktree_path required') + Deno.exit(1) + } + await generateArtifacts(p!) +} diff --git a/scripts/install-deps.ts b/scripts/install-deps.ts new file mode 100755 index 0000000..8fb49cb --- /dev/null +++ b/scripts/install-deps.ts @@ -0,0 +1,78 @@ +#!/usr/bin/env -S deno run --allow-read --allow-run --allow-env +import { exists } from '@std/fs' + +type Manager = { check: string; cmd: string[] } + +async function pickManager(worktreePath: string): Promise { + if (await exists(`${worktreePath}/pnpm-lock.yaml`)) { + return { check: 'pnpm-lock.yaml', cmd: ['corepack', 'pnpm', 'install', '--frozen-lockfile'] } + } + if (await exists(`${worktreePath}/package-lock.json`)) { + return { check: 'package-lock.json', cmd: ['npm', 'ci'] } + } + if (await exists(`${worktreePath}/yarn.lock`)) { + return { check: 'yarn.lock', cmd: ['yarn', 'install', '--frozen-lockfile'] } + } + if (await exists(`${worktreePath}/bun.lock`)) { + return { check: 'bun.lock', cmd: ['bun', 'install', '--frozen-lockfile'] } + } + if (await exists(`${worktreePath}/Cargo.toml`)) { + return { check: 'Cargo.toml', cmd: ['cargo', 'build'] } + } + if (await exists(`${worktreePath}/go.mod`)) { + return { check: 'go.mod', cmd: ['go', 'mod', 'download'] } + } + if (await exists(`${worktreePath}/Gemfile`)) { + return { check: 'Gemfile', cmd: ['bundle', 'install'] } + } + if ( + (await exists(`${worktreePath}/pyproject.toml`)) || + (await exists(`${worktreePath}/requirements.txt`)) + ) { + return { check: 'pyproject/requirements', cmd: ['pip-install'] } + } + return null +} + +async function installDeps(worktreePath: string): Promise { + console.log('install-deps: installing dependencies in worktree...') + const mgr = await pickManager(worktreePath) + if (!mgr) { + console.log('install-deps: no recognized package manager, skipping') + console.log('install-deps: done') + return + } + if (mgr.cmd[0] === 'pip-install') { + let cmd = new Deno.Command('pip', { args: ['install', '-e', '.'], cwd: worktreePath }) + let { code } = await cmd.output() + if (code !== 0) { + cmd = new Deno.Command('pip', { + args: ['install', '-r', 'requirements.txt'], + cwd: worktreePath, + }) + ;({ code } = await cmd.output()) + if (code !== 0) { + console.error(`install-deps: pip install exited ${code}`) + Deno.exit(code) + } + } + console.log('install-deps: done') + return + } + const cmd = new Deno.Command(mgr.cmd[0], { args: mgr.cmd.slice(1), cwd: worktreePath }) + const { code } = await cmd.output() + if (code !== 0) { + console.error(`install-deps: ${mgr.cmd.join(' ')} exited ${code}`) + Deno.exit(code) + } + console.log('install-deps: done') +} + +if (import.meta.main) { + const p = Deno.args[0] + if (!p) { + console.error('worktree_path required') + Deno.exit(1) + } + await installDeps(p!) +} diff --git a/scripts/lib/fs.ts b/scripts/lib/fs.ts new file mode 100644 index 0000000..d2138da --- /dev/null +++ b/scripts/lib/fs.ts @@ -0,0 +1,26 @@ +export async function isDirectory(path: string): Promise { + try { + const s = await Deno.stat(path) + return s.isDirectory + } catch { + return false + } +} + +export async function isFile(path: string): Promise { + try { + const s = await Deno.stat(path) + return s.isFile + } catch { + return false + } +} + +export async function isSymlink(path: string): Promise { + try { + const s = await Deno.lstat(path) + return s.isSymlink + } catch { + return false + } +} diff --git a/scripts/lib/git.ts b/scripts/lib/git.ts new file mode 100644 index 0000000..2947f4a --- /dev/null +++ b/scripts/lib/git.ts @@ -0,0 +1,41 @@ +import { dirname, resolve } from '@std/path' + +async function gitRevParse(worktreeRoot: string, arg: string): Promise { + const cmd = new Deno.Command('git', { + args: ['rev-parse', arg], + cwd: worktreeRoot, + stdout: 'piped', + stderr: 'piped', + }) + const { code, stdout, stderr } = await cmd.output() + if (code !== 0) { + const msg = new TextDecoder().decode(stderr).trim() + throw new Error(`git rev-parse ${arg} failed in ${worktreeRoot}: ${msg}`) + } + return new TextDecoder().decode(stdout).trim() +} + +export async function getGitDir(worktreeRoot: string): Promise { + return await gitRevParse(worktreeRoot, '--git-dir') +} + +export async function getGitCommonDir(worktreeRoot: string): Promise { + return await gitRevParse(worktreeRoot, '--git-common-dir') +} + +/** Returns primary repo path if worktreeRoot is a worktree, else null. */ +export async function resolvePrimaryRepo(worktreeRoot: string): Promise { + const gitDir = await getGitDir(worktreeRoot) + const gitCommonDir = await getGitCommonDir(worktreeRoot) + if (gitDir === gitCommonDir) return null + const absCommon = gitCommonDir.startsWith('/') + ? gitCommonDir + : resolve(worktreeRoot, gitCommonDir) + return dirname(absCommon) +} + +export async function runGit(args: string[], cwd: string): Promise<{ code: number }> { + const cmd = new Deno.Command('git', { args, cwd, stdout: 'null', stderr: 'null' }) + const { code } = await cmd.output() + return { code } +} diff --git a/scripts/lib/mod.ts b/scripts/lib/mod.ts new file mode 100644 index 0000000..1a71f11 --- /dev/null +++ b/scripts/lib/mod.ts @@ -0,0 +1,3 @@ +export * from './fs.ts' +export * from './git.ts' +export * from './paths.ts' diff --git a/scripts/lib/paths.ts b/scripts/lib/paths.ts new file mode 100644 index 0000000..eb67108 --- /dev/null +++ b/scripts/lib/paths.ts @@ -0,0 +1,14 @@ +import { relative } from '@std/path' + +/** + * Safe relative helper — returns null instead of throwing when paths are + * incompatible (different roots). Forward-looking vs bash `realpath --relative-to` + * which exits non-zero. + */ +export function tryRelative(from: string, to: string): string | null { + try { + return relative(from, to) + } catch { + return null + } +} diff --git a/scripts/post-switch.ts b/scripts/post-switch.ts new file mode 100755 index 0000000..a1750cb --- /dev/null +++ b/scripts/post-switch.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env -S deno run --allow-run +async function postSwitch(worktreePath: string): Promise { + const cmd = new Deno.Command('git', { + args: ['-C', worktreePath, 'config', '--unset', 'extensions.relativeWorktrees'], + stdout: 'null', + stderr: 'null', + }) + await cmd.output() + console.log(`post-switch: done (${worktreePath})`) +} + +if (import.meta.main) { + const p = Deno.args[0] + if (!p) { + console.error('worktree_path required') + Deno.exit(1) + } + await postSwitch(p!) +} diff --git a/scripts/pre-merge.ts b/scripts/pre-merge.ts new file mode 100755 index 0000000..32445f2 --- /dev/null +++ b/scripts/pre-merge.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run +import { join } from '@std/path' +import { isSymlink } from './lib/fs.ts' +import { runGit } from './lib/git.ts' + +const ISSUE_PATTERNS = [/^[0-9].*-.*\.md$/, /^T[0-9a-fA-F].*-.*\.md$/] + +async function preMerge(worktreePath: string): Promise { + for await (const entry of Deno.readDir(worktreePath)) { + if (!ISSUE_PATTERNS.some((re) => re.test(entry.name))) continue + const full = join(worktreePath, entry.name) + if (!(await isSymlink(full))) continue + try { + await Deno.remove(full) + console.log(`pre-merge: removed symlinked issue ${entry.name}`) + } catch (err) { + console.error(`pre-merge: failed to remove ${entry.name}: ${err}`) + } + } + const { code } = await runGit(['add', '-A'], worktreePath) + if (code !== 0) { + console.error(`pre-merge: git add -A failed (exit ${code})`) + Deno.exit(code) + } +} + +if (import.meta.main) { + const p = Deno.args[0] + if (!p) { + console.error('worktree_path required') + Deno.exit(1) + } + await preMerge(p) +} diff --git a/scripts/pre-start.ts b/scripts/pre-start.ts new file mode 100755 index 0000000..9190e89 --- /dev/null +++ b/scripts/pre-start.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run --allow-env +import { dirname, relative } from '@std/path' +import { resolvePrimaryRepo } from './lib/git.ts' + +async function convertMainRepoGitdirToRelative( + worktreePath: string, + primaryPath: string, +): Promise { + const worktreesDir = `${primaryPath}/.git/worktrees` + try { + await Deno.stat(worktreesDir) + } catch { + return 0 + } + const expected = `${worktreePath}/.git` + let converted = 0 + for await (const entry of Deno.readDir(worktreesDir)) { + if (!entry.isDirectory) continue + const gitdirFile = `${worktreesDir}/${entry.name}/gitdir` + let target: string + try { + target = (await Deno.readTextFile(gitdirFile)).trim() + } catch { + continue + } + if (target !== expected || !target.startsWith('/')) continue + const baseDir = dirname(gitdirFile) + let rel: string + try { + rel = relative(baseDir, target) + } catch { + continue + } + await Deno.writeTextFile(gitdirFile, rel + '\n') + console.log(` gitdir: ${entry.name} -> ${rel}`) + converted++ + } + return converted +} + +async function convertWorktreeGitfileToRelative(worktreePath: string): Promise { + const gitFile = `${worktreePath}/.git` + let stat: Deno.FileInfo + try { + stat = await Deno.stat(gitFile) + } catch { + return false + } + if (!stat.isFile) return false + const line = (await Deno.readTextFile(gitFile)).split('\n')[0] ?? '' + if (!line.startsWith('gitdir:')) return false + const absPath = line.slice('gitdir:'.length).trim() + if (!absPath.startsWith('/')) return false + let rel: string + try { + rel = relative(worktreePath, absPath) + } catch { + return false + } + await Deno.writeTextFile(gitFile, `gitdir: ${rel}\n`) + console.log(` .git -> ${rel}`) + return true +} + +async function linkSharedDir( + name: string, + worktreePath: string, + primaryPath: string, +): Promise { + const src = `${primaryPath}/${name}` + try { + const s = await Deno.stat(src) + if (!s.isDirectory) { + console.log(`pre-start: no ${name} in primary, skipping symlink`) + return false + } + } catch { + console.log(`pre-start: no ${name} in primary, skipping symlink`) + return false + } + const target = `${worktreePath}/${name}` + try { + const t = await Deno.lstat(target) + if (!t.isSymlink) { + console.log(`pre-start: ${target} is a real directory, leaving it`) + return false + } + } catch { + // missing — create + } + try { + await Deno.remove(target) + } catch { + // ignore + } + const rel = relative(worktreePath, src) + await Deno.symlink(rel, target) + console.log(`pre-start: ${name} -> ${rel}`) + return true +} + +async function main(args: string[] = Deno.args): Promise { + const worktreePath = args[0] + if (!worktreePath) { + console.error('worktree_path required') + Deno.exit(1) + } + let primaryPath: string | null = args[1] ?? null + if (!primaryPath) { + try { + primaryPath = await resolvePrimaryRepo(worktreePath) + } catch { + primaryPath = null + } + } + if (primaryPath) { + await convertMainRepoGitdirToRelative(worktreePath, primaryPath) + await convertWorktreeGitfileToRelative(worktreePath) + } + const cmd = new Deno.Command('git', { + args: ['-C', worktreePath, 'config', '--unset', 'extensions.relativeWorktrees'], + stdout: 'null', + stderr: 'null', + }) + await cmd.output() + + if (!primaryPath) { + console.log('pre-start: running in primary repo (not a worktree) — done') + return + } + await linkSharedDir('.repos', worktreePath, primaryPath) + await linkSharedDir('.issues', worktreePath, primaryPath) + await linkSharedDir('wiki', worktreePath, primaryPath) +} + +if (import.meta.main) await main() diff --git a/scripts/worktree-to-relative.ts b/scripts/worktree-to-relative.ts new file mode 100755 index 0000000..28b0840 --- /dev/null +++ b/scripts/worktree-to-relative.ts @@ -0,0 +1,51 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run +import { dirname, relative } from '@std/path' +import { resolvePrimaryRepo } from './lib/git.ts' + +/** + * Convert all .git/worktrees//gitdir paths to relative. + * Uses Deno APIs directly, idempotent, reports count. + * CLI product output. + */ +async function worktreeToRelative(worktreePath = Deno.cwd()): Promise { + const primaryPath = await resolvePrimaryRepo(worktreePath) + if (!primaryPath) { + console.error(`error: not a git repo at ${worktreePath}`) + Deno.exit(1) + } + const worktreesDir = `${primaryPath}/.git/worktrees` + try { + await Deno.stat(worktreesDir) + } catch { + console.log('no worktrees') + return 0 + } + let count = 0 + for await (const entry of Deno.readDir(worktreesDir)) { + const gitdirFile = `${worktreesDir}/${entry.name}/gitdir` + let target: string + try { + target = (await Deno.readTextFile(gitdirFile)).trim() + } catch { + continue + } + if (!target.startsWith('/')) continue + const baseDir = dirname(gitdirFile) + let rel: string + try { + rel = relative(baseDir, target) + } catch { + continue + } + await Deno.writeTextFile(gitdirFile, rel + '\n') + console.log(` gitdir: ${entry.name} -> ${rel}`) + count++ + } + console.log(`converted ${count} worktree gitdir files`) + return count +} + +if (import.meta.main) { + const p = Deno.args[0] ?? Deno.cwd() + await worktreeToRelative(p) +} diff --git a/subtrees.toml b/subtrees.toml new file mode 100644 index 0000000..4e66a8d --- /dev/null +++ b/subtrees.toml @@ -0,0 +1,5 @@ +[[repos]] +name = "constitution" +url = "https://github.com/systemfsoftware/constitution" +branch = "main" +description = "Shared engineering laws for clean code, testing, and project organization at System F Software" From ecda65ba895fbc160ddc55be180d7d85b70639de Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 11 Sep 2026 13:42:35 +0000 Subject: [PATCH 3/4] chore(repo): register constitution and worktrunk-scripts in subtrees.toml --- subtrees.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 subtrees.toml diff --git a/subtrees.toml b/subtrees.toml new file mode 100644 index 0000000..a384ed7 --- /dev/null +++ b/subtrees.toml @@ -0,0 +1,11 @@ +[[repos]] +name = "constitution" +url = "https://github.com/systemfsoftware/constitution" +description = "Shared engineering constitution: pure-core / imperative-shell design law" +branch = "main" + +[[repos]] +name = "worktrunk-scripts" +url = "https://github.com/systemfsoftware/worktrunk-scripts" +description = "Deno worktree hooks for worktrunk automating environment setup, warm-copy indexing, and path isolation" +branch = "main" From 5a53d9104e5f1720a32b6e359c0eee645fb48dbe Mon Sep 17 00:00:00 2001 From: Ryan Lee Date: Fri, 11 Sep 2026 13:51:07 +0000 Subject: [PATCH 4/4] chore(repo): exclude git-subtree synthetic squashes from PR commitlint --- commitlint.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/commitlint.config.ts b/commitlint.config.ts index 0c833ca..3244361 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -230,6 +230,7 @@ const configuration: UserConfig = { }, defaultIgnores: true, + ignores: [(commit) => commit.startsWith("Squashed '") || commit.includes('git-subtree-dir:')], formatter: '@commitlint/format', }