@holmes-lab/holmes-kit 0.18.0 → 0.19.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 +69 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +68 -0
- package/dist/holmes/cpg/arch-observe.d.ts +15 -0
- package/dist/holmes/cpg/arch-observe.js +19 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
- package/dist/holmes/cpg/cpg-scanner.js +27 -3
- package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
- package/dist/holmes/cpg/cycle-detect.js +251 -0
- package/dist/holmes/cpg/scan-cache.d.ts +1 -1
- package/dist/holmes/cpg/scanned-file.d.ts +36 -0
- package/dist/holmes/cpg/scanned-file.js +2 -0
- package/dist/holmes/governance/constitution.d.ts +20 -0
- package/dist/holmes/governance/constitution.js +17 -0
- package/dist/holmes/governance/ledger-store.d.ts +9 -0
- package/dist/holmes/governance/ledger-store.js +47 -0
- package/dist/holmes/governance/provenance-chain.d.ts +16 -1
- package/dist/holmes/governance/provenance-chain.js +5 -3
- package/dist/holmes/hooks/pre-tool-use.js +3 -1
- package/dist/holmes/hooks/stop.d.ts +14 -0
- package/dist/holmes/hooks/stop.js +73 -0
- package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
- package/dist/holmes/mcp/defuse-bound.js +8 -0
- package/dist/holmes/mcp/handlers.d.ts +5 -0
- package/dist/holmes/mcp/handlers.js +103 -4
- package/dist/holmes/mcp/history-admission.d.ts +15 -0
- package/dist/holmes/mcp/history-admission.js +37 -0
- package/dist/holmes/mcp/maintenance-analyze.d.ts +8 -0
- package/dist/holmes/mcp/maintenance-analyze.js +44 -8
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/paired-power.d.ts +14 -0
- package/dist/holmes/review/paired-power.js +57 -0
- package/dist/holmes/review/replay-corpus.d.ts +11 -0
- package/dist/holmes/review/replay-corpus.js +34 -0
- package/dist/holmes/review/run-replay.js +60 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
- package/dist/holmes/rtm/defuse-symbols.js +91 -0
- package/dist/holmes/rtm/incremental.js +5 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +32 -5
- package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/spec/legacy-fields.d.ts +2 -0
- package/dist/holmes/spec/legacy-fields.js +9 -0
- package/dist/holmes/spec/legacy-format.d.ts +1 -1
- package/dist/holmes/spec/legacy-format.js +4 -1
- package/dist/holmes/spec/spec-parser.js +5 -3
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +14 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SymbolRange {
|
|
2
|
+
name: string;
|
|
3
|
+
startLine: number;
|
|
4
|
+
endLine: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The symbols a commit's changed lines fall inside, in symbol-list order, deduped.
|
|
8
|
+
*
|
|
9
|
+
* A change that lands outside every symbol (an import line, a top-level constant) yields nothing —
|
|
10
|
+
* and a case with no symbol truth drops OUT of the symbol denominator rather than scoring zero. An
|
|
11
|
+
* unanswerable case is not a failed one, which is the same rule `evaluationMetrics` already applies
|
|
12
|
+
* to files.
|
|
13
|
+
*/
|
|
14
|
+
export declare function symbolsTouched(symbols: readonly SymbolRange[], changedLines: readonly number[]): string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.symbolsTouched = symbolsTouched;
|
|
4
|
+
/**
|
|
5
|
+
* The symbols a commit's changed lines fall inside, in symbol-list order, deduped.
|
|
6
|
+
*
|
|
7
|
+
* A change that lands outside every symbol (an import line, a top-level constant) yields nothing —
|
|
8
|
+
* and a case with no symbol truth drops OUT of the symbol denominator rather than scoring zero. An
|
|
9
|
+
* unanswerable case is not a failed one, which is the same rule `evaluationMetrics` already applies
|
|
10
|
+
* to files.
|
|
11
|
+
*/
|
|
12
|
+
function symbolsTouched(symbols, changedLines) {
|
|
13
|
+
if (symbols.length === 0 || changedLines.length === 0)
|
|
14
|
+
return [];
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const s of symbols) {
|
|
17
|
+
// Both ends inclusive: the signature line and the closing line are part of the symbol.
|
|
18
|
+
if (changedLines.some((line) => line >= s.startLine && line <= s.endLine) && !out.includes(s.name)) {
|
|
19
|
+
out.push(s.name);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DataFlowFacts } from '../cpg/language-parser';
|
|
2
|
+
/**
|
|
3
|
+
* Order a file's function names by how close they sit, in def-use terms, to what the request says.
|
|
4
|
+
*
|
|
5
|
+
* Three tiers, stable within each so the result is deterministic (the downstream lists are
|
|
6
|
+
* position-indexed): functions the request names directly, functions one def-use step from those,
|
|
7
|
+
* then everything else in its original order. A term matching nothing leaves the order untouched —
|
|
8
|
+
* an empty first tier must not shuffle the list.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rankSymbolsByDefUse(facts: DataFlowFacts, terms: readonly string[], order: readonly string[]): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Append what def-use suggested behind what the lexical layer matched.
|
|
13
|
+
*
|
|
14
|
+
* Never in front: this repository has already measured a re-ranker deleting the lexical answer, so
|
|
15
|
+
* a second signal rides behind the first rather than replacing it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function enrichCandidateSymbols(lexical: readonly string[], suggested: readonly string[]): string[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rankSymbolsByDefUse = rankSymbolsByDefUse;
|
|
4
|
+
exports.enrichCandidateSymbols = enrichCandidateSymbols;
|
|
5
|
+
/** Names a function mentions anywhere the walk recovered them: params, defs, and call arguments. */
|
|
6
|
+
function surfaceOf(facts) {
|
|
7
|
+
const out = new Map();
|
|
8
|
+
const push = (fn, ...words) => {
|
|
9
|
+
const bucket = out.get(fn) ?? out.set(fn, []).get(fn);
|
|
10
|
+
for (const w of words)
|
|
11
|
+
if (w !== '' && !bucket.includes(w))
|
|
12
|
+
bucket.push(w);
|
|
13
|
+
};
|
|
14
|
+
for (const p of facts.params)
|
|
15
|
+
push(p.fn, p.name);
|
|
16
|
+
for (const d of facts.defs)
|
|
17
|
+
push(d.fn, d.name, ...d.expr.refs, ...d.expr.callees);
|
|
18
|
+
for (const c of facts.calls) {
|
|
19
|
+
push(c.fn, c.callee);
|
|
20
|
+
for (const a of c.args)
|
|
21
|
+
push(c.fn, ...a.refs, ...a.callees);
|
|
22
|
+
}
|
|
23
|
+
for (const r of facts.returns)
|
|
24
|
+
push(r.fn, ...r.expr.refs, ...r.expr.callees);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Order a file's function names by how close they sit, in def-use terms, to what the request says.
|
|
29
|
+
*
|
|
30
|
+
* Three tiers, stable within each so the result is deterministic (the downstream lists are
|
|
31
|
+
* position-indexed): functions the request names directly, functions one def-use step from those,
|
|
32
|
+
* then everything else in its original order. A term matching nothing leaves the order untouched —
|
|
33
|
+
* an empty first tier must not shuffle the list.
|
|
34
|
+
*/
|
|
35
|
+
function rankSymbolsByDefUse(facts, terms, order) {
|
|
36
|
+
if (order.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
const lowered = terms.map((t) => t.toLowerCase()).filter((t) => t !== '');
|
|
39
|
+
if (lowered.length === 0)
|
|
40
|
+
return [...order];
|
|
41
|
+
const surface = surfaceOf(facts);
|
|
42
|
+
// A function is "named" when a request term appears in its own name or in any identifier its
|
|
43
|
+
// body touches. Substring, not equality: `config` has to reach `configPath`.
|
|
44
|
+
const names = (fn) => [fn, ...(surface.get(fn) ?? [])];
|
|
45
|
+
const direct = new Set();
|
|
46
|
+
for (const fn of new Set([...order, ...surface.keys()])) {
|
|
47
|
+
if (names(fn).some((n) => lowered.some((t) => n.toLowerCase().includes(t))))
|
|
48
|
+
direct.add(fn);
|
|
49
|
+
}
|
|
50
|
+
if (direct.size === 0)
|
|
51
|
+
return [...order];
|
|
52
|
+
// One def-use step: a function that reads what a direct one defined, or that sits on either end
|
|
53
|
+
// of a call with it. Call adjacency counts BOTH ways — the caller of a named function is as
|
|
54
|
+
// relevant as its callee, and a one-directional rule would silently favour one.
|
|
55
|
+
const definedByDirect = new Set();
|
|
56
|
+
for (const d of facts.defs)
|
|
57
|
+
if (direct.has(d.fn))
|
|
58
|
+
definedByDirect.add(d.name);
|
|
59
|
+
const near = new Set();
|
|
60
|
+
const mark = (fn) => { if (!direct.has(fn))
|
|
61
|
+
near.add(fn); };
|
|
62
|
+
for (const d of facts.defs)
|
|
63
|
+
if (d.expr.refs.some((r) => definedByDirect.has(r)))
|
|
64
|
+
mark(d.fn);
|
|
65
|
+
for (const c of facts.calls) {
|
|
66
|
+
if (c.args.some((a) => a.refs.some((r) => definedByDirect.has(r))))
|
|
67
|
+
mark(c.fn);
|
|
68
|
+
if (direct.has(c.fn))
|
|
69
|
+
mark(c.callee);
|
|
70
|
+
if (direct.has(c.callee))
|
|
71
|
+
mark(c.fn);
|
|
72
|
+
}
|
|
73
|
+
for (const r of facts.returns)
|
|
74
|
+
if (r.expr.refs.some((x) => definedByDirect.has(x)))
|
|
75
|
+
mark(r.fn);
|
|
76
|
+
const tier = (fn) => (direct.has(fn) ? 0 : near.has(fn) ? 1 : 2);
|
|
77
|
+
return [...order].sort((a, b) => tier(a) - tier(b) || order.indexOf(a) - order.indexOf(b));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append what def-use suggested behind what the lexical layer matched.
|
|
81
|
+
*
|
|
82
|
+
* Never in front: this repository has already measured a re-ranker deleting the lexical answer, so
|
|
83
|
+
* a second signal rides behind the first rather than replacing it.
|
|
84
|
+
*/
|
|
85
|
+
function enrichCandidateSymbols(lexical, suggested) {
|
|
86
|
+
const out = [...lexical];
|
|
87
|
+
for (const s of suggested)
|
|
88
|
+
if (!out.includes(s))
|
|
89
|
+
out.push(s);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
}
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
125
|
-
fm[
|
|
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 ?? ''),
|
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.
|
|
4
|
+
"version": "0.19.0",
|
|
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",
|
|
@@ -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 로 집행**한다. 코드 그래프도 같은 기준을 향한다.
|