@holmes-lab/holmes-kit 0.19.5 → 0.20.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +17 -1
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
@@ -0,0 +1,118 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { SpecStore } from '../../spec/spec-store';
3
+ import { DraftResult } from '../../reverse/draft';
4
+ import { AnchorMapping } from '../../reverse/anchor';
5
+ export interface AdoptionContext {
6
+ listSpecs(): Promise<Spec[]>;
7
+ writeSpec: SpecStore['write'];
8
+ assertReadableRoot(tool: string, root: string): void;
9
+ occupiedTarget(specs: Spec[]): string | undefined;
10
+ }
11
+ export declare function createAdoptionHandlers(context: AdoptionContext): {
12
+ /**
13
+ * Read-only inventory of a brownfield target: candidate clusters, coverage, and what the scan
14
+ * could not resolve. Deliberately does NOT call assertRepoTopLevel — a target that is not a git
15
+ * repository is a supported case here, reported as `isGit: false`.
16
+ */
17
+ reverse_scan(a: {
18
+ root: string;
19
+ maxFlagged?: number;
20
+ }): Promise<{
21
+ root: string;
22
+ isGit: boolean;
23
+ scanned: number;
24
+ droppedUntracked: number;
25
+ candidates: number;
26
+ anchored: number;
27
+ unanchored: number;
28
+ clusters: import("../../reverse/cluster").Cluster[];
29
+ testClusters: import("../../reverse/cluster").Cluster[];
30
+ coverage: {
31
+ structurallyResolved: number;
32
+ inTestClusters: number;
33
+ cohesionUnmeasured: number;
34
+ testsMatched: number;
35
+ testsUnmatched: number;
36
+ withDynamicIndicators: number;
37
+ indicators: Partial<Record<import("../../reverse/dynamic-wiring").Indicator, number>>;
38
+ uncalibratedIndicatorLanguages: string[];
39
+ ignoreUnsupportedPatterns: string[];
40
+ };
41
+ needsHumanConfirmation: import("../../reverse/scan").FlaggedFile[];
42
+ tests: import("../../reverse/test-map").TestMapping;
43
+ }>;
44
+ /**
45
+ * Draft H-SPEC/A-SPEC/T-SPEC documents for the recovered clusters under an EXISTING parent REQ.
46
+ *
47
+ * The REQ is the human's to write and this tool refuses without one — a requirement states
48
+ * business intent, which is not in the code. Everything emitted is `status: draft`, and writing
49
+ * requires an explicit `dryRun: false`.
50
+ *
51
+ * WHERE THE DOCUMENTS LAND: in the SERVER's configured spec store (`HOLMES_SPECS`, default
52
+ * `.ax/specs` relative to the server's working directory) — not inside `root`. In the intended
53
+ * adoption the two are the same directory, because the server runs inside the target it governs;
54
+ * pointing `root` at a different repository drafts that repository's slices into THIS store,
55
+ * which is a governance decision the caller has to make deliberately.
56
+ */
57
+ reverse_draft(a: {
58
+ root: string;
59
+ parentReqId?: string;
60
+ cluster?: string;
61
+ dryRun?: boolean;
62
+ }): Promise<{
63
+ ok: boolean;
64
+ reason: string;
65
+ availableClusters?: undefined;
66
+ dryRun?: undefined;
67
+ parentReqId?: undefined;
68
+ clusters?: undefined;
69
+ drafted?: undefined;
70
+ skipped?: undefined;
71
+ refused?: undefined;
72
+ } | {
73
+ ok: boolean;
74
+ reason: string;
75
+ availableClusters: string[];
76
+ dryRun?: undefined;
77
+ parentReqId?: undefined;
78
+ clusters?: undefined;
79
+ drafted?: undefined;
80
+ skipped?: undefined;
81
+ refused?: undefined;
82
+ } | {
83
+ ok: boolean;
84
+ dryRun: boolean;
85
+ parentReqId: string;
86
+ clusters: number;
87
+ drafted: DraftResult[];
88
+ skipped: {
89
+ clusterKey: string;
90
+ reason: string;
91
+ }[];
92
+ refused: {
93
+ clusterKey: string;
94
+ reason: string;
95
+ findings?: unknown[];
96
+ }[];
97
+ reason?: undefined;
98
+ availableClusters?: undefined;
99
+ }>;
100
+ /**
101
+ * Insert `@implements` anchors into source files. Dry-run by DEFAULT, and anchoring to a
102
+ * non-approved A-SPEC is refused — a reverse-engineered draft describes code; description does
103
+ * not confer approval.
104
+ */
105
+ reverse_anchor(a: {
106
+ root: string;
107
+ mapping: AnchorMapping[];
108
+ dryRun?: boolean;
109
+ }): Promise<{
110
+ blockers: Record<string, string>;
111
+ applied: number;
112
+ dryRun: boolean;
113
+ would: number;
114
+ failed: import("../../reverse/anchor").RefusedAnchor[];
115
+ edits: import("../../reverse/anchor").AnchorEdit[];
116
+ refused: import("../../reverse/anchor").RefusedAnchor[];
117
+ }>;
118
+ };
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createAdoptionHandlers = createAdoptionHandlers;
4
+ const spec_store_1 = require("../../spec/spec-store");
5
+ const validator_1 = require("../../spec/validator");
6
+ const legacy_format_1 = require("../../spec/legacy-format");
7
+ const approval_blockers_1 = require("../../spec/approval-blockers");
8
+ const scan_1 = require("../../reverse/scan");
9
+ const draft_1 = require("../../reverse/draft");
10
+ const anchor_1 = require("../../reverse/anchor");
11
+ function createAdoptionHandlers(context) {
12
+ return {
13
+ // @implements A-SPEC-126
14
+ // Brownfield reverse engineering. Three explicitly-invoked tools, nothing running as a side
15
+ // effect of an ordinary session, and every write path opt-in: scan writes nothing at all, draft
16
+ // and anchor are dry-run by DEFAULT.
17
+ /**
18
+ * Read-only inventory of a brownfield target: candidate clusters, coverage, and what the scan
19
+ * could not resolve. Deliberately does NOT call assertRepoTopLevel — a target that is not a git
20
+ * repository is a supported case here, reported as `isGit: false`.
21
+ */
22
+ async reverse_scan(a) {
23
+ // @implements A-SPEC-189 §10 (round 11) — `reverse_scan`/`reverse_draft` never call
24
+ // `projectRootOf`, so §7's refusal marker never reached them and their own POINTED sentence
25
+ // ("<root> is not a directory") arrived at the wire as a raw -32603 fault. A refusal about the
26
+ // caller's own argument is a refusal wherever it is authored.
27
+ context.assertReadableRoot('reverse_scan', a.root);
28
+ // `surfaceByCluster` is drafting evidence, not reading material. On the calibration target it
29
+ // is 11,460 characters across 15 clusters, and this response enters context on every scan —
30
+ // paying that to READ what only DRAFTING consumes. `reverse_draft` reads it in-process instead.
31
+ const { surfaceByCluster: _drafting, ...response } = (0, scan_1.reverseScan)(a.root, { maxFlagged: a.maxFlagged });
32
+ return response;
33
+ },
34
+ /**
35
+ * Draft H-SPEC/A-SPEC/T-SPEC documents for the recovered clusters under an EXISTING parent REQ.
36
+ *
37
+ * The REQ is the human's to write and this tool refuses without one — a requirement states
38
+ * business intent, which is not in the code. Everything emitted is `status: draft`, and writing
39
+ * requires an explicit `dryRun: false`.
40
+ *
41
+ * WHERE THE DOCUMENTS LAND: in the SERVER's configured spec store (`HOLMES_SPECS`, default
42
+ * `.ax/specs` relative to the server's working directory) — not inside `root`. In the intended
43
+ * adoption the two are the same directory, because the server runs inside the target it governs;
44
+ * pointing `root` at a different repository drafts that repository's slices into THIS store,
45
+ * which is a governance decision the caller has to make deliberately.
46
+ */
47
+ async reverse_draft(a) {
48
+ context.assertReadableRoot('reverse_draft', a.root);
49
+ if (typeof a.parentReqId !== 'string' || a.parentReqId.trim() === '') {
50
+ return {
51
+ ok: false,
52
+ reason: 'parentReqId is required. A REQ states business intent, which does not exist in the code ' +
53
+ 'and cannot be recovered from it — write the REQ first, then re-run with its id.',
54
+ };
55
+ }
56
+ const parentReqId = a.parentReqId.trim();
57
+ const specs = await context.listSpecs();
58
+ const req = specs.find((s) => s.id === parentReqId);
59
+ // An unresolvable parent would emit an orphan H-SPEC: a chain that only looks complete.
60
+ if (!req)
61
+ return { ok: false, reason: `parent REQ ${parentReqId} was not found in the spec store — create it first` };
62
+ // @implements A-SPEC-184
63
+ // A document with no `type:` is not a wrong-kind parent — it is an older-format one, and saying
64
+ // "is a undefined" left the adopter holding a visible document with nowhere to go. When the type
65
+ // IS declared, `legacyMessage` returns null and the original wording stands: that path was
66
+ // already accurate and REQ-184 does not touch it.
67
+ const formatWhy = (0, legacy_format_1.legacyMessage)(req);
68
+ if (formatWhy)
69
+ return { ok: false, reason: formatWhy };
70
+ if (req.type !== 'REQ')
71
+ return { ok: false, reason: `${parentReqId} is a ${req.type}; drafts must hang off a REQ` };
72
+ const report = (0, scan_1.reverseScan)(a.root);
73
+ // A cluster already drafted is recognised by the `reverse_cluster` key its documents carry, so
74
+ // re-running over the same tree never duplicates a slice.
75
+ const alreadyDrafted = new Set(specs.map((s) => s.frontmatter.reverse_cluster).filter((k) => typeof k === 'string'));
76
+ const selected = a.cluster ? report.clusters.filter((c) => (0, draft_1.clusterKeyOf)(c) === a.cluster) : report.clusters;
77
+ // A key that matches nothing must not read as success: "drafted 0" is indistinguishable from
78
+ // "everything was already drafted", so a mistyped key would silently skip the work.
79
+ if (a.cluster && selected.length === 0) {
80
+ const keys = report.clusters.map(draft_1.clusterKeyOf);
81
+ return {
82
+ ok: false,
83
+ reason: `no cluster matches key "${a.cluster}" — reverse_scan reports ${keys.length} cluster(s)`,
84
+ availableClusters: keys.slice(0, 50),
85
+ };
86
+ }
87
+ let base = (0, draft_1.nextIdBase)(specs.map((s) => s.id));
88
+ const drafted = [];
89
+ const skipped = [];
90
+ const refused = [];
91
+ for (const cluster of selected) {
92
+ const key = (0, draft_1.clusterKeyOf)(cluster);
93
+ if (alreadyDrafted.has(key)) {
94
+ skipped.push({ clusterKey: key, reason: 'already drafted' });
95
+ continue;
96
+ }
97
+ const d = (0, draft_1.draftSpecs)(cluster, req.id, String(base), {
98
+ surface: report.surfaceByCluster[key] ?? [],
99
+ testFiles: cluster.testFiles ?? [],
100
+ testsUnmatched: report.coverage.testsUnmatched,
101
+ });
102
+ if (!d.ok) {
103
+ refused.push({ clusterKey: key, reason: d.reason });
104
+ continue;
105
+ }
106
+ // Validate BEFORE writing: a draft that fails the project's own validator is not evidence of
107
+ // anything, and emitting one would put a broken document into the chain.
108
+ const known = [...specs, ...drafted.flatMap((x) => x.specs), ...d.specs];
109
+ const resolve = (id) => known.find((s) => s.id === id) ?? null;
110
+ const errors = d.specs.flatMap((s) => (0, validator_1.validateSpec)(s, resolve).findings.filter((f) => f.level === 'error').map((f) => ({ spec: s.id, ...f })));
111
+ if (errors.length) {
112
+ refused.push({ clusterKey: key, reason: 'drafted documents failed validation', findings: errors });
113
+ continue;
114
+ }
115
+ drafted.push(d);
116
+ base++;
117
+ }
118
+ const dryRun = a.dryRun !== false; // opt IN to writing
119
+ // @implements A-SPEC-188 — the minted id sits above every PARSEABLE id, but a file list()
120
+ // cannot read is invisible to nextIdBase, so its path can collide with a fresh draft. Same
121
+ // rule as spec_create: a path already occupied is a human's to look at, never overwritten.
122
+ // The whole cluster moves to `refused` — a partially-written chain only looks complete.
123
+ // Judged in the DRY RUN too (round-3): a preview that lists a cluster as drafted which the
124
+ // real run would refuse is a preview that lies.
125
+ const written = [];
126
+ for (const d of drafted) {
127
+ const clash = context.occupiedTarget(d.specs);
128
+ if (clash) {
129
+ refused.push({ clusterKey: d.clusterKey, reason: `초안 목적지(${clash})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다. 사람이 확인해 옮기거나 고친 뒤 다시 실행하십시오.` });
130
+ continue;
131
+ }
132
+ if (!dryRun) {
133
+ try {
134
+ for (const s of d.specs)
135
+ await context.writeSpec(s);
136
+ }
137
+ catch (e) {
138
+ if (e instanceof spec_store_1.TargetPathOccupiedError) {
139
+ refused.push({ clusterKey: d.clusterKey, reason: `초안 목적지(${e.occupiedPath})에 스토어가 읽지 못하는 파일이 이미 있습니다 — 덮어쓰지 않습니다. 사람이 확인해 옮기거나 고친 뒤 다시 실행하십시오.` });
140
+ continue;
141
+ }
142
+ throw e;
143
+ }
144
+ }
145
+ written.push(d);
146
+ }
147
+ // @implements A-SPEC-299
148
+ // Deliberately NOT changed. This looked like the same false pass as spec_slice_approve, but an
149
+ // approved store-integrity test pins `ok: true` for a run that refused every cluster because a
150
+ // destination held an unreadable file: nothing was written, nothing was damaged, and `ok` here
151
+ // means the call completed without harm. That is a defensible contract, and the case against
152
+ // it was analogy rather than an observed failure. Evidence beats symmetry.
153
+ return { ok: true, dryRun, parentReqId: req.id, clusters: report.clusters.length, drafted: written, skipped, refused };
154
+ },
155
+ /**
156
+ * Insert `@implements` anchors into source files. Dry-run by DEFAULT, and anchoring to a
157
+ * non-approved A-SPEC is refused — a reverse-engineered draft describes code; description does
158
+ * not confer approval.
159
+ */
160
+ async reverse_anchor(a) {
161
+ const specs = await context.listSpecs();
162
+ const approved = specs.filter((s) => s.type === 'A-SPEC' && s.status === 'approved').map((s) => s.id);
163
+ const plan = (0, anchor_1.planAnchors)(a.root, a.mapping ?? [], approved);
164
+ // @implements A-SPEC-182
165
+ // The third place the harness refuses over an unapproved A-SPEC. Enriched HERE rather than
166
+ // inside planAnchors, which is a pure planner holding only the approved-id list — this is the
167
+ // boundary that has the spec objects, so the planner stays testable without them.
168
+ const byId = new Map(specs.map((s) => [s.id, s]));
169
+ // ONE payload per distinct A-SPEC, on a sibling field — not appended to every refused entry.
170
+ // Review measured the first attempt, which memoized only the COMPUTATION: 149 files mapped to
171
+ // one draft A-SPEC still concatenated the same 361-character sentence 149 times, 48 KB of pure
172
+ // duplication in a single tool result. The comment claimed a property the code did not have,
173
+ // which is how it survived a round of review — hence the size assertion in the test.
174
+ const blockers = {};
175
+ for (const r of plan.refused) {
176
+ if (!/is not approved$/.test(r.reason) || blockers[r.aspec] !== undefined)
177
+ continue;
178
+ const why = (0, approval_blockers_1.blockerSummary)(byId.get(r.aspec), (id) => byId.get(id) ?? null, r.aspec);
179
+ if (why)
180
+ blockers[r.aspec] = why.trim();
181
+ }
182
+ const applied = (0, anchor_1.applyAnchors)(a.root, plan.edits, { dryRun: a.dryRun !== false });
183
+ return { ...plan, ...applied, blockers };
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,100 @@
1
+ import { type DerivedRebuild } from '../../spec/entity-integration-discharge';
2
+ import { EntityIntegrationPlan } from '../../spec/entity-integration-transaction';
3
+ export type IntegrationSealTransition = {
4
+ kind: 'spec-unsealed';
5
+ id: string;
6
+ } | {
7
+ kind: 'spec-approved';
8
+ id: string;
9
+ digest: string;
10
+ };
11
+ /**
12
+ * @implements A-SPEC-635 — the seal transitions an integration performs, derived from its plan: every
13
+ * `unseal` step withdraws a seal, every `approve` step issues one whose digest is read from the
14
+ * published document bytes and must be that document's own `specDigest` (otherwise the seal is not
15
+ * what the transition policy computed → `audit-conflict`). Through the public tool a forged step is
16
+ * already refused `plan-changed` by step revalidation; this check is the audit's own invariant.
17
+ */
18
+ /**
19
+ * @implements A-SPEC-645
20
+ * The `entity:`/`old-id:`/`new-id:` inputs for every mapping step whose record changes its display id,
21
+ * so an integration's renumbers are readable from the ledger like entity_renumber's. Without them the
22
+ * evidence consumers had no way to follow a document an integration renamed.
23
+ */
24
+ export declare function renumberedMappingInputs(steps: ReadonlyArray<{
25
+ phase: string;
26
+ entityId?: string;
27
+ change: {
28
+ locator: string;
29
+ before: string | null;
30
+ after: string | null;
31
+ };
32
+ }>, renames?: ReadonlyArray<{
33
+ entityId: string;
34
+ oldDisplayId: string;
35
+ newDisplayId: string;
36
+ }>): string[];
37
+ export declare function integrationSealTransitions(operation: EntityIntegrationPlan): IntegrationSealTransition[];
38
+ export declare function createEntityIntegrationHandlers(context: {
39
+ storeRoot?: string;
40
+ rebuildFor?: (root: string) => DerivedRebuild;
41
+ }): {
42
+ entity_integrate(a: {
43
+ root?: string;
44
+ operation?: string;
45
+ base?: string;
46
+ left?: string;
47
+ right?: string;
48
+ operationId?: string;
49
+ plan?: unknown;
50
+ }): Promise<{
51
+ ok: true;
52
+ state: "completed";
53
+ operationId: string;
54
+ } | {
55
+ ok: true;
56
+ state: "abandoned";
57
+ operationId: string;
58
+ restored: string[];
59
+ untouched: string[];
60
+ preserved: string[];
61
+ } | {
62
+ ok: true;
63
+ record: import("../../spec/entity-integration-discharge").DerivedDischargeRecord;
64
+ alreadyDischarged: boolean;
65
+ } | {
66
+ ok: boolean;
67
+ obligations: import("../../spec/entity-integration-discharge").DerivedObligationStatus[];
68
+ plan?: undefined;
69
+ target?: undefined;
70
+ executable?: undefined;
71
+ code?: undefined;
72
+ reason?: undefined;
73
+ } | {
74
+ ok: boolean;
75
+ plan: import("../../spec/entity-integration-transaction").EntityIntegrationRecoveryInspection;
76
+ target: string;
77
+ executable: boolean;
78
+ obligations?: undefined;
79
+ code?: undefined;
80
+ reason?: undefined;
81
+ } | {
82
+ blockers: string[];
83
+ target?: string | undefined;
84
+ plan?: EntityIntegrationPlan | undefined;
85
+ preview: import("../../spec/entity-integration-transaction").EntityIntegrationCandidate;
86
+ ok: boolean;
87
+ executable: boolean;
88
+ obligations?: undefined;
89
+ code?: undefined;
90
+ reason?: undefined;
91
+ } | {
92
+ ok: boolean;
93
+ code: string;
94
+ reason: string;
95
+ obligations?: undefined;
96
+ plan?: undefined;
97
+ target?: undefined;
98
+ executable?: undefined;
99
+ }>;
100
+ };