@holmes-lab/holmes-kit 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  <!-- @implements A-SPEC-209 -->
8
+ ## [0.27.0] - 2026-09-20
9
+
10
+ A repository can now say, as a number anyone can reproduce, how much of it is governed.
11
+
12
+ ### Added
13
+ - **`holmes-kit attest` — how many source files trace to an APPROVED spec** (A-SPEC-702). The request
14
+ was a "generated under holmes-kit" header in every source file. That was declined: a header is a
15
+ claim that can be pasted and keeps saying "governed" after an ungoverned edit, it brands files that
16
+ belong to the consumer, the same sentence in every file is measured retrieval noise, and line 1
17
+ already carries the `implements` anchor a gate verifies. What was missing is a repository-level
18
+ statement: the spec-side census lived only inside the dashboard and the file-side one did not exist.
19
+ `attest` adds **no new judgement** — the denominator is what the scanner returned, the numerator is
20
+ anchors ∩ approved ids (a draft anchor is not a trace), the spec axis is the existing link census —
21
+ and prints where the number is true: commit and a digest of the approved specs. `+dirty` is set only
22
+ when a path that *decides* the number is uncommitted (a scanned source or a spec), so writing the
23
+ badge does not make the badge a lie. `--exclude a,b` names trees that are not yours (vendored);
24
+ exclusions match at a path boundary, a prefix meaning "everything" is dropped, and they are echoed in
25
+ every output. What cannot be measured is `n/a` with the reason, never `0%` or `100%`; percentages are
26
+ floored, so `100` appears only when every file is traced; ten or fewer untraced files are named.
27
+ `--badge` writes a self-contained SVG (`.ax/badges/traced.svg` — no href, font, script or fetch).
28
+ `--readme` rewrites **only** the region between `<!-- holmes-kit:attest:begin -->` and
29
+ `<!-- holmes-kit:attest:end -->`: a README without the markers is never written (placing them is the
30
+ consent), a half, doubled or reversed marker is refused with exit 1, and a CRLF README stays CRLF.
31
+ The numbers never change the exit code — this is a statement, not a gate. CLI only, so it is the
32
+ same in every harness and adds no MCP schema cost. Measured on this repository: 396/614 (64.4%) as
33
+ scanned; 386/386 once `reference/` (212 vendored files), `dist-tools/` (build output that carried
34
+ anchors and had been counting as traced source) and `docs/goals/probes/` are named as excluded.
35
+
36
+ ## [0.26.2] - 2026-09-20
37
+
38
+ The last stretch of the number-collision repair: the plan `doctor` has been printing since 0.26.1 can
39
+ now be executed — the plan a person saw, under one approval, before the merge.
40
+
41
+ ### Added
42
+ - **`spec_reconcile` — the seen plan, one approval, before the merge** (A-SPEC-700.2). 0.26.1 detected a
43
+ spec number another machine took and printed the plan; executing it was still a person calling
44
+ `spec_renumber` once per family. `spec_reconcile({operation:'plan'})` is read-only: it chains the
45
+ existing collectors, collision detector, planner and `planRenumber` — no new judgement — and returns
46
+ the moves, how many documents / `depends_on` / anchors each rewrites, the re-seal order, the prose
47
+ candidates, and a `target` derived from the plan's content alone (no clock, no nonce: the same state
48
+ plans to the same target). `apply` takes that plan back, **rebuilds it inside the store hold**, and
49
+ refuses `plan-changed` if it is no longer the one that was seen — a fetch in between may have given
50
+ the destination away. It then demands one `config-write` approval bound to `target`, and runs every
51
+ move through the same path `spec_renumber` uses. Found while building it: `spec_renumber`'s own apply
52
+ asks for **no approval at all**, although it rewrites approved documents and source anchors; this
53
+ tool is the first path that honours REQ-700's "one approval" constraint, and the standalone tool is
54
+ unchanged (a separate decision). It never seals — `spec_approve` stays the only sealer, and the order
55
+ is returned. Prose is reported, never rewritten. No remote, or no git, is an answer (`moves: []` with
56
+ a note), not a refusal. Measured cost: the tool adds ≈414 schema tokens (41 tools ≈ 10,724).
57
+ An adversarial pass **run, not read**: forged `moves` under a true `target` are never read (what runs
58
+ is the recomputed plan); a grant is spent once and a refused apply spends nothing; a second move that
59
+ stops reports `moved` / `remaining` and planning again yields only the rest. It found one defect,
60
+ fixed here: with two colliding families that depend on each other, the re-seal order named a document
61
+ a sibling move had already renamed.
62
+ - `doctor`'s collision advice now ends by naming the tool: `… then re-seal with spec_approve, and merge
63
+ (spec_reconcile plan → apply runs every move under one approval)`.
64
+
65
+ ### Fixed
66
+ - **A finished entity operation is no longer read as "interrupted" or "vanished"** (A-SPEC-701). The
67
+ pending-operation scan read each `journal.json` twice while retirement is a single rename, so a scan
68
+ racing a retirement could see the journal on the first read and not the second, and refuse
69
+ `recovery-required` for work that had just completed — measured as the Linux CI red on
70
+ `entity-store-boundaries` at a commit that changed no code. The scan reads once and skips an
71
+ operation whose directory has retired; the set of legal outcomes is not widened.
72
+
8
73
  ## [0.26.1] - 2026-09-20
9
74
 
10
75
  Three judgements that were computed and reached nobody now reach a person, and a spec number another
package/README.md CHANGED
@@ -6,6 +6,13 @@
6
6
 
7
7
  > **"No Spec, No Code"** — Deterministic Agentic Software Engineering (ASE) harness with causal traceability.
8
8
 
9
+ <!-- holmes-kit:attest:begin -->
10
+ ![traced files](.ax/badges/traced.svg)
11
+
12
+ **386/386** source files (100%) trace to an approved spec · **629/680** approved A-SPECs (92.5%) are anchored from code.
13
+ Measured at `173b99650442` (specs `sha256:215f84d4e9f4`) — reproduce with `holmes-kit attest --exclude dist-tools/,docs/goals/probes/,reference/`.
14
+ <!-- holmes-kit:attest:end -->
15
+
9
16
  ---
10
17
 
11
18
  ## 🕵️ Philosophy & Vision: Beyond Code Generation
@@ -16,6 +23,8 @@
16
23
 
17
24
  ### 🛡️ Currently Supported Features (Production Features)
18
25
 
26
+ - 📏 **Say how much of the repository is governed — as a number, not a label** *(new in 0.27.0)*: `holmes-kit attest` reports how many scanned source files are anchored to an **approved** spec, beside how many approved A-SPECs are anchored from code, and says where that is true (commit, plus a digest of the approved specs) so anyone can reproduce it. No new judgement: the scanner's files, the anchors, the existing link census. A per-file "generated under holmes-kit" header was declined — it is a claim that survives an ungoverned edit, and line 1 already carries the anchor a gate verifies. `--exclude a,b` names vendored trees at a path boundary and is echoed in every output; what cannot be measured is `n/a`, never `0%`; `100` appears only when every file is traced. `--badge` writes a self-contained local SVG, and `--readme` rewrites **only** the region between the `holmes-kit:attest` markers — a README without them is never touched, and a half marker is refused. A statement, not a gate: the numbers never change the exit code.
27
+ - 🤝 **The collision plan runs under one approval** *(new in 0.26.2)*: 0.26.1 saw a spec number another machine took and printed the plan; carrying it out was still one `spec_renumber` call per family, and that tool's apply asks for no approval although it rewrites approved documents and source anchors. `spec_reconcile` `plan` is read-only and returns the moves, what each rewrites, the re-seal order, the prose it will **not** touch, and a `target` derived from the plan's content alone. `apply` rebuilds the plan inside the store hold, refuses `plan-changed` if a fetch moved it since you looked, demands one `config-write` approval bound to that `target`, and runs every move through the path `spec_renumber` already uses. Forged `moves` are never read — what runs is the recomputed plan. It never seals: `spec_approve` stays the only sealer. No remote is an answer, not a refusal.
19
28
  - 🔢 **A spec number another machine took is seen before the merge** *(new in 0.26.1)*: two checkouts allocated `REQ-694` thirteen minutes apart, neither could see the other, and it surfaced only as an add/add conflict after the push was refused — a rebuilt slice and five out-of-band approvals. `doctor --target` now reads each remote-tracking ref **as last fetched** (no network) and judges only the documents each side *added* since they parted, so a spec one side merely edited is divergence, not an alarm. On a collision the advice is the plan: `Before merging — move 901 → 903: spec_renumber(oldBase=901, newBase=903); then re-seal`. The side that moves is the one not published yet, the destination is free on both sides, and "published" means equal content — never path, because a spec's path is its number.
20
29
  - 🔁 **The cycle ratchet finally speaks — and takes a named exception** *(new in 0.26.1)*: since the commit that introduced it, the Stop hook computed the code cycles and called the verdict without them, so the `ART-2` line had never been printed; every unit test passed because each handed the evidence in directly. It is emitted now, bounded (five cycles, five members each, the rest counted — one vendored cycle here has 46 members), and `ax.config.json` gains `architecture.cycleIgnore`: prefixes for trees that are not yours to fix. A cycle is excepted only when every member lies under one, `reference` does not pardon `reference-impl/`, a broken config excepts nothing, and excepted cycles stay counted in the ledger.
21
30
  - 🧬 **Declare your architecture, in every language the graph can follow** *(new in 0.26.0)*: a C-SPEC's `## Forbidden Edges` takes a third rule kind beside `import` and `call` — `- inherits src/app -x-> ConcreteBase`, an exact match enforced at the pre-edit gate. Nothing is inferred: the rule reads the graph's `inherits` edges and never asks which class is "abstract", which is what makes it hold in Go, where interface satisfaction cannot be inferred at all. A project that already breaks the rule it wants adopts it with a **baseline that expires** — `- allow until 2026-09-27 : inherits src/legacy/a.ts -> LegacyBase`, exact on kind, file and target, never a prefix. Past its date the allowance still holds, because an expiry that blocked would turn the rule into a barrier to adoption; it is **reported** instead, carrying `(reconfirmed n since <date>)` so a twelfth renewal cannot hide.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 256f1396-mu9du216
