@holmes-lab/holmes-kit 0.17.0 → 0.18.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,64 @@ 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.18.0] - 2026-09-08
9
+
10
+ Decisions become governed. A consuming project's operational decisions had been leaking into agent
11
+ memory outside the gate — because Holmes-Kit accepted functional contracts (REQ→H→A→T) but had no
12
+ slot for a **decision**. Now it does, and it reuses machinery already present rather than inventing
13
+ a channel.
14
+
15
+ ### Added
16
+ - **ADR as a first-class governed spec type (REQ-571)** — `spec_create(type: "ADR")` (refused as
17
+ "unknown spec type" through 0.17.0) now scaffolds a root decision document under `06_adr`
18
+ (sections Context / Decision / Consequences / Alternatives; fields `decided` / `decider`) and
19
+ inherits the store's full authoring governance: validate → `spec_approve` seal
20
+ (`approved_digest` + ledger) → post-seal tamper-block. Three deliberate boundaries: an ADR
21
+ carries **no** T-SPEC / `@implements` / Files-to-Touch duty (a decision is not a functional
22
+ contract — a separate axis from No-Spec-No-Code); it lives in its **own number space** (a
23
+ consuming project's existing `ADR-0001` neither blocks nor is blocked by the functional chain's
24
+ max); and its seal is **hitl-only** (autonomy never self-approves a decision, like an upstream
25
+ REQ/H-SPEC/C-SPEC).
26
+ - **Store ADRs join the existing decision surface with zero new edge kinds (REQ-571)** — a store
27
+ ADR merges into the `collectDecisions` population (store wins on id collision with a legacy
28
+ `.ax/decisions/` entry, which coexists for back-compat), so `ADR-XXXX` citations in specs and
29
+ source become `constrained_by` edges, `supersedes` chains link, and the DECISION node appears —
30
+ all through machinery that already shipped. The graph's `SPEC:ADR` node carries the Decision
31
+ line as its intent summary (intent-layer parity), and the citation system recognizes `ADR-\d+`
32
+ (recognition only — the lexical match-scoring population stays A-SPEC-limited, so an ADR never
33
+ admits or reorders a file). A migration guide for consuming projects ships at
34
+ `docs/adr-migration.md`.
35
+
36
+ - **The graph advisory now arrives at DESIGN time (REQ-572)** — measured on our own work: the
37
+ sealing advisory for a slice named `validator.ts` / `legacy-format.ts` / `rtm-check.ts` /
38
+ `constitution.ts`, three regressions landed in exactly that cluster, and the agent read none of
39
+ it until the suite went red. The calculation was never the gap; the **delivery time** was. So the
40
+ read-only `approval_status` — already the "what is blocking this right now" tool — also answers
41
+ `graphPreview`: `impact` (files calling INTO your declared Files-to-Touch from outside, each
42
+ anchor carrying its spec's intent sentence) and `density` (anchor-dense files inside the scope),
43
+ computed by the **same functions `spec_approve` calls**, so a preview can never disagree with the
44
+ seal. Read-only stays read-only: no scan, no build, and **no ledger append** (a query must not
45
+ pollute the observation denominator); every failure degrades to an absent field. Both authoring
46
+ playbooks now carry the step — read the impact before freezing Files-to-Touch, then either widen
47
+ the declaration, narrow the design, or leave it knowingly — pinned by test so the instruction
48
+ cannot evaporate. It stays a **discipline, not a gate** (hard-gate promotion waits on the
49
+ observation ledger's false-positive rate).
50
+ - **Root-cause analysis carries the decision context (REQ-572)** — `maintenance_analyze` candidates
51
+ now ride with `decisionContext`: the ADRs constraining that file (directly, or through the specs
52
+ it anchors) and each decision's own sentence. That is the order a person diagnoses in — what
53
+ broke, then why it was left this way — and it needs no new mechanism: ADRs entered the graph with
54
+ REQ-571 and `constrained_by` has been built from citations since A-SPEC-293. Information only,
55
+ capped, never an input to the ranking.
56
+
57
+ ### Notes
58
+ - Version bump is **minor** carrying one `gate-behavior` change (the new ADR type is accepted where
59
+ it was refused); the release classifier routes this to HITL by design. The four existing spec
60
+ types are unchanged — verdicts pinned across the suite.
61
+ - REQ-572's own last slice was authored **through** the new design-time read: the preview reported
62
+ `maintenance-analyze.ts` at 35 anchors and `handlers.ts` at 83 (p90 = 7), so the decision-context
63
+ logic went into a new pure module instead of thickening either file. First recorded case of the
64
+ advisory changing a design before the code was written.
65
+
8
66
  ## [0.17.0] - 2026-09-07
9
67
 
10
68
  An adversarial review of 0.16.0's own new surfaces, run against the shipped tarball the day it
package/README.md CHANGED
@@ -16,6 +16,9 @@
16
16
 
17
17
  ### 🛡️ Currently Supported Features (Production Features)
18
18
 
19
+ - 🧭 **The graph speaks BEFORE you commit to a scope** *(new in 0.18.0)*: the read-only `approval_status` now also answers `graphPreview` — `impact` (files that call INTO your declared Files-to-Touch from outside it, each anchor carrying its spec's intent sentence) and `density` (anchor-dense files inside the scope) — computed by the **same functions the sealing advisory uses**, so the preview can never disagree with the seal. Read the impact, then widen the declaration, narrow the design, or leave it knowingly; the authoring playbooks carry the step (pinned by test) and it stays a discipline, not a gate. Root-cause work gets the other half: `maintenance_analyze` candidates ride with `decisionContext` — the ADRs constraining that file and each decision's own sentence — which is the order a person diagnoses in (what broke, then why it was left this way). Both are information only: value tests pin that no ranking, score or gate reads them.
20
+ - 📜 **ADR as a first-class governed document** *(new in 0.18.0)*: decisions stop leaking into agent memory outside the gate (a measured incident on a consuming project drove this). `spec_create(type: "ADR")` scaffolds a root decision document (Context / Decision / Consequences / Alternatives, `decided`/`decider`) under the store's full authoring governance — validate, `spec_approve` seal, ledger, tamper-block — with its **own number space** (your existing `ADR-0001` just works) and a **hitl-only seal** (autonomy never self-approves a decision). Store ADRs join the existing decision surface with zero new edge kinds: `ADR-XXXX` citations in specs/code become `constrained_by` edges, `supersedes` chains link, the graph's SPEC:ADR node carries the Decision line as its intent summary, and legacy `.ax/decisions/` entries coexist (store wins on id collision). A migration guide ships at `docs/adr-migration.md`.
21
+
19
22
  - 📣 **Impact Advisory at sealing time** *(new in 0.16.0)*: approving an A-SPEC now returns what your Files-to-Touch declaration *missed* — files whose symbols **call into** the declared scope from outside it (1-hop, capped, repo-relative allow-list), computed from the persisted RTM graph at the moment of sealing. Advisory, never verdict: it rides the response *after* the seal commits, degrades to absence on any failure, and every emission lands in an observation ledger so its false-positive rate is **measured before** anyone proposes a hard gate. The graph keeps itself fresh — `rtm_impact` rebuilds on basis drift and the Stop hook spawns a TTL-gated detached reindex (staleness was measured as the advisory's quality factor: 7 findings on an 8-day-old graph, 17 after a fresh one). *(0.17.0 hardening)*: the advisory/impact graph is **approved-only** (a draft needs no approval to exist, so it can no longer reach these agent-visible channels), summary prose can't forge graph rows (structural characters fold at both the extraction and storage boundaries), and annotations are capped with explicit omission counts (a hub-grade response shrank 104.7KB → 18.7KB, −82%). Sealing also gains an **anchor-density advisory** (observation-only): an A-SPEC whose Files-to-Touch contains an anchor-dense file (live anchors ≥ max(8, p90)) is annotated with `anchorDensity: [{path, anchors, p90}]` and ledgered — grounded in the measured precision tax of anchor accumulation; a count *gate* was considered and refused.
20
23
  - 🗣️ **The graph speaks intent** *(new in 0.16.0)*: every SPEC node stores a one-sentence intent summary (`"<title> — <first sentence of its intent section>"`, schema `rtm-graph/3`, old stores rebuild automatically) — extracted deterministically, **never generated** (same store, byte-identical graph; measured cost +6.4% build time / +4.2% file size). Advisory anchors arrive as `{id, summary}` and `rtm_impact` adds `impactedSummaries`, so the reader sees *which intent* is at risk without a spec-store round trip. Information only: value tests pin that no verdict, ranking or gate reads the prose.
21
24
  - 📇 **Session-context observability** *(new in 0.16.0)*: the ledger records which agent/model drove a session and what the governance overhead cost, per replica (`session-context.<replica>.jsonl`), grounding field reports in machine attribution instead of guesswork.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 20f5f64-mtrggtxw
1
+ cc24589-mtrjegw0
@@ -133,6 +133,7 @@ function specApprovalAutonomy(spec, _resolveParent) {
133
133
  case 'REQ':
134
134
  case 'H-SPEC':
135
135
  case 'C-SPEC':
136
+ case 'ADR': // @implements A-SPEC-571.1 — a decision is the human's to seal
136
137
  return 'hitl'; // wide blast radius / structural constraint
137
138
  case 'A-SPEC': {
138
139
  const grade = breakingGrade(spec);
@@ -1,6 +1,9 @@
1
1
  import { SpecStore } from '../spec/spec-store';
2
+ import { Spec } from '../spec/spec-parser';
2
3
  import { MaintenanceGroundTruth, MaintenanceAnalysis } from './maintenance-analyze';
3
4
  import { Action } from '../guardrail/phase';
5
+ import { ScannedFile } from '../cpg/cpg-scanner';
6
+ import { type DecisionRecord, type DecisionCitation } from '../rtm/rtm-builder';
4
7
  import { Basis } from './basis';
5
8
  import { Finding } from '../review/findings';
6
9
  import { Approval, Enforcement } from '../guardrail/risk-gate';
@@ -53,6 +56,16 @@ export declare function makeHandlers(store: SpecStore, opts?: ElicitOpts): RawHa
53
56
  }>;
54
57
  };
55
58
  type RawHandlers = ReturnType<typeof makeRawHandlers>;
59
+ /**
60
+ * @implements A-SPEC-293
61
+ * Read `.ax/decisions/*.md` and find who cites them. Citation is the link, chosen by measurement:
62
+ * `governs` resolves 9 of 65 names to symbols while 37 source files and 71 spec files cite an ADR.
63
+ * Failure here is never fatal — a project with no decisions directory simply has no decisions.
64
+ */
65
+ export declare function collectDecisions(root: string, scanned: readonly ScannedFile[], specs: readonly Spec[]): {
66
+ decisions: DecisionRecord[];
67
+ citations: DecisionCitation[];
68
+ };
56
69
  declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
57
70
  spec_create(a: any): Promise<{
58
71
  ok: boolean;
@@ -207,6 +220,11 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
207
220
  ok: boolean;
208
221
  reason: string;
209
222
  } | {
223
+ graphPreview?: {
224
+ impact?: import("../rtm/impact-advisory").ImpactAdvisory;
225
+ density?: import("../rtm/anchor-density").AnchorDensityFinding[];
226
+ graphAsOf?: string;
227
+ } | undefined;
210
228
  id: string;
211
229
  type?: string;
212
230
  status: string;
@@ -37,6 +37,7 @@ exports.HandlerRefusal = void 0;
37
37
  exports.isHandlerRefusal = isHandlerRefusal;
38
38
  exports.unreadableAmong = unreadableAmong;
39
39
  exports.makeHandlers = makeHandlers;
40
+ exports.collectDecisions = collectDecisions;
40
41
  // @implements A-SPEC-293
41
42
  // @implements A-SPEC-292
42
43
  // @implements A-SPEC-290
@@ -699,8 +700,24 @@ function collectDecisions(root, scanned, specs) {
699
700
  });
700
701
  }
701
702
  }
702
- catch {
703
- return { decisions: [], citations: [] };
703
+ catch { /* @implements A-SPEC-571.2 — no legacy .ax/decisions dir is not "no decisions": store
704
+ ADRs below are still a source. An unreadable dir degrades to the empty legacy set, not an early
705
+ return that would skip the store population. */
706
+ }
707
+ // @implements A-SPEC-571.2 — store ADRs JOIN the decisions population, so the existing consumers
708
+ // (DECISION nodes, constrained_by citations, supersedes) light up with no new edge kind. A store
709
+ // ADR is the canon: same id in legacy .ax/decisions is replaced (store wins).
710
+ for (const spec of specs) {
711
+ if (spec.type !== 'ADR')
712
+ continue;
713
+ const sup = spec.frontmatter?.supersedes;
714
+ const supersedes = typeof sup === 'string' && sup.trim() && sup.trim() !== 'null' ? sup.trim() : null;
715
+ const idx = decisions.findIndex((d) => d.id === spec.id);
716
+ const rec = { id: spec.id, title: spec.title, status: spec.status, supersedes };
717
+ if (idx >= 0)
718
+ decisions[idx] = rec;
719
+ else
720
+ decisions.push(rec);
704
721
  }
705
722
  const ids = new Set(decisions.map((d) => d.id));
706
723
  // @implements A-SPEC-546.1 — recognise ADR-\d{3,} (4-digit ADRs no longer invisible), via a pure fn.
@@ -1330,7 +1347,58 @@ function makeRawHandlers(store, opts) {
1330
1347
  const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
1331
1348
  return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
1332
1349
  }
1333
- return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, resolver(all)) };
1350
+ // @implements A-SPEC-572.1
1351
+ // The graph advisory, delivered at DESIGN time. Measured on our own work: A-SPEC-571.1's
1352
+ // sealing advisory named the exact cluster three regressions then landed in — and by then
1353
+ // the design was done. The calculation was never the gap; the delivery time was. So the
1354
+ // read-only "what is blocking this right now" tool also answers "what will this scope leak,
1355
+ // and where is it dense" — using the SAME functions spec_approve calls, so the preview can
1356
+ // never disagree with the seal. Read-only stays read-only: no scan, no build, no ledger
1357
+ // append (a query must not pollute the observation denominator), and every failure degrades
1358
+ // to an absent field on an otherwise identical response.
1359
+ let graphPreview;
1360
+ try {
1361
+ if (cur.spec.type === 'A-SPEC' && a.root) {
1362
+ const dbPath = path.join(a.root, '.ax', 'rtm.sqlite');
1363
+ if (fs.existsSync(dbPath)) {
1364
+ const { declaredImpactGap } = require('../rtm/impact-advisory');
1365
+ const { anchorDensityFindings } = require('../rtm/anchor-density');
1366
+ const { filesToTouch } = require('../spec/compat-impact');
1367
+ const { RtmGraph } = require('../rtm/rtm-graph');
1368
+ const graph = new RtmGraph(dbPath);
1369
+ try {
1370
+ const ftt = filesToTouch(cur.spec);
1371
+ const impact = declaredImpactGap(ftt, graph, (rel) => { try {
1372
+ return fs.readFileSync(path.join(a.root, rel), 'utf8');
1373
+ }
1374
+ catch {
1375
+ return null;
1376
+ } });
1377
+ const density = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
1378
+ if (impact || density.length > 0) {
1379
+ const graphAsOf = (() => { try {
1380
+ return fs.statSync(dbPath).mtime.toISOString();
1381
+ }
1382
+ catch {
1383
+ return undefined;
1384
+ } })();
1385
+ graphPreview = {
1386
+ ...(impact ? { impact } : {}),
1387
+ ...(density.length > 0 ? { density } : {}),
1388
+ ...(graphAsOf ? { graphAsOf } : {}),
1389
+ };
1390
+ }
1391
+ }
1392
+ finally {
1393
+ graph.close();
1394
+ }
1395
+ }
1396
+ }
1397
+ }
1398
+ catch {
1399
+ graphPreview = undefined;
1400
+ }
1401
+ return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, resolver(all)), ...(graphPreview ? { graphPreview } : {}) };
1334
1402
  },
