@holmes-lab/holmes-kit 0.18.0 → 0.19.2

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 (81) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +3 -0
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/agents.d.ts +22 -0
  5. package/dist/holmes/cli/agents.js +76 -1
  6. package/dist/holmes/cli/approve.js +6 -1
  7. package/dist/holmes/cli/doctor.d.ts +36 -1
  8. package/dist/holmes/cli/doctor.js +182 -35
  9. package/dist/holmes/cli/index.js +7 -1
  10. package/dist/holmes/cli/init.js +12 -0
  11. package/dist/holmes/cli/native-deps.d.ts +65 -0
  12. package/dist/holmes/cli/native-deps.js +131 -0
  13. package/dist/holmes/cli/release-docs.d.ts +27 -0
  14. package/dist/holmes/cli/release-docs.js +68 -0
  15. package/dist/holmes/cpg/arch-observe.d.ts +15 -0
  16. package/dist/holmes/cpg/arch-observe.js +19 -0
  17. package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
  18. package/dist/holmes/cpg/cpg-scanner.js +27 -3
  19. package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
  20. package/dist/holmes/cpg/cycle-detect.js +251 -0
  21. package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
  22. package/dist/holmes/cpg/cycle-observation.js +146 -0
  23. package/dist/holmes/cpg/scan-cache.d.ts +1 -1
  24. package/dist/holmes/cpg/scanned-file.d.ts +36 -0
  25. package/dist/holmes/cpg/scanned-file.js +2 -0
  26. package/dist/holmes/governance/approval-queue.d.ts +23 -4
  27. package/dist/holmes/governance/approval-queue.js +44 -6
  28. package/dist/holmes/governance/constitution.d.ts +20 -0
  29. package/dist/holmes/governance/constitution.js +17 -0
  30. package/dist/holmes/governance/ledger-store.d.ts +9 -0
  31. package/dist/holmes/governance/ledger-store.js +47 -0
  32. package/dist/holmes/governance/provenance-chain.d.ts +16 -1
  33. package/dist/holmes/governance/provenance-chain.js +5 -3
  34. package/dist/holmes/hooks/pre-tool-use.js +3 -1
  35. package/dist/holmes/hooks/stop.d.ts +29 -0
  36. package/dist/holmes/hooks/stop.js +119 -3
  37. package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
  38. package/dist/holmes/mcp/defuse-bound.js +8 -0
  39. package/dist/holmes/mcp/handlers.d.ts +7 -0
  40. package/dist/holmes/mcp/handlers.js +132 -6
  41. package/dist/holmes/mcp/history-admission.d.ts +15 -0
  42. package/dist/holmes/mcp/history-admission.js +37 -0
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +45 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +117 -9
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +71 -4
  47. package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
  48. package/dist/holmes/project/install-scripts-policy.js +131 -0
  49. package/dist/holmes/project/npx-bin.d.ts +6 -0
  50. package/dist/holmes/project/npx-bin.js +10 -0
  51. package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
  52. package/dist/holmes/review/evaluation-metrics.js +18 -1
  53. package/dist/holmes/review/failed-test-names.d.ts +19 -0
  54. package/dist/holmes/review/failed-test-names.js +43 -0
  55. package/dist/holmes/review/paired-power.d.ts +14 -0
  56. package/dist/holmes/review/paired-power.js +57 -0
  57. package/dist/holmes/review/replay-corpus.d.ts +11 -0
  58. package/dist/holmes/review/replay-corpus.js +34 -0
  59. package/dist/holmes/review/run-replay.d.ts +23 -0
  60. package/dist/holmes/review/run-replay.js +90 -4
  61. package/dist/holmes/review/symbol-truth.d.ts +14 -0
  62. package/dist/holmes/review/symbol-truth.js +23 -0
  63. package/dist/holmes/review/test-runner.d.ts +27 -0
  64. package/dist/holmes/review/test-runner.js +59 -3
  65. package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
  66. package/dist/holmes/rtm/defuse-symbols.js +91 -0
  67. package/dist/holmes/rtm/incremental.js +5 -0
  68. package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
  69. package/dist/holmes/rtm/rtm-builder.js +32 -5
  70. package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
  71. package/dist/holmes/rtm/rtm-graph.js +13 -0
  72. package/dist/holmes/spec/legacy-fields.d.ts +2 -0
  73. package/dist/holmes/spec/legacy-fields.js +9 -0
  74. package/dist/holmes/spec/legacy-format.d.ts +1 -1
  75. package/dist/holmes/spec/legacy-format.js +4 -1
  76. package/dist/holmes/spec/spec-parser.js +5 -3
  77. package/docs/install-guide.md +54 -5
  78. package/package.json +4 -1
  79. package/playbooks/author-slice/PLAYBOOK.md +14 -0
  80. package/playbooks/publish/PLAYBOOK.md +32 -0
  81. package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