1
+ 4ec4b788-mu9vw1zt
@@ -0,0 +1,15 @@
1
+ import { type AttestReport } from '../rtm/traced-files';
2
+ export interface AttestOptions {
3
+ root: string;
4
+ exclude: string[];
5
+ json: boolean;
6
+ badge: boolean;
7
+ readme: boolean;
8
+ }
9
+ export interface AttestIo {
10
+ stdout(s: string): void;
11
+ stderr(s: string): void;
12
+ }
13
+ export declare const BADGE_REL: string;
14
+ export declare function buildAttestReport(root: string, exclude: string[]): Promise<AttestReport>;
15
+ export declare function runAttest(o: AttestOptions, io: AttestIo): Promise<number>;
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BADGE_REL = void 0;
37
+ exports.buildAttestReport = buildAttestReport;
38
+ exports.runAttest = runAttest;
39
+ // @implements A-SPEC-702
40
+ /**
41
+ * `holmes-kit attest` — the surface that calls `rtm/traced-files`.
42
+ *
43
+ * Reads: one scan, the spec store, git (optional — REQ-128). Writes: AT MOST two files, and only
44
+ * when asked: the badge (`--badge`) and the region between the README markers (`--readme`). A README
45
+ * without markers is never written: placing the markers is the consent. Nothing reaches the network,
46
+ * and the measured numbers never change the exit code — this is a statement, not a gate.
47
+ */
48
+ const fs = __importStar(require("node:fs"));
49
+ const path = __importStar(require("node:path"));
50
+ const node_child_process_1 = require("node:child_process");
51
+ const cpg_scanner_1 = require("../cpg/cpg-scanner");
52
+ const spec_store_1 = require("../spec/spec-store");
53
+ const root_1 = require("../project/root");
54
+ const test_scope_1 = require("../rtm/test-scope");
55
+ const file_anchors_1 = require("../rtm/file-anchors");
56
+ const link_census_1 = require("../rtm/link-census");
57
+ const traced_files_1 = require("../rtm/traced-files");
58
+ exports.BADGE_REL = path.join('.ax', 'badges', 'traced.svg');
59
+ const SPECS_REL = path.join('.ax', 'specs');
60
+ const git = (root, args) => {
61
+ try {
62
+ return (0, node_child_process_1.execFileSync)('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(process.env) }).toString('utf8');
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ };
68
+ /**
69
+ * `+dirty` only when something that DECIDES the number is uncommitted: a scanned source file or a
70
+ * spec. The badge and the README are outputs of this very command, and a session ledger is always
71
+ * moving — counting them would make a committed badge say `+dirty` forever.
72
+ */
73
+ function decidingPathsDirty(root, scanned) {
74
+ const out = git(root, ['status', '--porcelain', '-z', '--untracked-files=all']);
75
+ if (out === undefined)
76
+ return false;
77
+ const specs = SPECS_REL.split(path.sep).join('/') + '/';
78
+ for (const entry of out.split('\0')) {
79
+ if (entry.length < 4)
80
+ continue;
81
+ const p = entry.slice(3);
82
+ if (p.startsWith(specs) || scanned.has(p))
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ async function buildAttestReport(root, exclude) {
88
+ const scanned = new cpg_scanner_1.CpgScanner().scan(root, root);
89
+ const specs = await new spec_store_1.LocalMarkdownRepository(path.join(root, SPECS_REL)).list();
90
+ const approved = specs.filter((s) => s.status === 'approved');
91
+ const files = scanned.map((f) => ({ path: f.sourcePath.split(path.sep).join('/'), anchors: f.implementsSpecs ?? [] }));
92
+ // The spec axis, the way the dashboard computes it (A-SPEC-655). `weakMentions` stays empty: it
93
+ // only splits the REASON a spec is unlinked, and this command reports the count.
94
+ const excludes = (0, traced_files_1.normalizeExcludes)(exclude);
95
+ const kept = files.filter((f) => !excludes.some((e) => f.path.startsWith(e)));
96
+ const census = (0, link_census_1.linkCensus)({
97
+ approvedIds: approved.filter((s) => s.id.startsWith('A-SPEC')).map((s) => s.id),
98
+ implemented: new Set(kept.flatMap((f) => f.anchors)),
99
+ testAnchors: (0, test_scope_1.scanTestAnchors)(root),
100
+ fileAnchors: (0, file_anchors_1.scanFileAnchors)(root),
101
+ weakMentions: {},
102
+ });
103
+ const commit = git(root, ['rev-parse', 'HEAD'])?.trim();
104
+ return {
105
+ files: (0, traced_files_1.tracedFiles)({ files, approvedIds: approved.map((s) => s.id), exclude }),
106
+ specs: { total: census.total, linked: census.codeLinked },
107
+ provenance: (0, traced_files_1.attestProvenance)({
108
+ commit,
109
+ dirty: commit ? decidingPathsDirty(root, new Set(files.map((f) => f.path))) : false,
110
+ approved: approved.map((s) => ({ id: s.id, digest: String(s.frontmatter.approved_digest ?? '') })),
111
+ exclude,
112
+ }),
113
+ };
114
+ }
115
+ async function runAttest(o, io) {
116
+ if (!fs.existsSync(o.root) || !fs.statSync(o.root).isDirectory()) {
117
+ io.stderr(`attest: ${o.root} is not a directory\n`);
118
+ return 2;
119
+ }
120
+ const report = await buildAttestReport(o.root, o.exclude);
121
+ io.stdout(o.json ? (0, traced_files_1.renderAttestJson)(report) : (0, traced_files_1.renderAttestText)(report));
122
+ let code = 0;
123
+ const note = (s) => { if (!o.json)
124
+ io.stdout(s);
125
+ else
126
+ io.stderr(s); };
127
+ if (o.badge) {
128
+ const file = path.join(o.root, exports.BADGE_REL);
129
+ const svg = (0, traced_files_1.renderAttestSvg)(report);
130
+ let same = false;
131
+ try {
132
+ same = fs.readFileSync(file, 'utf8') === svg;
133
+ }
134
+ catch { /* absent */ }
135
+ if (!same) {
136
+ fs.mkdirSync(path.dirname(file), { recursive: true });
137
+ fs.writeFileSync(file, svg);
138
+ }
139
+ note(`badge ${exports.BADGE_REL.split(path.sep).join('/')}${same ? ' (unchanged)' : ' written'}\n`);
140
+ }
141
+ if (o.readme) {
142
+ const file = path.join(o.root, 'README.md');
143
+ let text;
144
+ try {
145
+ text = fs.readFileSync(file, 'utf8');
146
+ }
147
+ catch {
148
+ text = undefined;
149
+ }
150
+ if (text === undefined)
151
+ note('readme README.md not found — nothing written\n');
152
+ else {
153
+ const r = (0, traced_files_1.replaceMarkerRegion)(text, (0, traced_files_1.renderAttestMarker)(report));
154
+ if (r.ok) {
155
+ if (r.changed)
156
+ fs.writeFileSync(file, r.text);
157
+ note(`readme marker region ${r.changed ? 'updated' : 'unchanged'}\n`);
158
+ }
159
+ else if (r.reason === 'no-marker')
160
+ note('readme no marker in README.md — nothing written (add the begin/end markers to opt in)\n');
161
+ else {
162
+ io.stderr('attest: README.md has an unbalanced holmes-kit:attest marker (each must appear exactly once, begin first) — nothing written\n');
163
+ code = 1;
164
+ }
165
+ }
166
+ }
167
+ return code;
168
+ }
@@ -4,6 +4,7 @@
4
4
  * Commands:
5
5
  * init [--mode guardrail|governed] [--target dir] [--dry-run] [--remove] [--force] ...
6
6
  * doctor [--target dir] [--json]
7
+ * attest [--target dir] [--exclude a,b] [--json] [--badge] [--readme]
7
8
  */
8
9
  /**
9
10
  * The installed package root. From `dist/holmes/cli/index.js` that is three levels up. Node resolves
@@ -55,6 +55,7 @@ const colophon_1 = require("./colophon");
55
55
  * Commands:
56
56
  * init [--mode guardrail|governed] [--target dir] [--dry-run] [--remove] [--force] ...
57
57
  * doctor [--target dir] [--json]
58
+ * attest [--target dir] [--exclude a,b] [--json] [--badge] [--readme]
58
59
  */
59
60
  /**
60
61
  * The installed package root. From `dist/holmes/cli/index.js` that is three levels up. Node resolves
@@ -78,6 +79,8 @@ const KNOWN_FLAGS = {
78
79
  init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent', 'mcp-launcher', 'autonomy', 'no-autonomy'],
79
80
  autonomy: ['help', 'target', 'for'],
80
81
  doctor: ['help', 'target', 'json'],
82
+ // @implements A-SPEC-702 — a statement, not a gate: how many source files trace to an approved spec.
83
+ attest: ['help', 'target', 'exclude', 'json', 'badge', 'readme'],
81
84
  skills: ['help', 'target'],
82
85
  ci: ['help', 'target', 'specs-dir', 'json'],
83
86
  serve: ['help', 'target', 'port'],
@@ -119,7 +122,7 @@ class UnknownFlagError extends Error {
119
122
  * what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
120
123
  * --force). A boolean flag must never consume a token.
121
124
  */
122
- const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
125
+ const BOOLEAN_FLAGS = new Set(['badge', 'readme', 'help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
123
126
  function parseFlags(argv, known) {
124
127
  const flags = {};
125
128
  const positionals = [];
@@ -239,6 +242,14 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
239
242
  --target <dir> also verify that target's wiring points at this install
240
243
  --json machine-readable output
241
244
 
245
+ holmes-kit attest [flags] say how many source files trace to an APPROVED spec (a statement, not a gate)
246
+ --target <dir> the project to measure (default: current directory)
247
+ --exclude <a,b> path prefixes that are not this project's source (vendored trees); echoed in the output
248
+ --json machine-readable output
249
+ --badge write a self-contained SVG to .ax/badges/traced.svg
250
+ --readme update ONLY the region between the holmes-kit:attest markers in README.md;
251
+ a README without the markers is never written
252
+
242
253
  holmes-kit upgrade [flags] install the latest and re-pin every recorded workspace (one command)
243
254
  --dry-run show the plan (current→latest, workspaces to re-pin), change nothing
244
255
  --yes skip the confirmation prompt (required in a non-interactive shell)
@@ -376,6 +387,14 @@ async function main(argv) {
376
387
  }
377
388
  return reported.code;
378
389
  }
390
+ // @implements A-SPEC-702 — reads one scan and the spec store; writes only the badge and the README's
391
+ // marker region, and only when asked. The numbers never change the exit code.
392
+ if (cmd === 'attest') {
393
+ const { runAttest } = require('./attest');
394
+ const target = typeof flags.target === 'string' ? path.resolve(flags.target) : process.cwd();
395
+ const exclude = typeof flags.exclude === 'string' ? flags.exclude.split(',') : [];
396
+ return runAttest({ root: target, exclude, json: flags.json === true, badge: flags.badge === true, readme: flags.readme === true }, { stdout: (t) => process.stdout.write(t), stderr: (t) => process.stderr.write(t) });
397
+ }
379
398
  if (cmd === 'doctor') {
380
399
  // A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
381
400
  // (round-6) — the typo is the diagnosis, so say it.
@@ -1,5 +1,6 @@
1
1
  import { SpecStore } from '../../spec/spec-store';
2
2
  import { Approval } from '../../guardrail/risk-gate';
3
+ import { type RenumberPlan } from '../../spec/renumber';
3
4
  export interface SpecLifecycleContext {
4
5
  store: SpecStore;
5
6
  resolveLedgerRoot(root?: string): {
@@ -64,6 +65,19 @@ export declare function createSpecLifecycleHandlers(context: SpecLifecycleContex
64
65
  dependents: string[];
65
66
  reason?: undefined;
66
67
  }>;
68
+ /**
69
+ * @implements A-SPEC-700.2
70
+ * The plan a person SAW, executed under ONE approval, before the merge. `plan` reads; `apply`
71
+ * rebuilds the plan, refuses `plan-changed` if it is not the one that was seen, demands a
72
+ * `config-write` approval bound to the plan's `target`, then runs every move through the SAME
73
+ * path `spec_renumber` uses, inside one store hold. It never seals: `spec_approve` is the only
74
+ * sealer, so `approveOrder` is returned and the caller runs it.
75
+ */
76
+ spec_reconcile(a: {
77
+ root?: string;
78
+ operation?: string;
79
+ plan?: unknown;
80
+ }): Promise<Record<string, unknown>>;
67
81
  spec_renumber(a: {
68
82
  root?: string;
69
83
  oldBase: string;
@@ -79,13 +93,13 @@ export declare function createSpecLifecycleHandlers(context: SpecLifecycleContex
79
93
  } | {
80
94
  ok: boolean;
81
95
  dryRun: boolean;
82
- plan: import("../../spec/renumber").RenumberPlan;
96
+ plan: RenumberPlan;
83
97
  movedSpecs?: undefined;
84
98
  } | {
85
99
  ok: boolean;
86
100
  dryRun: boolean;
87
101
  movedSpecs: number;
88
- plan: import("../../spec/renumber").RenumberPlan;
102
+ plan: RenumberPlan;
89
103
  }>;
90
104
  /**
91
105
  * @implements A-SPEC-538.1