1335
1403
  /**
1336
1404
  * @implements A-SPEC-538.3
@@ -3,6 +3,7 @@ import type { Spec } from '../spec/spec-parser';
3
3
  import type { RtmGraph } from '../rtm/rtm-graph';
4
4
  import type { ContextBundle } from '../context/bundler';
5
5
  import { type TestScope } from '../rtm/test-scope';
6
+ import { type DecisionContextEntry } from '../rtm/decision-context';
6
7
  import type { ResolutionReport } from '../rtm/rtm-builder';
7
8
  import { type TestEvidence } from '../review/test-evidence';
8
9
  import { type LanguageGap } from '../cpg/language-capability';
@@ -212,6 +213,8 @@ export interface MaintenanceAnalysis {
212
213
  score: number;
213
214
  symbols: string[];
214
215
  evidence: string[];
216
+ /** @implements A-SPEC-572.3 — decisions constraining this candidate; information only. */
217
+ decisionContext?: DecisionContextEntry[];
215
218
  }>;
216
219
  /**
217
220
  * @implements A-SPEC-494 — the semantic ALTERNATES: top-3 cached-vector cosines among files
@@ -26,6 +26,11 @@ const scope_1 = require("../review/scope");
26
26
  // the measured numbers transfer to the shipped surface.
27
27
  const assoc_arm_1 = require("../assoc/assoc-arm");
28
28
  const acceptance_quality_1 = require("../spec/acceptance-quality");
29
+ // @implements A-SPEC-572.3 — the decision lookup lives in its own pure module: this file
30
+ // already carries 35 anchors (p90 is 7), and the design-time density advisory said so BEFORE
31
+ // this slice fixed its Files-to-Touch. Adding logic here would have made the file denser; the
32
+ // call site is one line, the logic is next door.
33
+ const decision_context_1 = require("../rtm/decision-context");
29
34
  const taint_1 = require("../rtm/taint");
30
35
  const test_evidence_1 = require("../review/test-evidence");
31
36
  const language_capability_1 = require("../cpg/language-capability");
@@ -377,6 +382,21 @@ function analyzeMaintenance(input) {
377
382
  symbols: sortedUnique(hit.matchedSymbols),
378
383
  evidence: [...hit.why],
379
384
  }));
385
+ // @implements A-SPEC-572.3
386
+ // "What broke" then "why is it this way" — the order a person diagnoses in. The decisions are
387
+ // already in the graph (REQ-571 put store ADRs there; A-SPEC-293 builds `constrained_by` from
388
+ // citations), so this is a lookup, not a new mechanism. Information only: it rides beside a
389
+ // candidate and never enters the score or the ordering above.
390
+ {
391
+ const anchorsOf = new Map(scanned.map((f) => [f.sourcePath, f.implementsSpecs ?? []]));
392
+ const ctx = (0, decision_context_1.decisionContextFor)(candidates.map((c) => c.file), (file) => anchorsOf.get(file) ?? [], input.graph);
393
+ for (const candidate of candidates) {
394
+ const entries = ctx.get(candidate.file);
395
+ if (entries && entries.length > 0) {
396
+ candidate.decisionContext = entries;
397
+ }
398
+ }
399
+ }
380
400
  // @implements A-SPEC-478 — the uncited semantic head rerank, exactly the arm S-495 measured:
381
401
  // same math (cosine over cached doc vectors, pool FIXED), same gate (the request cited no
382
402
  // spec). The gate is load-bearing both ways — uncited corpora gained +26%/+93% at the head,
@@ -15,12 +15,4 @@ export type IdVerdict = {
15
15
  reason: string;
16
16
  nextAvailable: number;
17
17
  };
18
- /**
19
- * 새 id 의 base 가 코퍼스 max base 를 한 칸 넘게 뛰면 거부한다.
20
- *
21
- * - 빈 코퍼스, 또는 base 를 못 뽑는 id → 통과(비교 대상이 없거나, 형태 검증은 별도 소관이라 한 결함에
22
- * 두 이름을 주지 않는다).
23
- * - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
24
- * - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
25
- */
26
18
  export declare function sequentialIdVerdict(newId: string, existingIds: string[]): IdVerdict;
