@holmes-lab/holmes-kit 0.16.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 +130 -0
- package/README.md +8 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.js +15 -1
- package/dist/holmes/cli/mcp-version.d.ts +4 -1
- package/dist/holmes/cli/mcp-version.js +5 -2
- package/dist/holmes/governance/autonomy.js +17 -1
- package/dist/holmes/mcp/handlers.d.ts +25 -5
- package/dist/holmes/mcp/handlers.js +146 -24
- package/dist/holmes/mcp/maintenance-analyze.d.ts +3 -0
- package/dist/holmes/mcp/maintenance-analyze.js +20 -0
- package/dist/holmes/mcp/spec-id-guard.d.ts +0 -8
- package/dist/holmes/mcp/spec-id-guard.js +10 -1
- package/dist/holmes/rtm/anchor-density.d.ts +43 -0
- package/dist/holmes/rtm/anchor-density.js +117 -0
- package/dist/holmes/rtm/decision-context.d.ts +23 -0
- package/dist/holmes/rtm/decision-context.js +47 -0
- package/dist/holmes/rtm/impact-advisory.d.ts +4 -0
- package/dist/holmes/rtm/impact-advisory.js +9 -2
- package/dist/holmes/rtm/localize.js +4 -2
- package/dist/holmes/rtm/rtm-builder.js +18 -8
- package/dist/holmes/rtm/rtm-graph.d.ts +12 -0
- package/dist/holmes/rtm/rtm-graph.js +29 -4
- package/dist/holmes/spec/spec-types.d.ts +1 -1
- package/dist/holmes/spec/spec-types.js +14 -1
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +19 -0
- package/playbooks/publish/PLAYBOOK.md +16 -7
- package/playbooks/tdd-slice/PLAYBOOK.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,136 @@ 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
|
+
|
|
66
|
+
## [0.17.0] - 2026-09-07
|
|
67
|
+
|
|
68
|
+
An adversarial review of 0.16.0's own new surfaces, run against the shipped tarball the day it
|
|
69
|
+
went out, drove this release: the newest repair is always the next target, and this time the
|
|
70
|
+
findings were sealed before anyone else could find them. The release gate itself also gets the
|
|
71
|
+
fix for the incident that let 0.16.0 ship with a frozen README.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- **Graph-row forgery via spec prose (REQ-569 S1, HIGH)** — a DRAFT spec (no approval needed to
|
|
75
|
+
exist) whose YAML double-quoted title carried `\n`/`\t` escapes could forge rows in the graph's
|
|
76
|
+
canonical dump, which the PPR view parsed as **real call edges** — poisoning `rankedImpact` and
|
|
77
|
+
`maintenance_analyze` (reproduced against the shipped 0.16.0 artifact, then killed). Sealed at
|
|
78
|
+
BOTH boundaries independently: `specSummary` now folds whitespace over the whole summary (title
|
|
79
|
+
included — the sentence-only fold was the hole), and `RtmGraph.addNode` folds structural
|
|
80
|
+
characters at the storage boundary so no future caller can break a dump row either. Each face
|
|
81
|
+
verified alone. A high-effort review of this very fix then widened it: tabs riding in via spec
|
|
82
|
+
`id:`/`depends_on:` still shifted dump columns, so `dumpCanonical` now folds EVERY text column
|
|
83
|
+
at emission (single choke point; the 13-cell row invariant is pinned by test), and the graph
|
|
84
|
+
extractor version bumped to `holmes-rtm/2` so a persisted store written by unfixed 0.16.0 —
|
|
85
|
+
whose summary column may already carry forged rows — is force-rebuilt instead of reused.
|
|
86
|
+
- **Grader/doctor parity pair (REQ-569 S4)** — `isHighRiskPath` folds `.` path segments before
|
|
87
|
+
judging (`src/holmes/./governance/x.ts` no longer dodges the risk roots; `..` is deliberately
|
|
88
|
+
NOT folded — the grader widens sight, never impersonates path resolution, and the enforcer
|
|
89
|
+
stays byte-literal). The codex doctor's npx-pin branch now compares the pin against the
|
|
90
|
+
installed version and WARNs on a stale pin (parity with the `.mcp.json` drift check) — an old
|
|
91
|
+
fixture that had enshrined the gap as PASS was repinned to the current version. Review
|
|
92
|
+
follow-ups sealed in the same release: a `..`-bearing Files-to-Touch token is now FLAGGED
|
|
93
|
+
high-risk (the hook does not canonicalize relative paths, so a `src/app/../holmes/...` spelling
|
|
94
|
+
stayed admissible at the byte-literal enforcer while the grader called it benign — flagging the
|
|
95
|
+
ambiguous ascent token closes the self-approval hole without impersonating path resolution);
|
|
96
|
+
non-semver pins (`@latest`, `^x.y.z`) judge `unknown` instead of an inverted "stale pin" WARN;
|
|
97
|
+
and the sealing-time advisory now closes its SQLite handle (a per-approval native-handle leak in
|
|
98
|
+
the long-lived MCP server, and a file-lock risk on Windows).
|
|
99
|
+
- **The docs-currency gate is bidirectional now (REQ-570)** — 0.16.0 shipped with README's
|
|
100
|
+
feature list frozen at "v0.14.x" and zero 0.16.0 entries, because the gate's instruction was
|
|
101
|
+
"grep for stale phrases": a feature never written produces zero hits, and zero hits read as
|
|
102
|
+
"no drift". The publish playbook now demands three checks per user-facing change — ADDITION
|
|
103
|
+
(the entry must exist; zero grep hits are a missing-entry signal, not a pass), drift (the old
|
|
104
|
+
wording), and stale markers (version-pinned section labels are drift generators and get
|
|
105
|
+
removed, not policed). The incident is recorded in the playbook itself, the README repaired
|
|
106
|
+
retroactively, and the new wording pinned by tests.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
- **The impact/advisory graph is approved-only (REQ-569 S2)** — the persisted RTM graph that
|
|
110
|
+
feeds `rtm_impact`, the sealing-time advisory and the intent summaries now builds from SEALED
|
|
111
|
+
specs only: a draft needs no approval to exist, so it can no longer reach the agent-visible
|
|
112
|
+
channels (that was the forgery's delivery vehicle). The closure walks edges, so the channel
|
|
113
|
+
filter is node existence — an anchor naming a draft keeps its id but carries no prose until
|
|
114
|
+
approval. Diagnosis (`rtm_check`) and matching (`issue_localize`) keep their existing
|
|
115
|
+
populations; old `rtm.sqlite` files rebuild automatically on basis drift. Replay pins unmoved
|
|
116
|
+
to the digit.
|
|
117
|
+
- **Annotation caps (REQ-569 S3)** — `impactedSummaries` caps at 40 (`summariesOmitted` counts
|
|
118
|
+
the rest; `impacted` itself is never truncated) and advisory anchors cap at 10 per file
|
|
119
|
+
(`anchorsOmitted`). Measured on this repository's hub-grade impact (342 specs): the response
|
|
120
|
+
shrank **104,706 → 18,708 bytes (−82%)**, and the omitted tail costs no summary lookups.
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
- **Anchor-density advisory (REQ-569 S5, observation only)** — sealing an A-SPEC whose
|
|
124
|
+
Files-to-Touch contains an anchor-dense file (live `implements` count ≥ max(8, p90 of the
|
|
125
|
+
store's distribution)) annotates the response with `anchorDensity: [{path, anchors, p90}]` and
|
|
126
|
+
records it to `anchor-density.<replica>.jsonl` (paths and integers only). Grounded in the
|
|
127
|
+
measured precision tax of anchor accumulation (authoring one spec moved replay recall
|
|
128
|
+
0.5476→0.5060); a count GATE was considered and refused — the thresholds are prose constants,
|
|
129
|
+
and promotion or rejection will be decided by this ledger, the impactAdvisory lifecycle.
|
|
130
|
+
|
|
131
|
+
### Notes
|
|
132
|
+
- One inherited finding was honestly killed instead of "fixed": the recorded
|
|
133
|
+
"scanTestAnchors only sees a file's first anchor" defect does **not reproduce** at HEAD (all
|
|
134
|
+
standalone-comment anchors are consumed by ART-4 and the execution-evidence attribution alike);
|
|
135
|
+
the only non-recognition is the trailing-comment form, which is the sealed anchor idiom rule.
|
|
136
|
+
Stale findings get re-measured, not re-fixed.
|
|
137
|
+
|
|
8
138
|
## [0.16.0] - 2026-09-07
|
|
9
139
|
|
|
10
140
|
The graph learns to speak intent, and the call graph learns to speak up at sealing time. An A-SPEC
|
package/README.md
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
### 🛡️ Currently Supported Features (
|
|
17
|
+
### 🛡️ Currently Supported Features (Production Features)
|
|
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
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
18
25
|
|
|
19
26
|
- 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` code anchors (comma-lists and every anchor in a file participate in the gate).
|
|
20
27
|
- 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion → green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
cc24589-mtrjegw0
|
|
@@ -897,7 +897,21 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
897
897
|
}
|
|
898
898
|
else {
|
|
899
899
|
const pin = (0, mcp_version_1.mcpLaunchVersion)({ command: entry.command, args: entry.args });
|
|
900
|
-
|
|
900
|
+
// @implements A-SPEC-569.4 — parity with the .mcp.json drift check (A-SPEC-251.2): a pin
|
|
901
|
+
// that RESOLVES is not enough, because a stale pin quietly keeps launching yesterday's
|
|
902
|
+
// gate. Same verdict function (non-semver pins like @latest judge 'unknown' → PASS, not
|
|
903
|
+
// an inverted "stale" WARN), same fix shape; an unreadable pin stays FAIL as before.
|
|
904
|
+
const cliVer = (0, mcp_launcher_1.readPackageVersion)(packageRoot); // same accessor as the .mcp.json check — one source of truth
|
|
905
|
+
const drift = cliVer ? (0, mcp_version_1.versionDriftVerdict)(pin, cliVer) : 'unknown';
|
|
906
|
+
if (pin === null) {
|
|
907
|
+
add('codex wiring', 'FAIL', `cannot read a launch version from the wiring: ${entry.command} ${entry.args.join(' ')}`, 'Rewire with holmes-kit init --target <dir> --agent codex.');
|
|
908
|
+
}
|
|
909
|
+
else if (drift === 'drift') {
|
|
910
|
+
add('codex wiring', 'WARN', `resolves via the npx pin ${pin}, but this install is ${cliVer} — a stale pin keeps launching the old server`, 'Rewire with holmes-kit init --target <dir> --agent codex --force to refresh the pin.');
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
add('codex wiring', 'PASS', `resolves via the npx pin ${pin}`);
|
|
914
|
+
}
|
|
901
915
|
}
|
|
902
916
|
// @implements A-SPEC-442 (was A-SPEC-423)
|
|
903
917
|
// Codex CAN hard-enforce, but only from an INSTALLED plugin: it loads plugins from
|
|
@@ -19,5 +19,8 @@ export interface McpEntryShape {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function mcpLaunchVersion(entry: McpEntryShape, readVersion?: (packageDir: string) => string | undefined): string | null;
|
|
21
21
|
export type DriftVerdict = 'match' | 'drift' | 'unknown';
|
|
22
|
-
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
22
|
+
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
23
|
+
* @implements A-SPEC-569.4 — 비-semver 런치 문자열(`latest`·`next`·`^0.16.0`)도 unknown:
|
|
24
|
+
* `@latest` 핀은 최신을 띄우는데 "구 서버를 계속 띄운다"는 drift 경고는 역진단이었다(고강도
|
|
25
|
+
* 리뷰 F5). 정확 semver 만 오프라인에서 비교 가능하다 — 판정 불능은 결함으로 둔갑시키지 않는다. */
|
|
23
26
|
export declare function versionDriftVerdict(launchVersion: string | null, cliVersion: string): DriftVerdict;
|
|
@@ -74,9 +74,12 @@ function mcpLaunchVersion(entry, readVersion) {
|
|
|
74
74
|
}
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
77
|
+
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
78
|
+
* @implements A-SPEC-569.4 — 비-semver 런치 문자열(`latest`·`next`·`^0.16.0`)도 unknown:
|
|
79
|
+
* `@latest` 핀은 최신을 띄우는데 "구 서버를 계속 띄운다"는 drift 경고는 역진단이었다(고강도
|
|
80
|
+
* 리뷰 F5). 정확 semver 만 오프라인에서 비교 가능하다 — 판정 불능은 결함으로 둔갑시키지 않는다. */
|
|
78
81
|
function versionDriftVerdict(launchVersion, cliVersion) {
|
|
79
|
-
if (launchVersion === null)
|
|
82
|
+
if (launchVersion === null || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(launchVersion))
|
|
80
83
|
return 'unknown';
|
|
81
84
|
return launchVersion === cliVersion ? 'match' : 'drift';
|
|
82
85
|
}
|
|
@@ -80,7 +80,22 @@ const TAINT_MARKERS = ['taint', 'dataflow-taint', 'flow-sensitive'];
|
|
|
80
80
|
// purpose, because a glob spanning the gate surface must never self-approve.
|
|
81
81
|
const GLOB_RISK_ROOTS = [...HIGH_RISK_PREFIXES, 'src/holmes/rtm/'];
|
|
82
82
|
function isHighRiskPath(p) {
|
|
83
|
-
|
|
83
|
+
// @implements A-SPEC-569.4 — '.' segments fold BEFORE the verdict: `src/holmes/./governance/x`
|
|
84
|
+
// dodged every prefix check while naming the gate surface exactly (REQ-556's remaining edge).
|
|
85
|
+
// Deliberately NOT `..`: folding that would impersonate path resolution this grader cannot do
|
|
86
|
+
// (shape is not location) — over-inclusion is allowed here, invented precision is not. The
|
|
87
|
+
// enforcer (matchesFtt) stays byte-literal, so a /./-token still admits nothing: only the
|
|
88
|
+
// grader's sight widened, never the gate's admission.
|
|
89
|
+
const unquoted = p.replace(/^["'`]|["'`]$/g, '');
|
|
90
|
+
// @implements A-SPEC-569.4 (revision, high-effort review F3) — a `..` segment is NOT folded
|
|
91
|
+
// (folding would impersonate path resolution this grader cannot do) but it IS flagged: the hook
|
|
92
|
+
// does not canonicalize relative paths, so `src/app/../holmes/governance/x.ts` stays admissible
|
|
93
|
+
// at the byte-literal enforcer under its own spelling while a fold-blind grader called it
|
|
94
|
+
// benign. An ascent token is ambiguous about where it lands, and ambiguity over the gate
|
|
95
|
+
// surface grades high-risk — over-inclusive on purpose, the rule this module already owns.
|
|
96
|
+
if (/(^|\/)\.\.(\/|$)/.test(unquoted))
|
|
97
|
+
return true;
|
|
98
|
+
const raw = unquoted.replace(/\/\.(?=\/|$)/g, '').replace(/^(\.\/)+/, '');
|
|
84
99
|
const s = raw.toLowerCase();
|
|
85
100
|
if (HIGH_RISK_PREFIXES.some((pre) => s.startsWith(pre)))
|
|
86
101
|
return true;
|
|
@@ -118,6 +133,7 @@ function specApprovalAutonomy(spec, _resolveParent) {
|
|
|
118
133
|
case 'REQ':
|
|
119
134
|
case 'H-SPEC':
|
|
120
135
|
case 'C-SPEC':
|
|
136
|
+
case 'ADR': // @implements A-SPEC-571.1 — a decision is the human's to seal
|
|
121
137
|
return 'hitl'; // wide blast radius / structural constraint
|
|
122
138
|
case 'A-SPEC': {
|
|
123
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;
|
|
@@ -272,6 +290,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
272
290
|
conflict: import("../spec/version-conflict").ConflictDetail;
|
|
273
291
|
findings?: undefined;
|
|
274
292
|
} | {
|
|
293
|
+
anchorDensity?: import("../rtm/anchor-density").AnchorDensityFinding[] | undefined;
|
|
275
294
|
impactAdvisory?: import("../rtm/impact-advisory").ImpactAdvisory | undefined;
|
|
276
295
|
approved: string;
|
|
277
296
|
digest: string;
|
|
@@ -530,11 +549,6 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
530
549
|
changed: string[];
|
|
531
550
|
}): Promise<{
|
|
532
551
|
breadthWarning?: string | undefined;
|
|
533
|
-
impacted: string[];
|
|
534
|
-
impactedSummaries: {
|
|
535
|
-
id: string;
|
|
536
|
-
summary: string | null;
|
|
537
|
-
}[];
|
|
538
552
|
rankedImpact: {
|
|
539
553
|
file: string;
|
|
540
554
|
score: number;
|
|
@@ -545,6 +559,12 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
545
559
|
reason: "hub" | "depth";
|
|
546
560
|
inDegree?: number;
|
|
547
561
|
}[] | undefined;
|
|
562
|
+
summariesOmitted?: number | undefined;
|
|
563
|
+
impacted: string[];
|
|
564
|
+
impactedSummaries: {
|
|
565
|
+
id: string;
|
|
566
|
+
summary: string | null;
|
|
567
|
+
}[];
|
|
548
568
|
}>;
|
|
549
569
|
rtm_reindex(a: {
|
|
550
570
|
root: 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
|
|
@@ -153,7 +154,14 @@ const cacheDirFor = (root) => {
|
|
|
153
154
|
// by an older build is rebuilt rather than read with new assumptions.
|
|
154
155
|
// @implements A-SPEC-568.1 — /3: nodes gained the intent `summary` column.
|
|
155
156
|
const RTM_GRAPH_SCHEMA = 'rtm-graph/3';
|
|
156
|
-
|
|
157
|
+
// @implements A-SPEC-569.3 — how many impacted specs get their intent sentence attached. A prose
|
|
158
|
+
// constant, never a verdict input: the impacted list itself is never truncated.
|
|
159
|
+
const SUMMARY_CAP = 40;
|
|
160
|
+
// @implements A-SPEC-569.1 (revision) — /2: pre-fix 0.16.0 builds could persist forged structural
|
|
161
|
+
// characters in the summary column, and every other basis field would still match after upgrading.
|
|
162
|
+
// A-SPEC-283's own rule applies to us too: an older build's artifact is rebuilt, never read with
|
|
163
|
+
// new assumptions.
|
|
164
|
+
const RTM_EXTRACTOR_VERSION = 'holmes-rtm/2';
|
|
157
165
|
const cachedScan = (root, repoRoot = root) => new cpg_scanner_1.CpgScanner(undefined, new scan_cache_1.ScanFileCache(cacheDirFor(root))).scan(root, repoRoot);
|
|
158
166
|
// @implements A-SPEC-131
|
|
159
167
|
// Same scan, with the skip report kept: the callers that make honesty claims (cpg_scan's surface,
|
|
@@ -692,8 +700,24 @@ function collectDecisions(root, scanned, specs) {
|
|
|
692
700
|
});
|
|
693
701
|
}
|
|
694
702
|
}
|
|
695
|
-
catch {
|
|
696
|
-
|
|
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);
|
|
697
721
|
}
|
|
698
722
|
const ids = new Set(decisions.map((d) => d.id));
|
|
699
723
|
// @implements A-SPEC-546.1 — recognise ADR-\d{3,} (4-digit ADRs no longer invisible), via a pure fn.
|
|
@@ -1323,7 +1347,58 @@ function makeRawHandlers(store, opts) {
|
|
|
1323
1347
|
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
1324
1348
|
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
1325
1349
|
}
|
|
1326
|
-
|
|
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 } : {}) };
|
|
1327
1402
|
},
|
|
1328
1403
|
/**
|
|
1329
1404
|
* @implements A-SPEC-538.3
|
|
@@ -1691,6 +1766,7 @@ function makeRawHandlers(store, opts) {
|
|
|
1691
1766
|
// parses or builds (scan:build measured 20~38x — an approval must not pay that), and every
|
|
1692
1767
|
// failure below degrades to "no advisory field" on an otherwise identical response.
|
|
1693
1768
|
let impactAdvisory;
|
|
1769
|
+
let anchorDensity;
|
|
1694
1770
|
try {
|
|
1695
1771
|
if (spec.type === 'A-SPEC' && a.root) {
|
|
1696
1772
|
const dbPath = path.join(a.root, '.ax', 'rtm.sqlite');
|
|
@@ -1698,33 +1774,61 @@ function makeRawHandlers(store, opts) {
|
|
|
1698
1774
|
const { declaredImpactGap, appendImpactAdvisory } = require('../rtm/impact-advisory');
|
|
1699
1775
|
const { filesToTouch } = require('../spec/compat-impact');
|
|
1700
1776
|
const { RtmGraph } = require('../rtm/rtm-graph');
|
|
1777
|
+
// Closed in finally (high-effort review F4): this handler lives in a long-running MCP
|
|
1778
|
+
// server, and an unclosed native handle per approval accumulates for the process
|
|
1779
|
+
// lifetime — and on Windows can hold rtm.sqlite locked against the next rebuild.
|
|
1701
1780
|
const graph = new RtmGraph(dbPath);
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
return null;
|
|
1707
|
-
} });
|
|
1708
|
-
if (gap) {
|
|
1709
|
-
const graphAsOf = (() => { try {
|
|
1710
|
-
return fs.statSync(dbPath).mtime.toISOString();
|
|
1781
|
+
try {
|
|
1782
|
+
const ftt = filesToTouch(candidate);
|
|
1783
|
+
const gap = declaredImpactGap(ftt, graph, (rel) => { try {
|
|
1784
|
+
return fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1711
1785
|
}
|
|
1712
1786
|
catch {
|
|
1713
|
-
return
|
|
1714
|
-
} })
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1787
|
+
return null;
|
|
1788
|
+
} });
|
|
1789
|
+
if (gap) {
|
|
1790
|
+
const graphAsOf = (() => { try {
|
|
1791
|
+
return fs.statSync(dbPath).mtime.toISOString();
|
|
1792
|
+
}
|
|
1793
|
+
catch {
|
|
1794
|
+
return undefined;
|
|
1795
|
+
} })();
|
|
1796
|
+
impactAdvisory = { ...gap, ...(graphAsOf ? { graphAsOf } : {}) };
|
|
1797
|
+
appendImpactAdvisory(a.root, {
|
|
1798
|
+
aspec: a.id, files: gap.files.map((f) => f.path), more: gap.more,
|
|
1799
|
+
...(graphAsOf ? { graphAsOf } : {}), ts: new Date().toISOString(),
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
// @implements A-SPEC-569.5 — anchor-density OBSERVATION, same reopened graph, same
|
|
1803
|
+
// no-scan contract, same lifecycle as the advisory above (observe → ledger → measure
|
|
1804
|
+
// before anyone proposes promotion). Never a verdict input: the seal is already done,
|
|
1805
|
+
// and its own failure degrades to "no field" on an otherwise identical response.
|
|
1806
|
+
try {
|
|
1807
|
+
const { anchorDensityFindings, appendAnchorDensity } = require('../rtm/anchor-density');
|
|
1808
|
+
const findings = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
|
|
1809
|
+
if (findings.length > 0) {
|
|
1810
|
+
anchorDensity = findings;
|
|
1811
|
+
appendAnchorDensity(a.root, {
|
|
1812
|
+
aspec: a.id, files: findings.map((f) => ({ path: f.path, anchors: f.anchors })),
|
|
1813
|
+
p90: findings[0].p90, ts: new Date().toISOString(),
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
catch {
|
|
1818
|
+
anchorDensity = undefined;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
finally {
|
|
1822
|
+
graph.close();
|
|
1720
1823
|
}
|
|
1721
1824
|
}
|
|
1722
1825
|
}
|
|
1723
1826
|
}
|
|
1724
1827
|
catch {
|
|
1725
1828
|
impactAdvisory = undefined;
|
|
1829
|
+
anchorDensity = undefined;
|
|
1726
1830
|
}
|
|
1727
|
-
return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}) };
|
|
1831
|
+
return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}), ...(anchorDensity ? { anchorDensity } : {}) };
|
|
1728
1832
|
},
|
|
1729
1833
|
async spec_list(a) {
|
|
1730
1834
|
assertSpecStoreReachable('spec_list', store, a.root); // @implements A-SPEC-419
|
|
@@ -2513,7 +2617,14 @@ function makeRawHandlers(store, opts) {
|
|
|
2513
2617
|
// was asked. Bind the derivation and use it.
|
|
2514
2618
|
const root = projectRootOf(a.root);
|
|
2515
2619
|
const scanned = cachedScan(root);
|
|
2516
|
-
|
|
2620
|
+
// @implements A-SPEC-569.2 — the impact/advisory graph is APPROVED-ONLY. A draft needs no
|
|
2621
|
+
// approval to exist, and the 0.16.0 adversarial round showed one reaching the agent-visible
|
|
2622
|
+
// channels (impacted closure, advisory anchor summaries) — the trust boundary for those
|
|
2623
|
+
// channels is the act of approval. NOT filterGoverned: that predicate passes drafts (it only
|
|
2624
|
+
// drops outdated/legacy), which is exactly what let this in. Diagnosis (rtm_check) and
|
|
2625
|
+
// matching (issue_localize / maintenance_analyze) keep their own populations — the replay
|
|
2626
|
+
// pins were measured on them.
|
|
2627
|
+
const specs = (await store.list()).filter((s) => s.status === 'approved');
|
|
2517
2628
|
// @implements A-SPEC-283
|
|
2518
2629
|
// Reuse the persisted graph when its basis still holds. Measured: on the warm path the graph
|
|
2519
2630
|
// build is ~81% of the cost and reopening is ~0ms. `scanDigest` is the field that makes this
|
|
@@ -2540,7 +2651,12 @@ function makeRawHandlers(store, opts) {
|
|
|
2540
2651
|
// explainImpact, not impactedBy: the bounds and the breadth signal must reach the caller.
|
|
2541
2652
|
// An impact set is not just a list — a broad one means "review the contract", and a consumer
|
|
2542
2653
|
// that cannot tell the difference will try to bundle two hundred call sites.
|
|
2543
|
-
const { specs:
|
|
2654
|
+
const { specs: impactedRaw, reachedByDepth, stoppedAt, seedIsHub } = (0, rtm_builder_1.explainImpact)(g, a.changed);
|
|
2655
|
+
// @implements A-SPEC-569.2 — the closure walks EDGES, and an implements edge is owned by
|
|
2656
|
+
// the code file, so an anchor naming a draft (or a spec nobody wrote) still emits one —
|
|
2657
|
+
// deliberately, for rtm_check's dangling diagnosis. The CHANNEL filter is node existence:
|
|
2658
|
+
// approved-only specs were given nodes above, so only sealed intent reaches the caller.
|
|
2659
|
+
const impacted = impactedRaw.filter((id) => g.hasNode(id));
|
|
2544
2660
|
// @implements A-SPEC-469 — the graded FILE surface beside the spec closure, same code path
|
|
2545
2661
|
// as the S-484 measurement (identity, not reimplementation). Seeds are the changed symbols'
|
|
2546
2662
|
// nodes; the files that own them are excluded — a prediction naming the change itself is
|
|
@@ -2554,7 +2670,11 @@ function makeRawHandlers(store, opts) {
|
|
|
2554
2670
|
// @implements A-SPEC-568.2 — the intent sentence beside every impacted spec id, same order
|
|
2555
2671
|
// as `impacted` (which stays a bare id list for its existing consumers). Information only:
|
|
2556
2672
|
// nothing reads it back into the walk, the ranking or any gate.
|
|
2557
|
-
|
|
2673
|
+
// @implements A-SPEC-569.3 — capped: measured on this repository, uncapped summaries were
|
|
2674
|
+
// 94% of a 104,706-byte response (a hub-grade impact of 339 specs). The omission is COUNTED,
|
|
2675
|
+
// never silent, and `impacted` itself stays complete — only the annotation is bounded.
|
|
2676
|
+
const shownSummaries = impacted.slice(0, SUMMARY_CAP);
|
|
2677
|
+
const impactedSummaries = shownSummaries.map((id) => {
|
|
2558
2678
|
let summary = null;
|
|
2559
2679
|
try {
|
|
2560
2680
|
summary = g.summaryOf(id);
|
|
@@ -2562,9 +2682,11 @@ function makeRawHandlers(store, opts) {
|
|
|
2562
2682
|
catch { /* summary stays null */ }
|
|
2563
2683
|
return { id, summary };
|
|
2564
2684
|
});
|
|
2685
|
+
const summariesOmitted = impacted.length - shownSummaries.length;
|
|
2565
2686
|
return {
|
|
2566
2687
|
impacted,
|
|
2567
2688
|
impactedSummaries,
|
|
2689
|
+
...(summariesOmitted > 0 ? { summariesOmitted } : {}),
|
|
2568
2690
|
rankedImpact,
|
|
2569
2691
|
reachedByDepth,
|
|
2570
2692
|
bounded: stoppedAt.length > 0 ? stoppedAt.slice(0, 20) : undefined,
|
|
@@ -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,
|