@@ -56,7 +56,12 @@ function applyIncremental(graph, changes, ctx) {
56
56
  }
57
57
  if (!ctx.allScanned)
58
58
  return { callEdgesResolved: false };
59
+ // @implements A-SPEC-573.5 — `calls_ambiguous` is rebuilt by the same pass, so it must be cleared
60
+ // by the same pass. Missing it left a stale candidate edge behind when an ambiguity RESOLVED
61
+ // (the second definer was deleted), and incremental stopped converging with a full rebuild —
62
+ // caught by A-SPEC-280's convergence property, which is exactly what it is for.
59
63
  graph.removeEdgesByRel('calls');
64
+ graph.removeEdgesByRel('calls_ambiguous');
60
65
  (0, rtm_builder_1.addCallEdges)(ctx.allScanned(), graph, ctx.buildOptions);
61
66
  return { callEdgesResolved: true };
62
67
  }
@@ -16,6 +16,14 @@ import { ScannedFile } from '../cpg/cpg-scanner';
16
16
  */
17
17
  export interface ResolutionReport {
18
18
  resolved: number;
19
+ /**
20
+ * @implements A-SPEC-573.5
21
+ * Candidate edges kept for a call whose name is defined in more than one place. Census on this
22
+ * repository (2026-09-08): 102 qualified-name and 590 last-segment collisions were being dropped
23
+ * against 3,067 resolved edges — 22.6% of the call graph, at a mean fan-out of 2.29. They ride on
24
+ * the `calls_ambiguous` relation, which every certainty-requiring reader filters out by name.
25
+ */
26
+ ambiguousEdges: number;
19
27
  /** Callee defined in several files: precision-over-recall refused to guess. A real miss. */
20
28
  ambiguous: number;
21
29
  /** Callee defined nowhere in the scan — usually an external package or runtime builtin. */
@@ -139,7 +139,7 @@ function buildRtm(specs, scanned, graph, opts) {
139
139
  // on a 56k-insert graph (216 ms -> 173 ms with a transaction; 88 -> 52 ms with cached statements).
140
140
  // It also makes a failed build atomic — no half-graph that could be mistaken for a complete one.
141
141
  let resolution = {
142
- resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
142
+ resolved: 0, ambiguousEdges: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
143
143
  };
144
144
  graph.transaction(() => {
145
145
  // Add spec nodes and dependencies
@@ -185,7 +185,7 @@ function buildRtm(specs, scanned, graph, opts) {
185
185
  */
186
186
  function addCallEdges(scanned, graph, opts) {
187
187
  const report = {
188
- resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
188
+ resolved: 0, ambiguousEdges: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
189
189
  };
190
190
  // @implements A-SPEC-300
191
191
  // Indexes are built PER LANGUAGE FAMILY. Resolution used to look across the whole scan, so a Java
@@ -247,11 +247,37 @@ function addCallEdges(scanned, graph, opts) {
247
247
  }
248
248
  const family = familyOf(f.sourcePath);
249
249
  const table = definedIn.get(family);
250
+ // @implements A-SPEC-573.5 — an ambiguous target is a CANDIDATE SET, not a dead end. Both
251
+ // shapes of ambiguity keep every candidate on `calls_ambiguous`; `calls` still means "one
252
+ // place, certain", so nothing that reads certainty changes. Emitting nothing was buying
253
+ // precision at the cost of 22.6% of the call graph.
254
+ const ambiguousCandidates = (qn, paths) => {
255
+ for (const p of paths) {
256
+ const to = `CODE:${qn}@${p}`;
257
+ if (to === fromId)
258
+ continue; // self-recursion carries no impact, ambiguous or not
259
+ graph.addEdge(fromId, to, 'calls_ambiguous', f.sourcePath, fact(opts, `${f.sourcePath}`, 'name-resolution', null));
260
+ report.ambiguousEdges++;
261
+ }
262
+ };
250
263
  let target = null;
251
264
  if (table?.has(e.to))
252
265
  target = { qn: e.to, paths: table.get(e.to) };
253
- else
254
- target = byLastSegment.get(family)?.get(e.to) ?? null;
266
+ else {
267
+ const byLast = byLastSegment.get(family);
268
+ const hit = byLast?.get(e.to);
269
+ if (hit === null) {
270
+ // The last segment is owned by several qualified names — keep every owner as a candidate.
271
+ for (const [qn, paths] of table ?? []) {
272
+ const last = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
273
+ if (last === e.to)
274
+ ambiguousCandidates(qn, paths);
275
+ }
276
+ report.unknownTarget++;
277
+ continue;
278
+ }
279
+ target = hit ?? null;
280
+ }
255
281
  if (!target) {
256
282
  report.unknownTarget++;
257
283
  continue;
@@ -259,9 +285,10 @@ function addCallEdges(scanned, graph, opts) {
259
285
  const sameFile = target.paths.includes(f.sourcePath);
260
286
  const path = sameFile ? f.sourcePath : (target.paths.length === 1 ? target.paths[0] : null);
261
287
  if (!path) {
288
+ ambiguousCandidates(target.qn, target.paths);
262
289
  report.ambiguous++;
263
290
  continue;
264
- } // defined in several files — emit nothing
291
+ }
265
292
  const toId = `CODE:${target.qn}@${path}`;
266
293
  if (toId === fromId) {
267
294
  report.selfReference++;
@@ -136,6 +136,17 @@ export declare class RtmGraph {
136
136
  */
137
137
  maxCalleeInDegree?: number;
138
138
  }): string[];
139
+ /**
140
+ * Every resolved FILE->FILE `imports` edge, as repo-relative path pairs.
141
+ *
142
+ * Exists because cycle detection needs the whole import graph at once, and the alternative —
143
+ * parsing `dumpCanonical()` — builds a multi-megabyte string on every call to answer a question
144
+ * one query answers.
145
+ */
146
+ importEdges(): Array<{
147
+ from: string;
148
+ to: string;
149
+ }>;
139
150
  /**
140
151
  * @implements A-SPEC-289
141
152
  * Repo-relative paths of the files that import this one, via resolved FILE->FILE `imports` edges.
@@ -224,6 +224,19 @@ class RtmGraph {
224
224
  : this.callees(id).filter((callee) => this.callerCount(callee) <= limit);
225
225
  return [...new Set([...this.impactSourcesOf(id), ...downstream])].sort();
226
226
  }
227
+ // @implements A-SPEC-574.2
228
+ /**
229
+ * Every resolved FILE->FILE `imports` edge, as repo-relative path pairs.
230
+ *
231
+ * Exists because cycle detection needs the whole import graph at once, and the alternative —
232
+ * parsing `dumpCanonical()` — builds a multi-megabyte string on every call to answer a question
233
+ * one query answers.
234
+ */
235
+ importEdges() {
236
+ return this.db.prepare("SELECT DISTINCT src, dst FROM edges WHERE rel='imports' ORDER BY src ASC, dst ASC")
237
+ .all()
238
+ .map((r) => ({ from: r.src.replace(/^FILE:/, ''), to: r.dst.replace(/^FILE:/, '') }));
239
+ }
227
240
  /**
228
241
  * @implements A-SPEC-289
229
242
  * Repo-relative paths of the files that import this one, via resolved FILE->FILE `imports` edges.
@@ -0,0 +1,2 @@
1
+ /** Frontmatter key that carries a pre-migration `depends_on` value. */
2
+ export declare const LEGACY_DEPENDS_FIELD = "legacy_depends_on";
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LEGACY_DEPENDS_FIELD = void 0;
4
+ // @implements A-SPEC-574.3
5
+ // One constant, in a module neither side owns. `spec-parser` needed the field name and `legacy-format`
6
+ // needed the parser's `Spec` type, so each imported the other — a cycle held up by a single string.
7
+ // Shared things belong to a third module; that is the whole rule.
8
+ /** Frontmatter key that carries a pre-migration `depends_on` value. */
9
+ exports.LEGACY_DEPENDS_FIELD = 'legacy_depends_on';
@@ -48,7 +48,7 @@ export declare const LEGACY_STATUS_FIELD = "legacy_status";
48
48
  * computation — asserting it anyway is the fabrication this repository keeps removing. Verbatim
49
49
  * preservation leaves the judgement to a person who can actually make it.
50
50
  */
51
- export declare const LEGACY_DEPENDS_FIELD = "legacy_depends_on";
51
+ export { LEGACY_DEPENDS_FIELD } from './legacy-fields';
52
52
  /**
53
53
  * Classify a document by FORMAT, not by validity.
54
54
  *
@@ -21,7 +21,10 @@ exports.LEGACY_STATUS_FIELD = 'legacy_status';
21
21
  * computation — asserting it anyway is the fabrication this repository keeps removing. Verbatim
22
22
  * preservation leaves the judgement to a person who can actually make it.
23
23
  */
24
- exports.LEGACY_DEPENDS_FIELD = 'legacy_depends_on';
24
+ // @implements A-SPEC-574.3 moved to `legacy-fields` and re-exported, so existing importers
25
+ // keep working while the cycle it created is gone.
26
+ var legacy_fields_1 = require("./legacy-fields");
27
+ Object.defineProperty(exports, "LEGACY_DEPENDS_FIELD", { enumerable: true, get: function () { return legacy_fields_1.LEGACY_DEPENDS_FIELD; } });
25
28
  /**
26
29
  * Classify a document by FORMAT, not by validity.
27
30
  *
@@ -37,7 +37,9 @@ exports.parseDependsOn = parseDependsOn;
37
37
  exports.parseSpec = parseSpec;
38
38
  exports.serializeSpec = serializeSpec;
39
39
  const yaml = __importStar(require("js-yaml"));
40
- const legacy_format_1 = require("./legacy-format");
40
+ // @implements A-SPEC-574.3 — from the shared module, not from `legacy-format`, which imports
41
+ // this file's `Spec` type. That pair was a cycle over one string constant.
42
+ const legacy_fields_1 = require("./legacy-fields");
41
43
  /**
42
44
  * Read `depends_on` in every spelling the corpus contains, keeping what cannot be represented.
43
45
  *
@@ -121,8 +123,8 @@ function parseSpec(input) {
121
123
  const deps = parseDependsOn(fm.depends_on);
122
124
  // @implements A-SPEC-221 — never overwrite an existing preservation: a second round trip must not
123
125
  // chew up what the first one saved, the same rule `legacy_status` follows.
124
- if (deps.legacy !== null && fm[legacy_format_1.LEGACY_DEPENDS_FIELD] === undefined) {
125
- fm[legacy_format_1.LEGACY_DEPENDS_FIELD] = deps.legacy;
126
+ if (deps.legacy !== null && fm[legacy_fields_1.LEGACY_DEPENDS_FIELD] === undefined) {
127
+ fm[legacy_fields_1.LEGACY_DEPENDS_FIELD] = deps.legacy;
126
128
  }
127
129
  return {
128
130
  id: String(fm.id ?? ''),
@@ -91,19 +91,63 @@ Your npm metadata cache predates the release — measured minutes after publishi
91
91
  registry already listed the version while a default-cache install still refused it. Add
92
92
  `--prefer-online`, or retry in a few minutes.
93
93
 
94
+ ### npm 12: `better-sqlite3` has no binary and nothing failed
95
+
96
+ npm 12 (and npm ≥ 11.19) **blocks dependency install scripts by default** and skips them
97
+ silently: `npm ci` exits 0, `better-sqlite3` never runs `prebuild-install`, and the first
98
+ `require` dies with "Could not locate the bindings file". Measured 2026-09-09 (Windows 11,
99
+ npm 12.0.1, Node 24.19.0). `npx holmes-kit doctor` names this cause as `scripts-blocked` and prints
100
+ the commands below; do not reinstall — a reinstall reproduces the same state.
101
+
102
+ Only **one** package needs its script: `better-sqlite3`. The 8 tree-sitter packages are also
103
+ listed as blocked, but they load from their shipped `prebuilds/` without the script (measured on
104
+ all 8), so they are deliberately NOT approved. This repository's `package.json` therefore carries
105
+
106
+ ```json
107
+ "allowScripts": { "better-sqlite3@12.11.1": true }
108
+ ```
109
+
110
+ pinned to the lockfile version, so a dependency bump forces a fresh review (a test fails until the
111
+ pin is updated). Recovery, by how you installed — on Windows use the `.cmd` spellings (see below):
112
+
113
+ | Layout | Commands |
114
+ |---|---|
115
+ | A project that depends on holmes-kit | `npm install-scripts approve better-sqlite3@12.11.1` then `npm rebuild better-sqlite3 --foreground-scripts` (from the project root; this writes the pin into YOUR package.json) |
116
+ | This repository checkout | already approved — `npm rebuild better-sqlite3 --foreground-scripts` if the binary is missing |
117
+ | Global (`npm install -g`) | `npm rebuild -g better-sqlite3 --foreground-scripts --allow-scripts=better-sqlite3` — a per-command flag scoped to one package; no npm config is changed. Target the dependency: `rebuild -g @holmes-lab/holmes-kit` re-links the bin and dies `EEXIST` under npm 12 (measured). If this fails `EPERM` under `C:\Program Files\nodejs`, your prefix is protected — see "Before `npm install -g`" above |
118
+ | Bare `npx` | not repairable in place — do a local install and use the first row |
119
+
120
+ Never use `--dangerously-allow-all-scripts` or a global `allow-scripts` config: the point of the
121
+ policy is that only the reviewed package runs code at install time. Older npm 11 (measured 11.6.2)
122
+ ignores the `allowScripts` field and runs scripts as before; npm 11.19 honours it exactly like 12.
123
+
124
+ ### Windows: `npm`/`npx` are blocked by the PowerShell execution policy
125
+
126
+ `npm`, `npx` and `holmes-kit` resolve to `.ps1` shims first, and a `Restricted`/`AllSigned`
127
+ policy refuses them (PSSecurityException). The `.cmd` shims always run: `npm.cmd`, `npx.cmd`.
128
+ doctor emits `.cmd` commands on Windows and appends the policy note when it can observe a
129
+ blocking policy. Do not change the execution policy to fix this.
130
+
94
131
  ### `better-sqlite3` fails to build
95
132
 
96
133
  The one dependency that may need a toolchain. The 8 tree-sitter grammars ship prebuilt binaries
97
134
  (`darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`) and compile nothing; `better-sqlite3`
98
135
  downloads a prebuild at install time and **falls back to compiling** when none matches your
99
- platform and Node ABI. If it compiles, you need:
136
+ platform and Node ABI. doctor reports this as `build-failed` — with the approval in place, the
137
+ script ran and left no binary — and names what it can observe on Windows (Python on PATH, Visual
138
+ Studio C++ Build Tools, a space in the install path). It cannot observe a failed prebuild
139
+ download; rerun with `--foreground-scripts` to see the script's own output. If it compiles, you
140
+ need:
100
141
 
101
142
  | Platform | Toolchain |
102
143
  |---|---|
103
- | Windows | Visual Studio Build Tools (C++ workload) |
144
+ | Windows | Visual Studio Build Tools (C++ workload) + Python 3 on PATH; prefer an install path without spaces |
104
145
  | macOS | Xcode Command Line Tools (`xcode-select --install`) |
105
146
  | Alpine | `apk add --no-cache python3 make g++` |
106
147
 
148
+ A binary that exists but fails with `NODE_MODULE_VERSION` was built for another Node — doctor
149
+ calls that `abi-mismatch`; rebuild against the Node you run.
150
+
107
151
  ### `spawn sh ENOENT` during a git-URL install
108
152
 
109
153
  `npm i -g git+ssh://…` is not a supported path: npm 11 clones the repository into its cache and
@@ -117,15 +161,20 @@ npx holmes-kit doctor # local install
117
161
  holmes-kit doctor # global install
118
162
  ```
119
163
 
120
- Expect `10 pass, 1 warn, 0 fail` on a healthy install. The lines that matter most:
164
+ Expect `0 fail` on a healthy install (a few `warn` lines are normal — measured `12 pass, 4 warn,
165
+ 0 fail` on a Windows source checkout). The lines that matter most:
121
166
 
122
167
  - `global prefix` — whether `-g` would work on this machine, and the remedy if not
123
- - `tree-sitter grammars` / `better-sqlite3` whether the native modules actually load
168
+ - `tree-sitter grammars` every grammar actually PARSES in a fresh process (`8 grammars parse`)
169
+ - `better-sqlite3` — loads and runs a `:memory:` query; on FAIL the detail names the cause
170
+ (`scripts-blocked`, `abi-mismatch`, `build-failed`, or `unknown` with the raw error) and the fix
171
+ is the exact command for your install layout
124
172
 
125
173
  ## What we deliberately do NOT do
126
174
 
127
175
  | Idea | Why not |
128
176
  |---|---|
129
- | A `postinstall` script that prints guidance | Triggers npm 11's `allow-scripts` warning and forfeits this package's current property of running no install scripts at all |
177
+ | A `postinstall` script that prints guidance | Under npm 12 it would be blocked like any other install script, and it forfeits this package's property of running no install scripts of its own |
178
+ | Approving the tree-sitter grammars in `allowScripts` | Measured unnecessary — they load from shipped prebuilds — and every extra approval is code that runs at install time |
130
179
  | Recommending `npx @holmes-lab/holmes-kit init` with no install | `init` writes wiring with absolute paths; under bare `npx` those point into the npx cache and break when it is pruned |
131
180
  | Fixing your npm prefix from inside the package | A package rewriting your npm configuration is exactly the supply-chain behaviour this guide warns about |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.18.0",
4
+ "version": "0.19.2",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",
@@ -85,5 +85,8 @@
85
85
  },
86
86
  "publishConfig": {
87
87
  "access": "public"
88
+ },
89
+ "allowScripts": {
90
+ "better-sqlite3@12.11.1": true
88
91
  }
89
92
  }
@@ -136,3 +136,17 @@ T-SPEC 필수 섹션은 4분면 그대로다: **Normal · Corner · Negative ·
136
136
  번호가 A-SPEC과 T-SPEC에 한정되는 것, 그리고 스펙 폴더 밑 `.ts`가 author 게이트로 강등되지 않는다는 사실. 번호
137
137
  상속 관습은 엔진이 강제하지 않으므로 테스트도 고정하지 않는다 — 관습은 관습이라 말한다. 변이
138
138
  검사로 판별력을 증명한 뒤 신뢰한다.
139
+
140
+ ### 순환 의존을 만들지 않는다
141
+
142
+ 두 모듈이 서로를 import 하면 순환이다. **설계 단계에서 피하는 것이 유일하게 값싼 시점이다** ——
143
+ 코드가 쓰인 뒤에는 공유 타입 추출이나 지연 `require()` 워크어라운드로만 풀 수 있고, 후자는
144
+ 순환을 숨길 뿐 없애지 않는다.
145
+
146
+ - **공유 타입은 별도 모듈로 뺀다.** A 와 B 가 같은 타입을 필요로 하면 그 타입은 A 도 B 도 아닌
147
+ 세 번째 모듈에 있어야 한다.
148
+ - **타입만 필요하면 `import type` 을 쓴다.** TypeScript 가 방출에서 지우므로 런타임 순환이 되지
149
+ 않는다. 값 import 로 두면 타입만 쓰면서도 순환을 만든다.
150
+ - **지연 `require()` 로 순환을 우회하지 않는다.** 그것은 수리가 아니라 청구서 이연이다.
151
+
152
+ 이 저장소는 **스펙 그래프의 무순환을 ART-2 로 집행**한다. 코드 그래프도 같은 기준을 향한다.
@@ -96,6 +96,38 @@ npm publish --access public
96
96
 
97
97
  ---
98
98
 
99
+ ### 6단계: 외부 문서 표면 (External Docs Reach) — 발행이 닿는 곳까지 정직하게
100
+
101
+ npm 만 갱신하고 끝나면, 사람들이 실제로 읽는 문서는 낡은 채로 남는다. **사고 이력**: 이 저장소에는
102
+ GitHub 리모트가 없어(origin 이 로컬 gitea) 0.16.0·0.17.0·0.18.0 **세 릴리스 동안** GitHub README 가
103
+ 한 번도 갱신되지 않았고, `package.json` 의 `homepage` 가 바로 그 문서를 가리킨다.
104
+
105
+ **발행이 성공한 뒤에만** 실행한다 — 실패한 릴리스의 문서를 최신이라고 주장하지 않는다.
106
+
107
+ 1. **대상 파생 (하드코딩 금지)**: `package.json` 의 `repository` 에서 `<owner>/<repo>` 를 얻는다
108
+ (`repoTargetFrom`). 이 플레이북은 **소비 프로젝트에도 설치**되므로 특정 저장소를 박아 두면 남의
109
+ 릴리스가 그 저장소를 덮어쓴다. 파생 실패 → **SKIP(사유: `repository` 필드 없음/비-GitHub)**.
110
+ 2. **덮어쓸 것을 먼저 본다**: `gh api repos/<owner>/<repo>/contents/README.md` 로 원격을 읽어
111
+ 로컬과의 차이를 **보고**한다. 원격이 분기했다면 동기화는 남의 편집을 지우는 행위다 — 사람이
112
+ 알고 결정해야 한다.
113
+ 3. **동기화**: 로컬 `README.md` 를 `gh api --method PUT` 으로 올린다(`sha` 는 2 에서 읽은 값).
114
+ 4. **재조회 검증**: 다시 읽어 로컬과 일치하는지 확인한다. **쓴 것과 남은 것은 다를 수 있다** —
115
+ 검증 없는 "갱신했다"는 주장이지 사실이 아니다.
116
+ 5. **profile README drift 감지 (자동 수정 금지)**: `<owner>/.github` 의 `profile/README.md` 를 읽어
117
+ `profileDriftFindings` 로 주장-현실 불일치를 **보고만** 한다. 포지셔닝 문안은 오너 결정이며
118
+ 릴리스 절차가 정할 것이 아니다.
119
+ 6. **SKIP 은 값이지 침묵이 아니다**: `gh` 미설치 / 미인증(`gh auth status` 실패) / 권한 없음 /
120
+ `repository` 부재 — 각각 **무엇을 하지 않았는지 명시**하고 다음으로 간다. 소비 프로젝트의 발행을
121
+ 우리 편의로 막지 않는다. 다만 **조용한 성공은 금지** — 아무 말 없이 넘어가면 그것은 3단계
122
+ 전부를 한 것처럼 읽힌다.
123
+
124
+ > [!CAUTION]
125
+ > 2.5단계가 로컬 문서에 적용하는 규율("drift 도 누락도 거짓 주장이다")은 **외부 표면에도** 적용된다.
126
+ > 다른 점은 하나뿐이다: repo README 는 로컬의 사본이라 **동기화**하고, profile README 는 독립
127
+ > 문서라 **감지·보고**한다. 무엇이 정본인가가 처방을 정한다.
128
+
129
+ ---
130
+
99
131
  ## 플레이북 트리거 조건
100
132
  - "npm publish"
101
133
  - "release to npm"
@@ -85,3 +85,17 @@ holmes는 기계적으로 판별한다: `red-error`로는 red→green 시퀀스
85
85
 
86
86
  `src/holmes/playbooks/tdd-slice.test.ts` — 이 스킬이 집행 태그(ART-1/4/8)와 red-assertion/red-error
87
87
  구별을 담고, 설치기가 이를 발견함을 고정한다. 상시 스위트 포함.
88
+
89
+ ### 순환 의존을 만들지 않는다
90
+
91
+ 두 모듈이 서로를 import 하면 순환이다. **설계 단계에서 피하는 것이 유일하게 값싼 시점이다** ——
92
+ 코드가 쓰인 뒤에는 공유 타입 추출이나 지연 `require()` 워크어라운드로만 풀 수 있고, 후자는
93
+ 순환을 숨길 뿐 없애지 않는다.
94
+
95
+ - **공유 타입은 별도 모듈로 뺀다.** A 와 B 가 같은 타입을 필요로 하면 그 타입은 A 도 B 도 아닌
96
+ 세 번째 모듈에 있어야 한다.
97
+ - **타입만 필요하면 `import type` 을 쓴다.** TypeScript 가 방출에서 지우므로 런타임 순환이 되지
98
+ 않는다. 값 import 로 두면 타입만 쓰면서도 순환을 만든다.
99
+ - **지연 `require()` 로 순환을 우회하지 않는다.** 그것은 수리가 아니라 청구서 이연이다.
100
+
101
+ 이 저장소는 **스펙 그래프의 무순환을 ART-2 로 집행**한다. 코드 그래프도 같은 기준을 향한다.