@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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,75 @@ 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.19.0] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Cycle governance for the code graph** (REQ-574). The spec graph has been required to be acyclic
|
|
12
|
+
by constitution (ART-2) for a long time; the code graph never was, in the same repository. Now it
|
|
13
|
+
is governed at three moments, because each can do a different thing:
|
|
14
|
+
- **Guidance, before design** — the authoring playbooks carry "share types through a third module,
|
|
15
|
+
do not create cycles" (pinned by test, inherited by the installed skills). At approval time the
|
|
16
|
+
graph cannot see the cycle a design *would* create — import edges come from scanning real source
|
|
17
|
+
— so words are the only thing that works there.
|
|
18
|
+
- **Advisory, at design time** — `approval_status`'s `graphPreview` gains `cycles`: the cycles the
|
|
19
|
+
files you declared are **already** in, with each edge classified as `type-erasable` (TypeScript
|
|
20
|
+
deletes it — not a runtime cycle), `lazy-require` (a workaround someone already paid for) or
|
|
21
|
+
`eager-value`. The note says in words that this is not a prediction.
|
|
22
|
+
- **Ratchet, after the code exists** — the Stop hook's constitution gains ART-2's sibling on the
|
|
23
|
+
code graph. Ships in `track` (records, never blocks); `strict` promotion waits on the observation
|
|
24
|
+
ledger. Not absolute zero but a ratchet, and the escape is a **named exception**, never a
|
|
25
|
+
threshold — a project with legacy cycles must still be able to adopt the harness.
|
|
26
|
+
- **Architecture observation beside the design-time advisory** (REQ-574 S4): `graphPreview` gains
|
|
27
|
+
`architecture` — lines, symbols, longest function, fan-in and fan-out for each declared file.
|
|
28
|
+
Numbers only: no grade, no threshold, no participation in any verdict (a test pins the absence of
|
|
29
|
+
such a field, because one would grow into the gate the measurement does not support).
|
|
30
|
+
- **A symbol axis in the replay benchmark** (REQ-573 S3-instrument): commit-touched symbols are
|
|
31
|
+
derived from the parent-side changed lines, so a mechanism aimed at function-level selection can
|
|
32
|
+
finally be judged.
|
|
33
|
+
- **Paired-power instrument** (REQ-573 S5): `editSetF1` and `pairedPower` report σ and the
|
|
34
|
+
minimum detectable effect at 80% power, so a verdict can state whether the difference it rests on
|
|
35
|
+
was large enough to detect.
|
|
36
|
+
- **The publish procedure reaches the external doc surfaces** (REQ-575): after a successful release,
|
|
37
|
+
the repo README is synchronised to GitHub and the org profile README is **inspected** for
|
|
38
|
+
claim-vs-reality drift (never auto-corrected — positioning wording is a person's call). The target
|
|
39
|
+
is derived from `package.json`'s `repository`, never hardcoded, because this playbook installs
|
|
40
|
+
into consuming projects.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Candidate admission narrowed to files that could be the answer** (REQ-573 S1). The commit-prose
|
|
44
|
+
channel admitted whatever git history named, so ledger JSONL, build baselines and CHANGELOG took
|
|
45
|
+
emission slots: measured over 12 real requests, **76 of 120 candidate slots (63.3%)** went to files
|
|
46
|
+
that cannot be the answer, one of them at the head of the list. History-derived candidates are now
|
|
47
|
+
restricted to files that could be source, and vendored trees are demoted the way the lexical path
|
|
48
|
+
already demotes them. Measured after: **0%**.
|
|
49
|
+
- **`maintenance_analyze` candidates carry def-use-ranked symbols** (REQ-573 S3): inside a file the
|
|
50
|
+
search already found, the functions the request's terms reach through data flow come first.
|
|
51
|
+
Measured on this repository: symbol Top-10 recall **0.087 → 0.287**, precision **0.038 → 0.098**,
|
|
52
|
+
file-level axes unchanged, request latency +10.3%. On two other corpora it moved nothing — the
|
|
53
|
+
gain is conditional on the file layer already being right, and it is reported as conditional.
|
|
54
|
+
- **Ambiguous call targets are preserved instead of dropped** (REQ-573 S4): a call whose name is
|
|
55
|
+
defined in several places now emits candidate edges on a new `calls_ambiguous` relation
|
|
56
|
+
(22.6% of resolved edges were being discarded). Every certainty-requiring reader filters on
|
|
57
|
+
`rel='calls'`, so gates and advisories are unaffected. **The PPR weight for it stays 0**: the
|
|
58
|
+
weighted arm was measured on two corpora and rejected (one unchanged, one monotonically worse).
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- **Three import cycles in this repository, and the workarounds they forced** (REQ-574 S2). All
|
|
62
|
+
three had the same cure — move the shared thing to a module neither side owns — and two lazy
|
|
63
|
+
`require()` calls existed only to dodge them. Their absence is now the evidence the cycles are gone.
|
|
64
|
+
- **The benchmark measured a pipeline that does not ship** (REQ-573 S3-instrument): the replay
|
|
65
|
+
narrowed the commit-prose boost on its own, a filter the product never had, which is why a defect
|
|
66
|
+
worth 63.3% of the product's emitted slots read as six-decimal no movement. Removing it moved four
|
|
67
|
+
pins (recorded before/after in the commit); the product did not change.
|
|
68
|
+
|
|
69
|
+
### Notes
|
|
70
|
+
- `RRF_K` was swept over {5, 10, 20, 60} on two corpora and **kept at 60**: no value improved both
|
|
71
|
+
corpora on every metric. Worth recording is that the sweep on the *unrepaired* benchmark said
|
|
72
|
+
k=10 dominated — fixing the instrument reversed the adoption decision.
|
|
73
|
+
- Judgement axis, measured: harness+judgement 0.2946 vs grep+judgement 0.2867 in edit-set F1, with
|
|
74
|
+
σ 0.1113 and a minimum detectable effect of 0.0551. The observed 0.0080 is a seventh of what this
|
|
75
|
+
design could detect, so the advantage is **not distinguishable from zero** at n=32.
|
|
76
|
+
|
|
8
77
|
## [0.18.0] - 2026-09-08
|
|
9
78
|
|
|
10
79
|
Decisions become governed. A consuming project's operational decisions had been leaking into agent
|
package/README.md
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🔁 **Import cycles are governed, at the three moments that can each do something** *(new in 0.19.0)*: the spec graph has been required to be acyclic since early on; the code graph now is too. **Guidance** reaches the agent before it designs (the authoring playbooks say "share types through a third module", pinned by test). **A design-time advisory** — `approval_status`'s `graphPreview` gains `cycles` — names the cycles your declared files are *already* in, with each edge classified as `type-erasable` (TypeScript deletes it, so it is not a runtime cycle), `lazy-require` (a workaround someone already paid for) or `eager-value`; the note says in words that this is **not** a prediction, because at approval time the code that would create a cycle does not exist yet. **A ratchet** in the Stop hook's constitution catches new cycles after the code is written — shipped in `track` (records, never blocks), and the escape is a **named exception**, never a threshold, so a project carrying legacy cycles can still adopt the harness. This repository went from three cycles to zero, and the two lazy `require()` workarounds they forced are gone.
|
|
20
|
+
- 📐 **Size and fan-in, shown but never judged** *(new in 0.19.0)*: the same design-time advisory carries `architecture` — lines, symbols, longest function, fan-in and fan-out for each declared file. Numbers only: no grade, no threshold, no participation in any verdict. A test pins the *absence* of a severity field, because one would grow into the gate the evidence does not support (the size/defect correlation in the literature is largely "there is more code"). Practitioners still reject inspecting more than five things, so the number is worth seeing — the person decides.
|
|
21
|
+
- 🎯 **Candidates you could actually act on** *(new in 0.19.0)*: the commit-history channel used to admit whatever git named, so ledger files and build baselines took emission slots — measured, **63.3% of candidate slots** went to files that cannot be the answer, one at the head of the list. Now history-derived candidates must be able to be source, vendored trees are demoted, and inside a file the search already found, def-use ranking puts the functions the request reaches through data flow first (symbol Top-10 recall 0.087 → 0.287 on this repository; unchanged on two others, and reported as conditional rather than general).
|
|
19
22
|
- 🧭 **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
23
|
- 📜 **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
24
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0043954-mtspy09e
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GitHub target this project publishes to, read from `package.json`.
|
|
3
|
+
*
|
|
4
|
+
* Derived, never hardcoded: this playbook is installed INTO consuming projects, so a literal
|
|
5
|
+
* `owner/repo` here would make someone else's release overwrite ours. When the field is missing,
|
|
6
|
+
* malformed, or points somewhere other than GitHub the answer is `null` — the caller then SKIPS and
|
|
7
|
+
* says so, which is the honest outcome. A fallback would be a guess wearing a default's clothes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function repoTargetFrom(pkg: unknown): {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
} | null;
|
|
13
|
+
export interface ProfileDriftInput {
|
|
14
|
+
profileText: string;
|
|
15
|
+
/** The version live on the registry, or null when the lookup failed. */
|
|
16
|
+
npmVersion: string | null;
|
|
17
|
+
profilePushedAt: string | null;
|
|
18
|
+
lastReleaseAt: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sentences a person should read before the next release — never a rewrite, and never a grade.
|
|
22
|
+
*
|
|
23
|
+
* No severity, no score: attaching one is how an advisory becomes a gate, and the wording of a
|
|
24
|
+
* positioning document is not something a gate should decide. An empty list is a real answer, which
|
|
25
|
+
* is why the "current document" case is tested — a check that always speaks is not a check.
|
|
26
|
+
*/
|
|
27
|
+
export declare function profileDriftFindings(input: ProfileDriftInput): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-575.1
|
|
3
|
+
// Publishing reaches surfaces the release procedure never knew about. This repository has NO GitHub
|
|
4
|
+
// remote — origin is a local gitea — so three releases went out while the README that npm's
|
|
5
|
+
// `homepage` points at stayed frozen at 2026-08-31, and the org profile still said the project was
|
|
6
|
+
// a walking skeleton under construction.
|
|
7
|
+
//
|
|
8
|
+
// Two surfaces, two treatments. The repo README is a COPY of the local one (section-for-section
|
|
9
|
+
// identical, measured), so it is synchronised. The profile README is an independent positioning
|
|
10
|
+
// document, so it is only INSPECTED — rewriting someone's positioning is not a release step.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.repoTargetFrom = repoTargetFrom;
|
|
13
|
+
exports.profileDriftFindings = profileDriftFindings;
|
|
14
|
+
/**
|
|
15
|
+
* The GitHub target this project publishes to, read from `package.json`.
|
|
16
|
+
*
|
|
17
|
+
* Derived, never hardcoded: this playbook is installed INTO consuming projects, so a literal
|
|
18
|
+
* `owner/repo` here would make someone else's release overwrite ours. When the field is missing,
|
|
19
|
+
* malformed, or points somewhere other than GitHub the answer is `null` — the caller then SKIPS and
|
|
20
|
+
* says so, which is the honest outcome. A fallback would be a guess wearing a default's clothes.
|
|
21
|
+
*/
|
|
22
|
+
function repoTargetFrom(pkg) {
|
|
23
|
+
const repository = pkg?.repository;
|
|
24
|
+
const url = typeof repository === 'string' ? repository
|
|
25
|
+
: typeof repository?.url === 'string'
|
|
26
|
+
? repository.url
|
|
27
|
+
: null;
|
|
28
|
+
if (url === null)
|
|
29
|
+
return null;
|
|
30
|
+
// The three shapes npm accepts, plus the `git+` prefix and the optional `.git` suffix.
|
|
31
|
+
const m = /^(?:git\+)?(?:https?:\/\/github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim());
|
|
32
|
+
return m === null ? null : { owner: m[1], repo: m[2] };
|
|
33
|
+
}
|
|
34
|
+
/** Phrases that claim the project has not shipped yet. */
|
|
35
|
+
const PRE_RELEASE_CLAIMS = ['early stage', 'under construction', 'walking-skeleton', 'walking skeleton', 'bootstrap'];
|
|
36
|
+
/**
|
|
37
|
+
* Sentences a person should read before the next release — never a rewrite, and never a grade.
|
|
38
|
+
*
|
|
39
|
+
* No severity, no score: attaching one is how an advisory becomes a gate, and the wording of a
|
|
40
|
+
* positioning document is not something a gate should decide. An empty list is a real answer, which
|
|
41
|
+
* is why the "current document" case is tested — a check that always speaks is not a check.
|
|
42
|
+
*/
|
|
43
|
+
function profileDriftFindings(input) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const text = input.profileText.toLowerCase();
|
|
46
|
+
// Only run the maturity check when the registry actually answered. Not knowing must not turn into
|
|
47
|
+
// a claim about what the document gets wrong.
|
|
48
|
+
if (input.npmVersion !== null && isShipped(input.npmVersion)) {
|
|
49
|
+
const claimed = PRE_RELEASE_CLAIMS.filter((p) => text.includes(p));
|
|
50
|
+
if (claimed.length > 0) {
|
|
51
|
+
out.push(`profile README still claims pre-release status (${claimed.join(', ')}) `
|
|
52
|
+
+ `while npm has ${input.npmVersion} — the claim is no longer true.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (input.profilePushedAt !== null && input.lastReleaseAt !== null
|
|
56
|
+
&& Date.parse(input.profilePushedAt) < Date.parse(input.lastReleaseAt)) {
|
|
57
|
+
out.push(`profile README (${input.profilePushedAt}) is older than the last release `
|
|
58
|
+
+ `(${input.lastReleaseAt}) — it has not been looked at across at least one shipment.`);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** A version that means "this shipped" — anything at or past 0.1.0. */
|
|
63
|
+
function isShipped(version) {
|
|
64
|
+
const m = /^(\d+)\.(\d+)\./.exec(version);
|
|
65
|
+
if (m === null)
|
|
66
|
+
return false;
|
|
67
|
+
return Number(m[1]) > 0 || Number(m[2]) > 0;
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ArchObservation {
|
|
2
|
+
path: string;
|
|
3
|
+
lines: number | null;
|
|
4
|
+
symbols: number;
|
|
5
|
+
longestFunction: number | null;
|
|
6
|
+
fanIn: number;
|
|
7
|
+
fanOut: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function architectureObservation(files: readonly string[], symbolsByFile: ReadonlyMap<string, Array<{
|
|
10
|
+
startLine: number;
|
|
11
|
+
endLine: number;
|
|
12
|
+
}>>, importEdges: readonly {
|
|
13
|
+
from: string;
|
|
14
|
+
to: string;
|
|
15
|
+
}[], readLines: (file: string) => number | null): ArchObservation[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.architectureObservation = architectureObservation;
|
|
4
|
+
function architectureObservation(files, symbolsByFile, importEdges, readLines) {
|
|
5
|
+
return [...files].sort().map((path) => {
|
|
6
|
+
const syms = symbolsByFile.get(path) ?? [];
|
|
7
|
+
const spans = syms.map((s) => s.endLine - s.startLine + 1);
|
|
8
|
+
return {
|
|
9
|
+
path,
|
|
10
|
+
// null, not 0: a file we could not read is a different fact from an empty file, and
|
|
11
|
+
// collapsing the two is how a report starts lying quietly.
|
|
12
|
+
lines: readLines(path),
|
|
13
|
+
symbols: syms.length,
|
|
14
|
+
longestFunction: spans.length === 0 ? null : Math.max(...spans),
|
|
15
|
+
fanIn: importEdges.filter((e) => e.to === path).length,
|
|
16
|
+
fanOut: importEdges.filter((e) => e.from === path).length,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -1,39 +1,6 @@
|
|
|
1
|
-
import { LanguageParser
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/** Repo-root-relative POSIX path — provenance key used by RtmGraph.removeBySource. */
|
|
5
|
-
sourcePath: string;
|
|
6
|
-
symbols: CodeSymbol[];
|
|
7
|
-
/**
|
|
8
|
-
* Call/import relations recovered from this file, with `from` a qualified name defined HERE and
|
|
9
|
-
* `to` a bare callee name or module path. Resolution to graph node ids happens in rtm-builder,
|
|
10
|
-
* where the whole scan is visible; a single file cannot know where a callee lives.
|
|
11
|
-
*
|
|
12
|
-
* Optional so a cache entry written by an older scanner still loads — such an entry simply carries
|
|
13
|
-
* no edges, which degrades to the previous anchor-only behaviour rather than crashing.
|
|
14
|
-
*/
|
|
15
|
-
edges?: CodeEdge[];
|
|
16
|
-
/**
|
|
17
|
-
* Def-use facts, present ONLY when the scanner was constructed with `{ dataflow: true }` AND the
|
|
18
|
-
* file's language has a data-flow walk.
|
|
19
|
-
*
|
|
20
|
-
* @implements A-SPEC-140.1
|
|
21
|
-
* Opt-in by measurement, not taste: the scan already parses each file twice (symbols, edges), and
|
|
22
|
-
* an unconditional third parse would regress the REQ-139 cold-scan baseline by ~50% to produce a
|
|
23
|
-
* signal only the taint pass reads. `undefined` therefore means "not extracted" — for the option
|
|
24
|
-
* being off, for a language with no walk, or for a cache entry from an older writer. It never
|
|
25
|
-
* means "extracted, found nothing"; that is `{defs:[],calls:[],params:[],returns:[]}`.
|
|
26
|
-
*/
|
|
27
|
-
dataflow?: DataFlowFacts;
|
|
28
|
-
implementsSpecs: string[];
|
|
29
|
-
/**
|
|
30
|
-
* `@implements` tags shaped like a well-formed governed spec id (REQ/H-SPEC/
|
|
31
|
-
* C-SPEC/T-SPEC-NNN) instead of an A-SPEC anchor — a wrong-kind anchor that
|
|
32
|
-
* would otherwise be silently dropped (audit finding D). Populated
|
|
33
|
-
* alongside `implementsSpecs`, never overlapping with it.
|
|
34
|
-
*/
|
|
35
|
-
unanchoredImplements: string[];
|
|
36
|
-
}
|
|
1
|
+
import { LanguageParser } from './language-parser';
|
|
2
|
+
import type { ScannedFile } from './scanned-file';
|
|
3
|
+
export type { ScannedFile } from './scanned-file';
|
|
37
4
|
/**
|
|
38
5
|
* Directories that never contain hand-written source for the project under analysis.
|
|
39
6
|
*
|
|
@@ -71,6 +38,13 @@ export declare const SKIP: Set<string>;
|
|
|
71
38
|
*/
|
|
72
39
|
export declare function isUnderSkippedDir(p: unknown): boolean;
|
|
73
40
|
/** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
|
|
41
|
+
/**
|
|
42
|
+
* Extension -> grammar. Exported because a SECOND consumer now reads data flow (the def-use symbol
|
|
43
|
+
* lane), and a second copy of this rule is a second thing to drift — the first wiring omitted the
|
|
44
|
+
* language entirely and parsed Python as TypeScript, which cost the other corpus 0.2376 -> 0.1741
|
|
45
|
+
* on the symbol axis before the instrument caught it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function langForPath(name: string): 'python' | 'csharp' | 'java' | 'go' | 'rust' | 'cpp' | 'tsx' | 'typescript';
|
|
74
48
|
export declare const SCANNABLE_EXTENSIONS: readonly [".ts", ".mts", ".cts", ".tsx", ".jsx", ".js", ".mjs", ".cjs", ".py", ".cs", ".java", ".go", ".rs", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".h"];
|
|
75
49
|
export { TEST_FILE_PATTERNS, isTestFile } from './test-files';
|
|
76
50
|
export declare class CpgScanner {
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CpgScanner = exports.isTestFile = exports.TEST_FILE_PATTERNS = exports.SCANNABLE_EXTENSIONS = exports.SKIP = void 0;
|
|
37
37
|
exports.isUnderSkippedDir = isUnderSkippedDir;
|
|
38
|
+
exports.langForPath = langForPath;
|
|
38
39
|
// @implements A-SPEC-208
|
|
39
40
|
const fs = __importStar(require("node:fs"));
|
|
40
41
|
const path = __importStar(require("node:path"));
|
|
@@ -109,6 +110,28 @@ function isVendorDir(dir) {
|
|
|
109
110
|
// Anchor parsing (incl. wrong-kind detection) moved to the shared comma-list parser in
|
|
110
111
|
// rtm/anchor-ids (A-SPEC-503.1) — the local single-capture regexes dropped every id after a comma.
|
|
111
112
|
/** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
|
|
113
|
+
// @implements A-SPEC-573.4
|
|
114
|
+
/**
|
|
115
|
+
* Extension -> grammar. Exported because a SECOND consumer now reads data flow (the def-use symbol
|
|
116
|
+
* lane), and a second copy of this rule is a second thing to drift — the first wiring omitted the
|
|
117
|
+
* language entirely and parsed Python as TypeScript, which cost the other corpus 0.2376 -> 0.1741
|
|
118
|
+
* on the symbol axis before the instrument caught it.
|
|
119
|
+
*/
|
|
120
|
+
function langForPath(name) {
|
|
121
|
+
if (/\.py$/.test(name))
|
|
122
|
+
return 'python';
|
|
123
|
+
if (/\.cs$/.test(name))
|
|
124
|
+
return 'csharp';
|
|
125
|
+
if (/\.java$/.test(name))
|
|
126
|
+
return 'java';
|
|
127
|
+
if (/\.go$/.test(name))
|
|
128
|
+
return 'go';
|
|
129
|
+
if (/\.rs$/.test(name))
|
|
130
|
+
return 'rust';
|
|
131
|
+
if (/\.(cpp|cc|cxx|hpp|hh|h)$/.test(name))
|
|
132
|
+
return 'cpp';
|
|
133
|
+
return TSX_GRAMMAR_RE.test(name) ? 'tsx' : 'typescript';
|
|
134
|
+
}
|
|
112
135
|
exports.SCANNABLE_EXTENSIONS = ['.ts', '.mts', '.cts', '.tsx', '.jsx', '.js', '.mjs', '.cjs', '.py', '.cs', '.java', '.go', '.rs', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.h'];
|
|
113
136
|
const SCANNABLE_RE = /\.(ts|mts|cts|tsx|jsx|js|mjs|cjs|py|cs|java|go|rs|cpp|cc|cxx|hpp|hh|h)$/;
|
|
114
137
|
// The test-file table moved to ./test-files, so the PreToolUse phase gate can read the SAME rules
|
|
@@ -118,6 +141,8 @@ var test_files_1 = require("./test-files");
|
|
|
118
141
|
Object.defineProperty(exports, "TEST_FILE_PATTERNS", { enumerable: true, get: function () { return test_files_1.TEST_FILE_PATTERNS; } });
|
|
119
142
|
Object.defineProperty(exports, "isTestFile", { enumerable: true, get: function () { return test_files_1.isTestFile; } });
|
|
120
143
|
const test_files_2 = require("./test-files");
|
|
144
|
+
// @implements A-SPEC-574.3 — a plain import now: the cycle that forced a lazy require is gone.
|
|
145
|
+
const scan_cache_1 = require("./scan-cache");
|
|
121
146
|
const anchor_ids_1 = require("../rtm/anchor-ids");
|
|
122
147
|
// .ts/.mts/.cts can contain TS type-assertion syntax (`<T>x`, arrow-generics)
|
|
123
148
|
// that the 'tsx' grammar misreads as JSX, so they must stay on 'typescript'.
|
|
@@ -208,7 +233,7 @@ class CpgScanner {
|
|
|
208
233
|
// C++ grammar parses plain C headers acceptably for our lexical
|
|
209
234
|
// symbol walk, and disambiguating a bare `.h` between C and C++
|
|
210
235
|
// is out of scope for this slice.
|
|
211
|
-
const lang =
|
|
236
|
+
const lang = langForPath(e.name);
|
|
212
237
|
// @implements A-SPEC-503.1 — comma-listed anchors: the old single-capture regexes
|
|
213
238
|
// dropped every id after the first (C12), and a wrong-kind id in second position was
|
|
214
239
|
// invisible to the warning. Both consumers now ride the shared list parser.
|
|
@@ -221,8 +246,7 @@ class CpgScanner {
|
|
|
221
246
|
const dataflow = () => (this.opts.dataflow ? this.parser.extractDataFlow(code, lang) : undefined);
|
|
222
247
|
// N7 incremental: on a content-hash hit, reuse cached symbols and skip the parse.
|
|
223
248
|
if (this.cache) {
|
|
224
|
-
const
|
|
225
|
-
const h = ScanFileCache.hashOf(code);
|
|
249
|
+
const h = scan_cache_1.ScanFileCache.hashOf(code);
|
|
226
250
|
const hit = this.cache.get(sourcePath, h);
|
|
227
251
|
// @implements A-SPEC-140.1
|
|
228
252
|
// A hit written by a scan that did NOT extract data-flow cannot satisfy one that does:
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type EdgeKind = 'type-erasable' | 'lazy-require' | 'eager-value';
|
|
2
|
+
export interface CycleEdge {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
kind: EdgeKind;
|
|
6
|
+
}
|
|
7
|
+
export interface Cycle {
|
|
8
|
+
/** The files in the cycle, sorted, so two runs report it the same way. */
|
|
9
|
+
files: string[];
|
|
10
|
+
edges: CycleEdge[];
|
|
11
|
+
/** True unless EVERY edge holding the cycle together is erased at compile time. */
|
|
12
|
+
runtime: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* What survives compilation for one import edge.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately conservative: a shape this cannot read is `eager-value`, never `type-erasable`.
|
|
18
|
+
* Reporting "this disappears at compile time" about something we did not understand is the one
|
|
19
|
+
* error that silently deletes a real cycle from the count.
|
|
20
|
+
*/
|
|
21
|
+
export declare function classifyImportEdge(source: string, specifier: string): EdgeKind;
|
|
22
|
+
/**
|
|
23
|
+
* Strongly connected components of size > 1 — Tarjan, iterative order-independent output.
|
|
24
|
+
*
|
|
25
|
+
* A self-edge is not a cycle: a file importing itself is a parse artefact, not a design defect.
|
|
26
|
+
*/
|
|
27
|
+
export declare function detectCycles(edges: readonly CycleEdge[]): {
|
|
28
|
+
cycles: Cycle[];
|
|
29
|
+
runtimeCycles: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* @implements A-SPEC-574.2
|
|
33
|
+
* The wording that ships with the advisory. Fixed here, not composed at the call site, so the one
|
|
34
|
+
* thing this surface must never claim cannot drift: it reports cycles that ALREADY exist among the
|
|
35
|
+
* declared files. What a design is about to create is not visible until the code is written.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CYCLE_ADVISORY_NOTE: string;
|
|
38
|
+
/**
|
|
39
|
+
* @implements A-SPEC-574.2
|
|
40
|
+
* Classify the edge `fromFile -> toFile` by finding the import statement that actually resolves to
|
|
41
|
+
* that target.
|
|
42
|
+
*
|
|
43
|
+
* Picking the statement by the target's BASENAME would classify the wrong import whenever two files
|
|
44
|
+
* share a name in different directories — which is ordinary in any tree with `util.ts` or
|
|
45
|
+
* `index.ts`. The specifier is resolved instead, the way the module system does it.
|
|
46
|
+
*
|
|
47
|
+
* A target no specifier in this source resolves to is `eager-value`: not finding it is not evidence
|
|
48
|
+
* that it disappears at compile time.
|
|
49
|
+
*/
|
|
50
|
+
export declare function classifyEdgeByTarget(source: string, fromFile: string, toFile: string): EdgeKind;
|
|
51
|
+
export interface CycleFinding extends Cycle {
|
|
52
|
+
/** The declared files that put this cycle in front of the author. */
|
|
53
|
+
declared: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @implements A-SPEC-574.2
|
|
57
|
+
* The cycles the DECLARED files are already in — read before the Files-to-Touch are frozen.
|
|
58
|
+
*
|
|
59
|
+
* This is not a prediction and must never be presented as one. Import edges come from scanning real
|
|
60
|
+
* source, so the cycle a design would CREATE does not exist to be seen at approval time; what can
|
|
61
|
+
* be seen is that a file the author is about to edit already sits in one. Selling the first as the
|
|
62
|
+
* second would be worse than silence.
|
|
63
|
+
*
|
|
64
|
+
* Calls `detectCycles` rather than computing its own: the advisory and the later verdict have to be
|
|
65
|
+
* the same function, or "what we warned" and "what we judge" drift apart (A-SPEC-572.1).
|
|
66
|
+
*/
|
|
67
|
+
export declare function cycleAdvisory(fttFiles: readonly string[], edges: readonly CycleEdge[]): CycleFinding[];
|
|
68
|
+
export interface CycleViolation {
|
|
69
|
+
key: string;
|
|
70
|
+
files: string[];
|
|
71
|
+
detail: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @implements A-SPEC-574.4
|
|
75
|
+
* The runtime cycles nobody allowed — a RATCHET, not an absolute zero.
|
|
76
|
+
*
|
|
77
|
+
* Absolute zero would keep every project with a legacy cycle from adopting the harness at all, and
|
|
78
|
+
* a rule that blocks adoption is a barrier rather than a rule. The escape is a NAME in the allow
|
|
79
|
+
* list, never a threshold: naming a cycle is a decision someone signed, while a threshold is a
|
|
80
|
+
* number anyone can move.
|
|
81
|
+
*
|
|
82
|
+
* Type-erasable-only cycles are not counted: TypeScript deletes those imports, so refusing them
|
|
83
|
+
* would refuse something that does not exist at runtime.
|
|
84
|
+
*/
|
|
85
|
+
export declare function cycleRatchetViolations(current: readonly Cycle[], allowed: readonly string[]): CycleViolation[];
|
|
86
|
+
/** Order-independent identity for a cycle, so the allow list cannot be defeated by a reordering. */
|
|
87
|
+
export declare function cycleKey(files: readonly string[]): string;
|