@@ -30,11 +30,20 @@ function specIdBase(id) {
30
30
  * - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
31
31
  * - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
32
32
  */
33
+ /** The id's number SPACE. @implements A-SPEC-571.1 — ADR keeps its own sequence so a decision
34
+ * (jarvis's ADR-0001) neither blocks nor is blocked by the functional chain's max. Everything
35
+ * else shares one space, exactly as before. */
36
+ function idSpaceOf(id) {
37
+ return /^ADR-/.test(id.trim()) ? 'ADR' : 'functional';
38
+ }
33
39
  function sequentialIdVerdict(newId, existingIds) {
34
40
  const base = specIdBase(newId);
35
41
  if (base === null)
36
42
  return { ok: true };
37
- const bases = (existingIds ?? []).map(specIdBase).filter((n) => n !== null);
43
+ // @implements A-SPEC-571.1 compare only within the same number space.
44
+ const space = idSpaceOf(newId);
45
+ const bases = (existingIds ?? []).filter((id) => idSpaceOf(id) === space)
46
+ .map(specIdBase).filter((n) => n !== null);
38
47
  if (bases.length === 0)
39
48
  return { ok: true };
40
49
  const maxBase = Math.max(...bases);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The DECISION context of a candidate — "why is this the way it is?".
3
+ *
4
+ * A person diagnosing a defect asks two questions in order: what broke (the candidates), and then
5
+ * why this code was left this way (the decision). The graph already answers the second — REQ-571
6
+ * put store ADRs into the decision population and A-SPEC-293 already builds `constrained_by` from
7
+ * citations — so this is a lookup, not a new mechanism: no new edge kind, no new tool.
8
+ *
9
+ * OBSERVATION ONLY. The context rides beside a candidate; nothing here enters the ranking, the
10
+ * score, or any gate (the c6 rule — prose informs, it never adjudicates).
11
+ */
12
+ export interface DecisionContextEntry {
13
+ adr: string;
14
+ /** The decision's own sentence, or null when the store carries no summary for it (id stays). */
15
+ decision: string | null;
16
+ }
17
+ export interface DecisionGraphLike {
18
+ decisionsConstraining(nodeIds: readonly string[]): string[];
19
+ summaryOf(id: string): string | null;
20
+ }
21
+ /** How many decisions ride beside one candidate. A prose constant — never a verdict input. */
22
+ export declare const MAX_DECISIONS = 3;
23
+ export declare function decisionContextFor(files: readonly string[], specIdsOf: (file: string) => readonly string[], graph: DecisionGraphLike): Map<string, DecisionContextEntry[]>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ // @implements A-SPEC-572.3
3
+ /**
4
+ * The DECISION context of a candidate — "why is this the way it is?".
5
+ *
6
+ * A person diagnosing a defect asks two questions in order: what broke (the candidates), and then
7
+ * why this code was left this way (the decision). The graph already answers the second — REQ-571
8
+ * put store ADRs into the decision population and A-SPEC-293 already builds `constrained_by` from
9
+ * citations — so this is a lookup, not a new mechanism: no new edge kind, no new tool.
10
+ *
11
+ * OBSERVATION ONLY. The context rides beside a candidate; nothing here enters the ranking, the
12
+ * score, or any gate (the c6 rule — prose informs, it never adjudicates).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.MAX_DECISIONS = void 0;
16
+ exports.decisionContextFor = decisionContextFor;
17
+ /** How many decisions ride beside one candidate. A prose constant — never a verdict input. */
18
+ exports.MAX_DECISIONS = 3;
19
+ function decisionContextFor(files, specIdsOf, graph) {
20
+ const out = new Map();
21
+ for (const file of files) {
22
+ let adrs = [];
23
+ try {
24
+ // The file itself AND the specs it anchors: a person chasing "why" follows both, and the
25
+ // constraint is often recorded one level up, on the spec rather than on the file.
26
+ adrs = graph.decisionsConstraining([
27
+ `FILE:${file}`,
28
+ ...specIdsOf(file).map((id) => `SPEC:${id}`),
29
+ ]);
30
+ }
31
+ catch {
32
+ continue;
33
+ } // an unreadable graph yields no context, never a throw
34
+ const shown = [...new Set(adrs)].sort().slice(0, exports.MAX_DECISIONS);
35
+ if (shown.length === 0)
36
+ continue; // no entry rather than an invented empty list
37
+ out.set(file, shown.map((adr) => {
38
+ let decision = null;
39
+ try {
40
+ decision = graph.summaryOf(`SPEC:${adr}`);
41
+ }
42
+ catch { /* id stays, prose does not */ }
43
+ return { adr, decision };
44
+ }));
45
+ }
46
+ return out;
47
+ }
@@ -10,8 +10,10 @@ exports.localizeIssue = localizeIssue;
10
10
  * repository's `S-<n>` slice shorthand (which names the whole REQ->T-SPEC chain at that number).
11
11
  * The prefix is REQUIRED — a bare `262` in "262 files were rewritten" is a number, not a citation.
12
12
  */
13
- const CITATION = /\b(?:(A-SPEC|H-SPEC|T-SPEC|C-SPEC|REQ)-(\d+(?:\.\d+)?)|S-(\d+(?:\.\d+)?))\b/gi;
14
- const CITABLE_KINDS = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'C-SPEC'];
13
+ // @implements A-SPEC-571.2 — ADR is citable like any governed id (recognition only; the match
14
+ // SCORING population stays A-SPEC-limited below, so an ADR never admits or reorders a file).
15
+ const CITATION = /\b(?:(A-SPEC|H-SPEC|T-SPEC|C-SPEC|ADR|REQ)-(\d+(?:\.\d+)?)|S-(\d+(?:\.\d+)?))\b/gi;
16
+ const CITABLE_KINDS = ['REQ', 'H-SPEC', 'A-SPEC', 'T-SPEC', 'C-SPEC', 'ADR'];
15
17
  /**
16
18
  * @implements A-SPEC-273
17
19
  * How much a citation is worth, normalized by the file's anchor count exactly as the lexical spec
@@ -73,6 +73,8 @@ const INTENT_SECTION = {
73
73
  'REQ': 'Problem / Need',
74
74
  'H-SPEC': 'Intent',
75
75
  'A-SPEC': 'Objective',
76
+ // @implements A-SPEC-571.2 — a decision's intent IS its Decision line.
77
+ 'ADR': 'Decision',
76
78
  };
77
79
  /** Longest intent sentence carried into the graph — a bound, applied deterministically. */
78
80
  const SUMMARY_SENTENCE_CAP = 200;
@@ -1,4 +1,4 @@
1
- export type SpecType = 'REQ' | 'H-SPEC' | 'A-SPEC' | 'C-SPEC' | 'T-SPEC';
1
+ export type SpecType = 'REQ' | 'H-SPEC' | 'A-SPEC' | 'C-SPEC' | 'T-SPEC' | 'ADR';
2
2
  export declare const SPEC_STATUSES: readonly ["draft", "review", "approved", "outdated"];
3
3
  export type SpecStatus = (typeof SPEC_STATUSES)[number];
4
4
  export interface SpecTypeDef {
@@ -12,7 +12,9 @@ exports.parentRuleText = parentRuleText;
12
12
  // its test's CANON, and the type itself — so adding a status compiled clean while spec_list kept
13
13
  // flagging it legacy.
14
14
  exports.SPEC_STATUSES = ['draft', 'review', 'approved', 'outdated'];
15
- exports.SPEC_ORDER = ['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC'];
15
+ // @implements A-SPEC-571.1 — ADR trails the functional chain: spec_next serves the REQ→…→T-SPEC
16
+ // order first, and a decision is off that critical path.
17
+ exports.SPEC_ORDER = ['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC', 'ADR'];
16
18
  /**
17
19
  * Where a requirement came from.
18
20
  *
@@ -102,6 +104,17 @@ exports.SPEC_TYPES = {
102
104
  requiredFields: ['coverage'],
103
105
  requiredSections: ['Normal Cases', 'Corner Cases', 'Negative Cases', 'Boundary Cases'],
104
106
  },
107
+ // @implements A-SPEC-571.1
108
+ // A DECISION, not a functional contract: a root type (no parent, like REQ) that records why a
109
+ // choice was made and inherits the store's authoring governance (stub → validate → seal → ledger
110
+ // → tamper-block). It carries NO T-SPEC/anchor/FtT duty — that is the No-Spec-No-Code chain, a
111
+ // separate axis. Its number space is its own (see spec-id-guard), so jarvis's ADR-0001 does not
112
+ // collide with the functional chain's max. 4-digit zero-padded ids are accepted (ADR-0001).
113
+ 'ADR': {
114
+ type: 'ADR', idRegex: /^ADR-\d{3,}$/, example: 'ADR-0001', folder: '06_adr', parents: [],
115
+ requiredFields: ['decided', 'decider'],
116
+ requiredSections: ['Context', 'Decision', 'Consequences', 'Alternatives'],
117
+ },
105
118
  };
106
119
  // @implements A-SPEC-100.1
107
120
  /**
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.17.0",
4
+ "version": "0.18.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",
@@ -82,6 +82,25 @@ Criteria · Non-Functional · Assumptions · Open Questions.** 필수 필드: `r
82
82
  부모 REQ의 Success Criteria가 이 설계로 어떻게 달성되는지가 본문이다. Open Questions는 비워두는
83
83
  칸이 아니다 — 아직 결정하지 않은 것을 결정하지 않았다고 적는 곳이고, 닫을 때는 근거와 함께 닫는다.
84
84
 
85
+ ## Files to Touch를 확정하기 전에 — 설계-시점 영향 범위 읽기
86
+
87
+ A-SPEC의 Files to Touch는 **스코프 선언**이고, 그래프는 그 선언이 무엇을 빠뜨리는지 이미 안다.
88
+ `approval_status(id)`를 부르면 `graphPreview`가 온다:
89
+
90
+ - `impact` — 선언한 FtT **밖**에서 그 안으로 호출해 들어오는 파일들(1-hop). 각 파일의 앵커 옆에
91
+ 그 스펙의 **의도 문장**이 붙으므로, "무슨 의도가 걸려 있는지"를 스토어를 열지 않고 읽는다.
92
+ - `density` — FtT 안의 **앵커-과밀** 파일(live 앵커가 분포 상위이며 절대치도 큰 것). 새 로직을
93
+ 거기 더할지, 새 파일로 뺄지 판단하는 근거다.
94
+
95
+ 읽고 나서 셋 중 하나를 **선택**한다: ①지목된 밖-파일을 FtT에 넣는다 ②스코프를 좁혀 그 파급이
96
+ 생기지 않게 설계를 바꾼다 ③근거를 갖고 그대로 둔다. 어느 쪽이든 선언은 이제 **알고 한 선언**이다.
97
+
98
+ > [!NOTE]
99
+ > **게이트는 이것을 막지 않는다** — 규율이지 차단이 아니다(하드 게이트 승격은 관측 원장이
100
+ > 오탐률을 답한 뒤의 별도 결정). 다만 읽지 않고 확정하면, 같은 소견이 봉인 뒤에 같은 말을
101
+ > 반복한다 — 실사고 기록: 봉인 소견이 지목한 파일 클러스터에서 회귀 3건이 났고, 그 소견은
102
+ > 설계가 끝난 뒤에야 읽혔다.
103
+
85
104
  ## A-SPEC / T-SPEC에 쓰는 것
86
105
 
87
106
  A-SPEC 필수 섹션: **Objective · Inputs / Outputs · Behavior · Test Points · Files to Touch · Done
@@ -69,6 +69,11 @@ holmes는 기계적으로 판별한다: `red-error`로는 red→green 시퀀스
69
69
 
70
70
  ## 절차
71
71
 
72
+ 0. **FtT 확정 전** `approval_status(<A-SPEC id>)`의 `graphPreview`로 영향 범위를 읽는다 —
73
+ `impact`(선언 밖에서 들어오는 1-hop 호출자, 의도 문장 병기)와 `density`(앵커-과밀 파일). 지목된
74
+ 것을 FtT에 넣을지, 스코프를 좁힐지, 근거를 갖고 둘지 **정하고 나서** 아래로 간다. 게이트가
75
+ **게이트는 이것을 막지 않는다** — 규율이지 차단이 아니다. 다만 읽지 않으면
76
+ 같은 소견을 봉인 뒤에 다시 듣는다(실사고 기록).
72
77
  1. `promote-slice`로 대상 A-SPEC과 그 T-SPEC을 승인한다(`[ART-1]` 게이트를 연다).
73
78
  2. 커버 테스트를 **먼저** 쓴다. 심볼이 없어 컴파일이 깨지면 틀린-값 스텁을 넣는다.
74
79
  3. `test_run` — **red-assertion**을 본다. `red-error`면 그건 아직 RED가 아니다; 스텁으로 고쳐라.