@holmes-lab/holmes-kit 0.1.7 → 0.1.8
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 +8 -0
- package/README.md +11 -7
- package/bin/holmes-mcp.js +0 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +50 -0
- package/dist/holmes/cli/ci-gate.d.ts +18 -0
- package/dist/holmes/cli/doctor.d.ts +44 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +18 -0
- package/dist/holmes/cli/governed-precondition.d.ts +27 -0
- package/dist/holmes/cli/index.d.ts +14 -0
- package/dist/holmes/cli/index.js +23 -1
- package/dist/holmes/cli/init.d.ts +69 -0
- package/dist/holmes/cli/interactive-prompt.d.ts +9 -0
- package/dist/holmes/cli/playbook-skills.d.ts +129 -0
- package/dist/holmes/cli/roles-readme.d.ts +12 -0
- package/dist/holmes/cli/serve.d.ts +14 -0
- package/dist/holmes/cli/serve.js +70 -0
- package/dist/holmes/cli/settings-merge.d.ts +66 -0
- package/dist/holmes/config/config.d.ts +13 -0
- package/dist/holmes/context/bundler.d.ts +40 -0
- package/dist/holmes/context/render.d.ts +9 -0
- package/dist/holmes/context/tiers.d.ts +54 -0
- package/dist/holmes/context/tokens.d.ts +2 -0
- package/dist/holmes/cpg/ast-mutation.d.ts +31 -0
- package/dist/holmes/cpg/ast-mutation.js +126 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +69 -0
- package/dist/holmes/cpg/dynamic-cpg.d.ts +18 -0
- package/dist/holmes/cpg/dynamic-cpg.js +82 -0
- package/dist/holmes/cpg/hash-cache.d.ts +21 -0
- package/dist/holmes/cpg/language-parser-walk.d.ts +31 -0
- package/dist/holmes/cpg/language-parser-worker.d.ts +1 -0
- package/dist/holmes/cpg/language-parser.d.ts +116 -0
- package/dist/holmes/cpg/program-slicing.d.ts +21 -0
- package/dist/holmes/cpg/program-slicing.js +123 -0
- package/dist/holmes/cpg/scan-cache.d.ts +21 -0
- package/dist/holmes/cpg/source-path.d.ts +2 -0
- package/dist/holmes/cpg/test-files.d.ts +36 -0
- package/dist/holmes/governance/constitution-debt.d.ts +3 -0
- package/dist/holmes/governance/constitution-report.d.ts +23 -0
- package/dist/holmes/governance/constitution.d.ts +61 -0
- package/dist/holmes/governance/identity.d.ts +28 -0
- package/dist/holmes/governance/ledger-lock.d.ts +55 -0
- package/dist/holmes/governance/ledger-store.conformance.d.ts +11 -0
- package/dist/holmes/governance/ledger-store.d.ts +114 -0
- package/dist/holmes/governance/progress-ledger.d.ts +25 -0
- package/dist/holmes/governance/provenance-chain.d.ts +154 -0
- package/dist/holmes/governance/provenance-ledger.d.ts +102 -0
- package/dist/holmes/governance/provenance-schema.d.ts +21 -0
- package/dist/holmes/governance/replica-id.d.ts +20 -0
- package/dist/holmes/governance/role-policy.d.ts +52 -0
- package/dist/holmes/governance/trust-score.d.ts +32 -0
- package/dist/holmes/guardrail/anchors.d.ts +8 -0
- package/dist/holmes/guardrail/blind-spots.d.ts +32 -0
- package/dist/holmes/guardrail/decision-ledger.d.ts +29 -0
- package/dist/holmes/guardrail/executable-artifact.d.ts +1 -0
- package/dist/holmes/guardrail/governance-history.d.ts +15 -0
- package/dist/holmes/guardrail/phase.d.ts +27 -0
- package/dist/holmes/guardrail/risk-classifier.d.ts +52 -0
- package/dist/holmes/guardrail/risk-gate.d.ts +77 -0
- package/dist/holmes/guardrail/risk-types.d.ts +48 -0
- package/dist/holmes/guardrail/tspec-state.d.ts +71 -0
- package/dist/holmes/guardrail/write-target.d.ts +77 -0
- package/dist/holmes/hooks/adapters/antigravity.d.ts +42 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +144 -0
- package/dist/holmes/hooks/stop.d.ts +91 -0
- package/dist/holmes/mcp/basis.d.ts +100 -0
- package/dist/holmes/mcp/handlers.d.ts +482 -0
- package/dist/holmes/mcp/server.d.ts +2 -0
- package/dist/holmes/mcp/stdio-client.d.ts +51 -0
- package/dist/holmes/mcp/supervisor.d.ts +48 -0
- package/dist/holmes/mcp/tool-schemas.d.ts +12 -0
- package/dist/holmes/mcp/validate-args.d.ts +27 -0
- package/dist/holmes/messages/registry.d.ts +23 -0
- package/dist/holmes/project/baseline.d.ts +66 -0
- package/dist/holmes/project/change-source.d.ts +89 -0
- package/dist/holmes/project/ignore.d.ts +33 -0
- package/dist/holmes/project/root.d.ts +34 -0
- package/dist/holmes/reverse/anchor.d.ts +47 -0
- package/dist/holmes/reverse/cluster.d.ts +45 -0
- package/dist/holmes/reverse/draft.d.ts +33 -0
- package/dist/holmes/reverse/dynamic-wiring.d.ts +18 -0
- package/dist/holmes/reverse/scan.d.ts +103 -0
- package/dist/holmes/reverse/surface.d.ts +91 -0
- package/dist/holmes/reverse/test-map.d.ts +91 -0
- package/dist/holmes/review/coverage.d.ts +21 -0
- package/dist/holmes/review/findings.d.ts +49 -0
- package/dist/holmes/review/package.d.ts +26 -0
- package/dist/holmes/review/review-targets.d.ts +28 -0
- package/dist/holmes/review/scope.d.ts +13 -0
- package/dist/holmes/review/test-evidence.d.ts +31 -0
- package/dist/holmes/review/test-runner.d.ts +141 -0
- package/dist/holmes/rtm/dataflow-taint.d.ts +81 -0
- package/dist/holmes/rtm/gap-analyzer.d.ts +28 -0
- package/dist/holmes/rtm/git-changes.d.ts +33 -0
- package/dist/holmes/rtm/heatmap.d.ts +21 -0
- package/dist/holmes/rtm/incremental.d.ts +38 -0
- package/dist/holmes/rtm/localize.d.ts +36 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +101 -0
- package/dist/holmes/rtm/rtm-check.d.ts +13 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +88 -0
- package/dist/holmes/rtm/taint.d.ts +35 -0
- package/dist/holmes/rtm/test-scope.d.ts +85 -0
- package/dist/holmes/server/dashboard.d.ts +30 -0
- package/dist/holmes/server/dashboard.js +1758 -0
- package/dist/holmes/spec/approval-blockers.d.ts +62 -0
- package/dist/holmes/spec/breaking-change.d.ts +42 -0
- package/dist/holmes/spec/legacy-format.d.ts +93 -0
- package/dist/holmes/spec/spec-digest.d.ts +29 -0
- package/dist/holmes/spec/spec-parser.d.ts +12 -0
- package/dist/holmes/spec/spec-store.conformance.d.ts +10 -0
- package/dist/holmes/spec/spec-store.d.ts +130 -0
- package/dist/holmes/spec/spec-types.d.ts +150 -0
- package/dist/holmes/spec/validator.d.ts +65 -0
- package/package.json +2 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partitions changed files into two sets: those the CpgScanner ingested
|
|
3
|
+
* (scanned) and those it skipped (unscanned).
|
|
4
|
+
*
|
|
5
|
+
* A changed file is considered "scanned" iff its repo-relative POSIX path
|
|
6
|
+
* is present in the scannedSourcePaths set. This provides an honest signal
|
|
7
|
+
* about what the review actually analyzed vs what it did not (REQ-124 gate 2a).
|
|
8
|
+
*
|
|
9
|
+
* @param changedFiles - List of repo-relative POSIX paths that changed
|
|
10
|
+
* @param scannedSourcePaths - Set of repo-relative POSIX paths the scanner ingested
|
|
11
|
+
* @returns Object with sorted, deduplicated scanned and unscanned arrays
|
|
12
|
+
*/
|
|
13
|
+
export declare function partitionChangedFiles(changedFiles: string[], scannedSourcePaths: string[]): {
|
|
14
|
+
scanned: string[];
|
|
15
|
+
unscanned: string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Human-readable description of what the CpgScanner ingests.
|
|
19
|
+
* Used to make the "unscanned" signal self-documenting in review output.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SCANNABLE_NOTE = "CpgScanner analyzes the TS/JS family (.ts/.mts/.cts/.tsx/.jsx/.js/.mjs/.cjs, incl. JSX), Python (.py), C# (.cs), Java (.java), Go (.go), Rust (.rs), and C++ (.cpp/.cc/.cxx/.hpp/.hh/.h) \u2014 the Aider-Polyglot + C# language set \u2014 extracting function/class/method symbols and @implements anchors. Excludes *.test.*/*.spec.*, Python (test_*.py/*_test.py) and Go (*_test.go) test files; skips node_modules/dist/.git. Call/import EDGES are extracted for TS/JS only (other languages: symbols + anchors, edges deferred). Other languages (e.g. Kotlin/Swift/Ruby/PHP) appear as unscanned.";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type Severity = 'critical' | 'important' | 'minor';
|
|
2
|
+
export interface Finding {
|
|
3
|
+
id: string;
|
|
4
|
+
severity: Severity;
|
|
5
|
+
category: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
specRef?: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
status: 'open' | 'resolved';
|
|
10
|
+
/**
|
|
11
|
+
* @implements A-SPEC-157
|
|
12
|
+
* The world this judgement was made in, as a basis digest.
|
|
13
|
+
*
|
|
14
|
+
* WRITTEN BY THE LEDGER, never by the caller. A judge that stamps its own basis turns the field
|
|
15
|
+
* from evidence into self-report — the same reason `HOLMES_ROLE` is out-of-band. Measured
|
|
16
|
+
* 2026-08-08: the live server answered "no impacted specs" for a commit touching 39 of them, so an
|
|
17
|
+
* agent recording "no problems" against a fresh-looking basis would have sealed that falsehood
|
|
18
|
+
* into the audit trail permanently.
|
|
19
|
+
*
|
|
20
|
+
* Absent on records written before this field existed. That absence is honest and is left alone.
|
|
21
|
+
*/
|
|
22
|
+
basis?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Append-only, spec/code-linked audit trail of review findings. Each call to
|
|
26
|
+
* `record` appends one JSON line per finding to a `.jsonl` file — never
|
|
27
|
+
* truncated, never rewritten — so the file itself is the durable evidence
|
|
28
|
+
* trail (Decision-Event style) that a diff-only review lacks.
|
|
29
|
+
*
|
|
30
|
+
* Ids and timestamps are caller-supplied: this ledger has no clock and no id
|
|
31
|
+
* generator, so recording is fully deterministic given its inputs.
|
|
32
|
+
*/
|
|
33
|
+
export declare class FindingsLedger {
|
|
34
|
+
private readonly path;
|
|
35
|
+
constructor(path: string);
|
|
36
|
+
/**
|
|
37
|
+
* @implements A-SPEC-157
|
|
38
|
+
* `basis` is a SEPARATE argument, not a field on the findings, so the caller cannot choose it.
|
|
39
|
+
* Optional because making it required would break every existing call site without declaring it
|
|
40
|
+
* (ADR-013 tier ③); when it is absent, no basis is written and that absence is observable.
|
|
41
|
+
*/
|
|
42
|
+
record(findings: Finding[], basis?: string): void;
|
|
43
|
+
list(filter?: {
|
|
44
|
+
status?: 'open' | 'resolved';
|
|
45
|
+
severity?: Severity;
|
|
46
|
+
}): Finding[];
|
|
47
|
+
/** Ordered-object serialization so identical findings always produce identical bytes. */
|
|
48
|
+
private static serialize;
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RtmGraph } from '../rtm/rtm-graph';
|
|
2
|
+
import { Spec } from '../spec/spec-parser';
|
|
3
|
+
import { ReviewScope } from './scope';
|
|
4
|
+
import { ContextBundle, ContentSource } from '../context/bundler';
|
|
5
|
+
export interface ReviewPackage {
|
|
6
|
+
scope: ReviewScope;
|
|
7
|
+
bundles: Array<{
|
|
8
|
+
seedId: string;
|
|
9
|
+
bundle: ContextBundle;
|
|
10
|
+
}>;
|
|
11
|
+
droppedSeeds: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Assembles a review package: the review scope (impacted specs, acceptance
|
|
15
|
+
* criteria, coverage gaps, etc.) plus a graph-scoped context bundle seeded at
|
|
16
|
+
* each impacted spec AND at each changed code symbol, so a reviewer sees the
|
|
17
|
+
* impacted specs, the changed code itself, and their neighborhoods — the
|
|
18
|
+
* surpass point over a diff-only review.
|
|
19
|
+
*
|
|
20
|
+
* Pure/deterministic given the injected content source: no Date, randomness,
|
|
21
|
+
* or LLM calls. Same inputs always yield a deep-equal ReviewPackage.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assembleReviewPackage(graph: RtmGraph, specs: Spec[], changedSymbols: string[], content: ContentSource, budget: number, opts?: {
|
|
24
|
+
maxSeeds?: number;
|
|
25
|
+
perSeedBudget?: number;
|
|
26
|
+
}): ReviewPackage;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
3
|
+
/** An outstanding P4 routing signal: a spec whose approved content moved, and the files to re-review. */
|
|
4
|
+
export interface ReviewNeeded {
|
|
5
|
+
spec: string;
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
files: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read the outstanding `review-needed` signals off the provenance chain. P4 does not auto-clear, so
|
|
12
|
+
* every recorded signal is currently "outstanding" — the surface lists them so a reviewer knows what
|
|
13
|
+
* moved and which files to examine. A missing/unreadable chain → [] (nothing to route), never a throw.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readReviewNeeded(ledgerFile: string): ReviewNeeded[];
|
|
16
|
+
/**
|
|
17
|
+
* The deterministic set of source files a targeted adversarial review must re-examine when a spec's
|
|
18
|
+
* approved content MOVES (P4). This is a ROUTING computation, not a verdict: it names WHERE to look,
|
|
19
|
+
* never whether the code is right — that judgment stays with the review layer / a human, which is the
|
|
20
|
+
* whole point of P4 (the decision layer checks form/existence/execution, never meaning).
|
|
21
|
+
*
|
|
22
|
+
* - An A-SPEC's targets are the source files whose scanned `@implements` names it (its direct anchors).
|
|
23
|
+
* - A REQ's or H-SPEC's targets are the union of its APPROVED direct A-SPEC children's anchors (one
|
|
24
|
+
* hop): a moved parent implicates the code under the approved slices beneath it.
|
|
25
|
+
*
|
|
26
|
+
* Pure, deduped, path-sorted — same (spec, anchor graph) → same set.
|
|
27
|
+
*/
|
|
28
|
+
export declare function anchoredForReview(specId: string, scanned: ScannedFile[], specs: Spec[]): string[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RtmGraph } from '../rtm/rtm-graph';
|
|
2
|
+
import { Spec } from '../spec/spec-parser';
|
|
3
|
+
export interface ReviewScope {
|
|
4
|
+
changedSymbols: string[];
|
|
5
|
+
impactedSpecs: string[];
|
|
6
|
+
acceptanceCriteria: Array<{
|
|
7
|
+
specId: string;
|
|
8
|
+
criteria: string;
|
|
9
|
+
}>;
|
|
10
|
+
unrequestedSymbols: string[];
|
|
11
|
+
coverageGaps: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function computeReviewScope(graph: RtmGraph, specs: Spec[], changedSymbols: string[]): ReviewScope;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXECUTION evidence for ART-4 coverage honesty (closes the standing "syntactic count is not
|
|
3
|
+
* execution proof" limit).
|
|
4
|
+
*
|
|
5
|
+
* countTestCases is a syntactic LOWER BOUND — it can be fabricated (`if(false){it(…)}`, aliasing).
|
|
6
|
+
* A real test RUN cannot: jest's per-file assertionResults list only cases that actually executed.
|
|
7
|
+
* This module turns a run into durable per-A-SPEC evidence, stored beside the other L4 ledgers (a
|
|
8
|
+
* write-gated surface), stamped with the git HEAD it was produced at so a STALE record can never
|
|
9
|
+
* vouch for changed code.
|
|
10
|
+
*
|
|
11
|
+
* Precedence used by the constitution: executed evidence for an A-SPEC (fresh) is AUTHORITATIVE —
|
|
12
|
+
* >0 satisfies ART-4, and an explicit 0 is a violation even if the syntactic count is high. An
|
|
13
|
+
* A-SPEC absent from the evidence falls back to the syntactic lower bound (a scoped run legitimately
|
|
14
|
+
* covers only part of the graph).
|
|
15
|
+
*
|
|
16
|
+
* Pure half: computeExecutedByAspec / isFresh (no clock, no I/O). I/O half: read/write, fail-open.
|
|
17
|
+
*/
|
|
18
|
+
export interface TestEvidence {
|
|
19
|
+
ts: string;
|
|
20
|
+
head: string;
|
|
21
|
+
tier: string;
|
|
22
|
+
passed: boolean;
|
|
23
|
+
executedByAspec: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
/** Map per-test-file executed counts onto the A-SPECs those files @implement (pure). */
|
|
26
|
+
export declare function computeExecutedByAspec(executedByFile: Record<string, number>, testAnchors: Record<string, string[]>): Record<string, number>;
|
|
27
|
+
/** Evidence is usable only for the exact commit it was produced at (pure). */
|
|
28
|
+
export declare function isFresh(ev: TestEvidence | undefined, currentHead: string): boolean;
|
|
29
|
+
export declare const EVIDENCE_FILE: string;
|
|
30
|
+
export declare function writeTestEvidence(root: string, ev: TestEvidence): boolean;
|
|
31
|
+
export declare function readTestEvidence(root: string): TestEvidence | undefined;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { RegressionTier, TestScope } from '../rtm/test-scope';
|
|
2
|
+
/**
|
|
3
|
+
* Operational wiring for the intelligent test-scope (target-arch §5): turn a TestScope decision into
|
|
4
|
+
* an actual test run. The PLAN is pure/deterministic (testable); the EXECUTE half shells out to jest.
|
|
5
|
+
*
|
|
6
|
+
* full -> run the entire suite (a foundational/wide change earns full regression)
|
|
7
|
+
* unit | partial -> run only the impacted test files (partial may widen later; today both are scoped)
|
|
8
|
+
*
|
|
9
|
+
* If a scoped run has no impacted test files, the plan is a no-op (nothing to run) — the caller decides
|
|
10
|
+
* whether that means "safe" or "add coverage" (a coverage gap is surfaced separately by review_scope).
|
|
11
|
+
*/
|
|
12
|
+
export interface TestRunPlan {
|
|
13
|
+
mode: 'full' | 'scoped' | 'noop';
|
|
14
|
+
testFiles: string[];
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function planTestRun(scope: Pick<TestScope, 'tier' | 'impactedTestFiles'>): TestRunPlan;
|
|
18
|
+
export interface TestRunResult {
|
|
19
|
+
tier: RegressionTier;
|
|
20
|
+
mode: TestRunPlan['mode'];
|
|
21
|
+
ranFiles: string[];
|
|
22
|
+
passed: boolean;
|
|
23
|
+
skipped: boolean;
|
|
24
|
+
tail: string;
|
|
25
|
+
/** repo-relative test file -> number of test cases that ACTUALLY EXECUTED. */
|
|
26
|
+
executedByFile?: Record<string, number>;
|
|
27
|
+
/**
|
|
28
|
+
* Test files whose ecosystem has no runner adapter yet, so nothing was executed for them. Reported
|
|
29
|
+
* rather than dropped: a scoped gate that silently ran only part of its scope has verified less
|
|
30
|
+
* than it claims, and the caller must be able to see that.
|
|
31
|
+
*/
|
|
32
|
+
unsupported?: string[];
|
|
33
|
+
/** Which adapters actually ran, for evidence provenance. */
|
|
34
|
+
ranWith?: string[];
|
|
35
|
+
}
|
|
36
|
+
export type Ecosystem = 'jest' | 'pytest' | 'go' | 'rust' | 'java' | 'dotnet';
|
|
37
|
+
/** Ecosystem of a test file, by extension/convention. `null` when no runner adapter exists for it. */
|
|
38
|
+
export declare function ecosystemOf(file: string): Ecosystem | null;
|
|
39
|
+
/**
|
|
40
|
+
* @implements A-SPEC-137.1
|
|
41
|
+
* Executed-test counts per Go package from `go test -json` output. A test is EXECUTED when a NAMED
|
|
42
|
+
* test (`Test` present) reaches `Action: 'pass'` or `'fail'` — a skip does not count (matching the
|
|
43
|
+
* jest/pytest rule), and a package-level result (no `Test`) is not a test case. Non-JSON lines (go
|
|
44
|
+
* interleaves build output) are ignored. Pure; empty/garbage input yields `{}` rather than throwing.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseGoTestJson(out: string): Record<string, number>;
|
|
47
|
+
/**
|
|
48
|
+
* Parse jest --json output into per-file EXECUTED case counts. This is the execution-proof upgrade
|
|
49
|
+
* over the syntactic countTestCases lower bound: a case only appears here if jest actually ran it
|
|
50
|
+
* (pending/skipped assertions are excluded), so `if(false){it(…)}`, aliasing, and other syntactic
|
|
51
|
+
* fabrications cannot produce evidence. Returns {} for unparseable output (caller falls back).
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseExecutedCounts(stdout: string, cwd: string): Record<string, number>;
|
|
54
|
+
/**
|
|
55
|
+
* Parse pytest's built-in JUnit XML into per-file EXECUTED case counts — the pytest analogue of
|
|
56
|
+
* parseExecutedCounts. `--junit-xml` ships with pytest core, so this needs no plugin, and it is the
|
|
57
|
+
* only pytest output that is both machine-readable and per-file.
|
|
58
|
+
*
|
|
59
|
+
* A `<testcase>` counts as executed unless it carries `<skipped>`, matching the jest rule where
|
|
60
|
+
* pending/skipped assertions are excluded. Errors and failures DO count: they executed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseJUnitXmlCounts(xml: string, ext?: string): Record<string, number>;
|
|
63
|
+
/**
|
|
64
|
+
* @implements A-SPEC-137.2
|
|
65
|
+
* Executed-test count from `cargo test` text output. cargo prints one `test <name> ... ok|FAILED|
|
|
66
|
+
* ignored` line per test across all test binaries (unit/integration/doctest); `ok` and `FAILED` are
|
|
67
|
+
* EXECUTED, `ignored` is the skip rule. The `test result:` summary line is not itself counted (the
|
|
68
|
+
* per-test lines are the evidence). Crate-granular — cargo does not name a passing test's source
|
|
69
|
+
* file. Pure; empty input → 0.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseCargoTest(out: string): number;
|
|
72
|
+
/**
|
|
73
|
+
* @implements A-SPEC-137.2
|
|
74
|
+
* Run the rust half of a plan. cargo runs the WHOLE crate (a crate is its smallest test unit), so
|
|
75
|
+
* scoped and full both invoke `cargo test` in the crate root; the crate total is credited to each
|
|
76
|
+
* input test file (crate-granular). A red suite still yields the count (cargo writes the test lines
|
|
77
|
+
* to stdout on failure). Absent toolchain → `unsupported`, never a pass. `opts.cargoPresent`
|
|
78
|
+
* overrides the probe for testing.
|
|
79
|
+
*/
|
|
80
|
+
export declare function runCargo(files: string[], _mode: TestRunPlan['mode'], cwd: string, opts?: {
|
|
81
|
+
cargoPresent?: boolean;
|
|
82
|
+
}): {
|
|
83
|
+
passed: boolean;
|
|
84
|
+
tail: string;
|
|
85
|
+
executed: Record<string, number>;
|
|
86
|
+
unsupported?: string[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* @implements A-SPEC-137.3
|
|
90
|
+
* Run the Java half of a plan via gradle or maven, taking evidence from the JUnit XML they write.
|
|
91
|
+
* The presence probe RUNS `java -version` (a `/usr/bin/java` shim with no JDK fails — PATH is not
|
|
92
|
+
* enough). Absent → `unsupported`, never a pass. On this machine (no JDK) only the degradation is
|
|
93
|
+
* exercised; the run+read path is reviewed but unverified here (see A-SPEC-137.3).
|
|
94
|
+
*/
|
|
95
|
+
export declare function runGradle(files: string[], _mode: TestRunPlan['mode'], cwd: string, opts?: {
|
|
96
|
+
javaPresent?: boolean;
|
|
97
|
+
}): {
|
|
98
|
+
passed: boolean;
|
|
99
|
+
tail: string;
|
|
100
|
+
executed: Record<string, number>;
|
|
101
|
+
unsupported?: string[];
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* @implements A-SPEC-137.3
|
|
105
|
+
* Run the C# half of a plan via `dotnet test` with the JUnit logger. The probe RUNS `dotnet
|
|
106
|
+
* --version`; absent → `unsupported`, never a pass. dotnet is absent on this machine, so only the
|
|
107
|
+
* degradation is exercised; the run+read path is reviewed but unverified here.
|
|
108
|
+
*/
|
|
109
|
+
export declare function runDotnet(files: string[], _mode: TestRunPlan['mode'], cwd: string, opts?: {
|
|
110
|
+
dotnetPresent?: boolean;
|
|
111
|
+
}): {
|
|
112
|
+
passed: boolean;
|
|
113
|
+
tail: string;
|
|
114
|
+
executed: Record<string, number>;
|
|
115
|
+
unsupported?: string[];
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* @implements A-SPEC-137.1
|
|
119
|
+
* Run the go half of a plan. Evidence is PACKAGE-granular (go test names a passing test's package,
|
|
120
|
+
* not its file), so each package's non-skip count is credited to its input `_test.go` files. A red
|
|
121
|
+
* suite still yields counts (go writes JSON events to stdout on failure too). Absent toolchain →
|
|
122
|
+
* `unsupported`, never a pass. `opts.goPresent` overrides the probe for testing.
|
|
123
|
+
*/
|
|
124
|
+
export declare function runGo(files: string[], mode: TestRunPlan['mode'], cwd: string, opts?: {
|
|
125
|
+
goPresent?: boolean;
|
|
126
|
+
}): {
|
|
127
|
+
passed: boolean;
|
|
128
|
+
tail: string;
|
|
129
|
+
executed: Record<string, number>;
|
|
130
|
+
unsupported?: string[];
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Execute a plan (I/O). Deterministic inputs → deterministic command; the pass/fail is the suite's.
|
|
134
|
+
* A no-op plan is `skipped: true, passed: true` (nothing to run is not a failure).
|
|
135
|
+
*
|
|
136
|
+
* Test files are grouped by ecosystem and each group runs under its own adapter, so a mixed repo is
|
|
137
|
+
* covered rather than silently reduced to whichever language happens to sort first. Files whose
|
|
138
|
+
* ecosystem has no adapter are returned in `unsupported` — never dropped quietly, because a scoped
|
|
139
|
+
* gate that ran part of its scope has verified less than it reports.
|
|
140
|
+
*/
|
|
141
|
+
export declare function runTestScope(scope: TestScope, cwd: string): TestRunResult;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DataFlowFacts } from '../cpg/language-parser';
|
|
2
|
+
/**
|
|
3
|
+
* Taint propagation over data-flow FACTS (REQ-140).
|
|
4
|
+
*
|
|
5
|
+
* This is the precise pass, as distinct from REQ-138's `taintReachability`, which reports call paths
|
|
6
|
+
* and cannot tell tainted data from sanitized data. On the requirement's own fixture:
|
|
7
|
+
*
|
|
8
|
+
* const raw = req.body.cmd;
|
|
9
|
+
* const safe = sanitize(raw);
|
|
10
|
+
* run(raw); // reported here
|
|
11
|
+
* run(safe); // NOT reported here — reachability reports both
|
|
12
|
+
*
|
|
13
|
+
* Both passes remain available; each states which it is via `kind`. Neither claims exploitability.
|
|
14
|
+
*/
|
|
15
|
+
/** One file's facts, keyed by the path the finding should name. */
|
|
16
|
+
export interface DataFlowFactSet extends Array<{
|
|
17
|
+
file: string;
|
|
18
|
+
facts: DataFlowFacts;
|
|
19
|
+
}> {
|
|
20
|
+
}
|
|
21
|
+
export interface DataFlowTaintConfig {
|
|
22
|
+
/** Case-insensitive substrings matched against a def's expression TEXT (`req.body`). */
|
|
23
|
+
sources: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Callee names matched EXACTLY (case-insensitively). `callee` is already a bare last segment, so
|
|
26
|
+
* an exact match is the right shape and a substring match is not.
|
|
27
|
+
*
|
|
28
|
+
* Measured on this repository with substring matching: every one of the four findings was a false
|
|
29
|
+
* positive, and two of them came from the matcher alone — `eval` matched `evaluateStop` and `exec`
|
|
30
|
+
* matched `computeExecutedByAspec`. Neither has anything to do with evaluation or execution.
|
|
31
|
+
* List the variants you mean (`exec`, `execSync`, `execFile`); a name is not a prefix.
|
|
32
|
+
*/
|
|
33
|
+
sinks: string[];
|
|
34
|
+
/** Callee names whose result is CLEAN whatever it reads — matched EXACTLY, same rule as `sinks`. */
|
|
35
|
+
sanitizers: string[];
|
|
36
|
+
/** Fixpoint bound; a run that stops here reports `converged: false`. Default 10. */
|
|
37
|
+
maxIterations?: number;
|
|
38
|
+
/** Reported-finding cap; the remainder is reported in `truncated`. Default 100. */
|
|
39
|
+
maxFindings?: number;
|
|
40
|
+
}
|
|
41
|
+
/** One step in the derivation of a finding — the rule that fired and what it tainted. */
|
|
42
|
+
export interface FlowStep {
|
|
43
|
+
rule: 'source' | 'assign' | 'arg-param' | 'return' | 'sink';
|
|
44
|
+
/** `fn::name`, or the sink call's identity for the final step. */
|
|
45
|
+
variable: string;
|
|
46
|
+
file: string;
|
|
47
|
+
line: number;
|
|
48
|
+
detail: string;
|
|
49
|
+
}
|
|
50
|
+
export interface DataFlowFinding {
|
|
51
|
+
file: string;
|
|
52
|
+
/** The qualified name of the function containing the sink call. */
|
|
53
|
+
fn: string;
|
|
54
|
+
callee: string;
|
|
55
|
+
line: number;
|
|
56
|
+
argIndex: number;
|
|
57
|
+
argText: string;
|
|
58
|
+
/** How the data got here: every step, in derivation order, naming the rule that fired. */
|
|
59
|
+
flow: FlowStep[];
|
|
60
|
+
}
|
|
61
|
+
export interface DataFlowTaintResult {
|
|
62
|
+
kind: 'data-flow';
|
|
63
|
+
findings: DataFlowFinding[];
|
|
64
|
+
/** Every tainted `fn::name`, sorted — the fixpoint's state, exposed so a hop can be tested directly. */
|
|
65
|
+
tainted: string[];
|
|
66
|
+
limits: string[];
|
|
67
|
+
truncated: number;
|
|
68
|
+
converged: boolean;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* What this pass does NOT model. Carried in every result, for the same reason REQ-138 carries
|
|
72
|
+
* `TAINT_LIMITS`: a screening signal that does not state its blind spots is read as a proof.
|
|
73
|
+
*/
|
|
74
|
+
export declare const DATAFLOW_LIMITS: readonly ["no field sensitivity (obj.a and obj.b are one value)", "no aliasing", "no path sensitivity", "no container/element tracking", "no reflection or dynamic dispatch", "sink matching is by callee NAME with no receiver type — RE.exec(s) is indistinguishable from child_process.exec(s)", "no anonymous-function parameters (arrow/function-expression params are not bound)", "not statement-order sensitive — a variable tainted anywhere in a function is tainted throughout it", "reports reachability of tainted data to a sink, never exploitability"];
|
|
75
|
+
/**
|
|
76
|
+
* Taint propagation to a fixpoint.
|
|
77
|
+
*
|
|
78
|
+
* Pure: no I/O, no clock, no randomness. Facts are visited in a stable order and findings are
|
|
79
|
+
* deduped and sorted, so the same input always produces byte-identical output.
|
|
80
|
+
*/
|
|
81
|
+
export declare function taintDataFlow(factSet: DataFlowFactSet, cfg: DataFlowTaintConfig): DataFlowTaintResult;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
3
|
+
/**
|
|
4
|
+
* L6 — implementation↔spec CONVERGENCE gap analyzer (target-architecture §7 L6).
|
|
5
|
+
*
|
|
6
|
+
* The convergence loop repeatedly detects where the implementation and the governed spec set have
|
|
7
|
+
* drifted apart, so each iteration can close a gap instead of guessing. Deterministic, graph-derived —
|
|
8
|
+
* spec-kit's converge / bkit's gap-analyzer made semantic via the CPG↔RTM link:
|
|
9
|
+
*
|
|
10
|
+
* unimplementedSpecs — approved A-SPECs no source file @implements (approved intent, no code)
|
|
11
|
+
* unanchoredFiles — scanned source files carrying NO @implements anchor (code outside governance;
|
|
12
|
+
* scope drift in the "unrequested work" direction)
|
|
13
|
+
* untestedSpecs — approved A-SPECs with implementing code but no anchored TEST file (the H1/
|
|
14
|
+
* ART-4 evidence view, precomputed here for the loop)
|
|
15
|
+
*
|
|
16
|
+
* Dangling anchors (code → nonexistent spec) are already rtmCheck's `dangling-implements`; this module
|
|
17
|
+
* covers the remaining drift directions. Pure: caller supplies the scan + specs (+ test anchors).
|
|
18
|
+
* ADVISORY by design — an unimplemented approved spec is NORMAL mid-development, so gaps feed the
|
|
19
|
+
* convergence loop (pick next work) and reviews, not a blocking gate.
|
|
20
|
+
*/
|
|
21
|
+
export interface ConvergenceGaps {
|
|
22
|
+
unimplementedSpecs: string[];
|
|
23
|
+
unanchoredFiles: string[];
|
|
24
|
+
untestedSpecs: string[];
|
|
25
|
+
/** convergence metric: 0 gaps -> 1.0; more gaps -> toward 0. Deterministic, monotone. */
|
|
26
|
+
score: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function computeConvergenceGaps(scanned: ScannedFile[], specs: Spec[], testAnchors?: Record<string, string[]>): ConvergenceGaps;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ChangeSet } from './incremental';
|
|
2
|
+
/**
|
|
3
|
+
* Pure parser for `git diff --name-status` output. Paths are already
|
|
4
|
+
* repo-relative POSIX paths as emitted by git — no path normalization here.
|
|
5
|
+
*
|
|
6
|
+
* Recognized status codes (one per tab-separated line):
|
|
7
|
+
* - `A\tpath` -> added
|
|
8
|
+
* - `M\tpath` -> modified
|
|
9
|
+
* - `D\tpath` -> deleted
|
|
10
|
+
* - `R<score>\tfrom\tto` -> renamed { from, to }
|
|
11
|
+
* - `C<score>\tfrom\tto` -> treated as an add of `to` (a copy introduces a
|
|
12
|
+
* new file at `to`; the original at `from` is untouched, so there is no
|
|
13
|
+
* corresponding delete/replace to emit for it).
|
|
14
|
+
*
|
|
15
|
+
* Blank lines are ignored. Unrecognized status codes are ignored rather than
|
|
16
|
+
* throwing, so a git version with an unfamiliar status letter degrades
|
|
17
|
+
* gracefully instead of crashing the whole reindex.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseNameStatus(raw: string): ChangeSet;
|
|
20
|
+
/**
|
|
21
|
+
* Rejects any git rev/ref that begins with `-`. Without this, a value like
|
|
22
|
+
* `--output=/victim/file` passed as `base`/`head` to `git diff` is parsed by
|
|
23
|
+
* git as an OPTION (not a positional ref), letting an attacker make git write
|
|
24
|
+
* or truncate an arbitrary file (audit B1, argument-injection). Defense in
|
|
25
|
+
* depth alongside the `--end-of-options` guard below.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertSafeRev(v: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Thin, side-effecting wrapper: invokes real git and hands the raw output to
|
|
30
|
+
* the pure parser above. Kept isolated here so `parseNameStatus` stays
|
|
31
|
+
* testable on a raw string with no process/filesystem dependency.
|
|
32
|
+
*/
|
|
33
|
+
export declare function gitChangedFiles(repoRoot: string, base: string, head: string): ChangeSet;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SpecSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
covered: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface HeatmapOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
theme?: 'dark' | 'light';
|
|
9
|
+
}
|
|
10
|
+
export interface HeatmapReport {
|
|
11
|
+
html: string;
|
|
12
|
+
coveragePct: number;
|
|
13
|
+
totalSpecs: number;
|
|
14
|
+
coveredSpecs: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a standalone interactive HTML/SVG Heatmap Report for RTM Traceability & Taint Coverage.
|
|
18
|
+
*
|
|
19
|
+
* @implements A-SPEC-212
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateRtmHeatmap(specs: SpecSummary[] | null | undefined, options?: HeatmapOptions): HeatmapReport;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { RtmGraph } from './rtm-graph';
|
|
2
|
+
import { Spec } from '../spec/spec-parser';
|
|
3
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
4
|
+
/** Paths are repo-relative POSIX paths. */
|
|
5
|
+
export interface ChangeSet {
|
|
6
|
+
added: string[];
|
|
7
|
+
modified: string[];
|
|
8
|
+
deleted: string[];
|
|
9
|
+
renamed: Array<{
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reduces every add/modify/delete/rename case to one primitive: replace file
|
|
16
|
+
* F's subgraph. Processing order is deterministic: deleted, renamed, added,
|
|
17
|
+
* modified.
|
|
18
|
+
*
|
|
19
|
+
* - deleted F: graph.removeBySource(F) — no re-add.
|
|
20
|
+
* - renamed {from,to}: graph.removeBySource(from); then `to` is treated as added.
|
|
21
|
+
* - added/modified F: graph.removeBySource(F); scanOne(F); if non-null,
|
|
22
|
+
* buildFileSubgraph(sf, specs, graph).
|
|
23
|
+
*
|
|
24
|
+
* removeBySource runs before any re-add — including for a pure add — so the
|
|
25
|
+
* operation is idempotent: re-applying the same ChangeSet has no net effect
|
|
26
|
+
* beyond the first application.
|
|
27
|
+
*
|
|
28
|
+
* SPEC-file changes (paths under .ax/specs/) are out of scope here: scanOne
|
|
29
|
+
* returns null for non-code files, so only their removeBySource side (a
|
|
30
|
+
* no-op, since SPEC nodes are keyed on the spec's own sourcePath — see
|
|
31
|
+
* buildRtm/rtm-builder) would run. Incremental SPEC-node updates are
|
|
32
|
+
* deferred to a full spec reload for now.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyIncremental(graph: RtmGraph, changes: ChangeSet, ctx: {
|
|
35
|
+
repoRoot: string;
|
|
36
|
+
specs: Spec[];
|
|
37
|
+
scanOne: (relPath: string) => ScannedFile | null;
|
|
38
|
+
}): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
3
|
+
/**
|
|
4
|
+
* N1 — issue LOCALIZATION engine v0 (target-architecture §7-N N1, first-class subsystem).
|
|
5
|
+
*
|
|
6
|
+
* Turns free-text issue/bug/task text into a STRUCTURED localization report — ranked candidate
|
|
7
|
+
* locations (files + symbols) with the evidence for each — as a reusable artifact, so downstream
|
|
8
|
+
* coding starts from a diagnosis instead of re-searching the repo every attempt (localization eats
|
|
9
|
+
* ~half of repair interaction cost — SweRank/SHERLOC motivation).
|
|
10
|
+
*
|
|
11
|
+
* v0 is the DETERMINISTIC core: lexical token matching over the CPG (symbol names, file paths) fused
|
|
12
|
+
* with the RTM spec link — issue terms that match a SPEC's title/sections pull in the files that
|
|
13
|
+
* @implements that spec (graph hop the lexical layer alone cannot make). No LLM, no clock, no I/O:
|
|
14
|
+
* same inputs → same report. The agentic multi-turn refinement layer (query reformulation, memory)
|
|
15
|
+
* is a documented later stage that builds ON this report format.
|
|
16
|
+
*/
|
|
17
|
+
export interface LocalizationHit {
|
|
18
|
+
file: string;
|
|
19
|
+
score: number;
|
|
20
|
+
matchedSymbols: string[];
|
|
21
|
+
viaSpecs: string[];
|
|
22
|
+
why: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface LocalizationReport {
|
|
25
|
+
terms: string[];
|
|
26
|
+
hits: LocalizationHit[];
|
|
27
|
+
matchedSpecs: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Significant tokens of the text, UNICODE-aware (review D2: the old [^a-z0-9] split dropped every
|
|
31
|
+
* non-ASCII codepoint, so Korean issue text — and this repo's Korean spec titles — tokenized to
|
|
32
|
+
* NOTHING and localization was a silent no-op). Letters/digits of any script are token chars;
|
|
33
|
+
* camelCase/snake are split; Latin tokens need length>=3, non-ASCII (e.g. Korean) length>=2.
|
|
34
|
+
*/
|
|
35
|
+
export declare function significantTerms(text: string): string[];
|
|
36
|
+
export declare function localizeIssue(issueText: string, scanned: ScannedFile[], specs: Spec[], topN?: number): LocalizationReport;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
import { RtmGraph } from './rtm-graph';
|
|
3
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
4
|
+
export interface BuildRtmOptions {
|
|
5
|
+
/** Resolve a spec id to its source file path (e.g. its .md file) for provenance tagging. */
|
|
6
|
+
specSourcePath?: (id: string) => string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Adds one scanned file's CODE nodes and `implements` edges to the graph,
|
|
10
|
+
* tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
|
|
11
|
+
* can later drop exactly this file's contribution (used by the incremental
|
|
12
|
+
* orchestrator — see Task 4).
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildFileSubgraph(f: ScannedFile, specs: Spec[], graph: RtmGraph): void;
|
|
15
|
+
export declare function buildRtm(specs: Spec[], scanned: ScannedFile[], graph: RtmGraph, opts?: BuildRtmOptions): void;
|
|
16
|
+
/**
|
|
17
|
+
* Resolve each file's recovered call relations to graph edges.
|
|
18
|
+
*
|
|
19
|
+
* A scanner emits `to` as a bare callee name, because a file cannot know where the callee lives.
|
|
20
|
+
* Resolution therefore happens here, where the whole scan is visible, under a deliberately strict
|
|
21
|
+
* rule — PRECISION OVER RECALL, per the D-CPG principle:
|
|
22
|
+
*
|
|
23
|
+
* 1. same file first — a call resolves to a definition in the calling file if one matches;
|
|
24
|
+
* 2. otherwise, only if the name is defined in EXACTLY ONE file across the scan;
|
|
25
|
+
* 3. otherwise nothing is emitted.
|
|
26
|
+
*
|
|
27
|
+
* Rule 3 is the important one. A `save()` defined in nine modules is genuinely ambiguous without type
|
|
28
|
+
* resolution, and guessing produces nine wrong edges that then poison every impact answer traversing
|
|
29
|
+
* them. A missing edge merely leaves a gap that L6 already surfaces; a wrong edge is silent.
|
|
30
|
+
*
|
|
31
|
+
* OWNERSHIP: the edge is tagged with the CALLER's file, which is the file whose text contains the
|
|
32
|
+
* call. Since both endpoints are `CODE:<qn>@<path>` ids, only the caller's file can ever emit this
|
|
33
|
+
* exact (src,dst,rel) triple — so the first-writer-wins hazard documented on RtmGraph cannot arise
|
|
34
|
+
* for call edges, and `removeBySource(caller)` drops exactly them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function addCallEdges(scanned: ScannedFile[], graph: RtmGraph): void;
|
|
37
|
+
/**
|
|
38
|
+
* Bounds on call-edge propagation (REQ-127). Defaults are constants, not laws — a project can tune
|
|
39
|
+
* them, and `explain` reports what each bound actually did.
|
|
40
|
+
*/
|
|
41
|
+
export interface ImpactOptions {
|
|
42
|
+
/**
|
|
43
|
+
* How many `calls` hops to walk backwards from the changed symbol. Unbounded reachability is not
|
|
44
|
+
* merely slow, it is useless: measured on a 240k-function layered graph, changing one hub reached
|
|
45
|
+
* 30% of the codebase, and a bundle that size destroys the token economy the harness exists to
|
|
46
|
+
* protect. 0 disables call propagation entirely (anchor-only, the pre-W4 behaviour).
|
|
47
|
+
*/
|
|
48
|
+
maxCallDepth?: number;
|
|
49
|
+
/**
|
|
50
|
+
* A symbol called from more than this many places is a HUB — a logger, a config accessor, an error
|
|
51
|
+
* constructor. Changing one genuinely does touch everything, which is exactly why propagating
|
|
52
|
+
* through it answers "the whole codebase" and helps nobody. Callers of a hub are still reported;
|
|
53
|
+
* the walk simply does not continue PAST it.
|
|
54
|
+
*/
|
|
55
|
+
hubInDegree?: number;
|
|
56
|
+
}
|
|
57
|
+
export declare const DEFAULT_IMPACT: Required<ImpactOptions>;
|
|
58
|
+
export interface ImpactExplanation {
|
|
59
|
+
specs: string[];
|
|
60
|
+
/** CODE nodes reached, by hop distance — 0 is the changed symbol itself. */
|
|
61
|
+
reachedByDepth: number[];
|
|
62
|
+
/** Symbols the walk refused to continue past, and why. Bounds must be visible, never silent. */
|
|
63
|
+
stoppedAt: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
reason: 'hub' | 'depth';
|
|
66
|
+
inDegree?: number;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Set when a CHANGED symbol is itself a hub — something called from more places than
|
|
70
|
+
* `hubInDegree`. Measured on a real target: one such symbol had 245 direct callers and its impact
|
|
71
|
+
* set covered 59% of all specs.
|
|
72
|
+
*
|
|
73
|
+
* That answer is CORRECT, and truncating it would be a lie. But it is also unusable as a context
|
|
74
|
+
* bundle, and no bound fixes that — tightening the hub threshold changed the maximum not at all,
|
|
75
|
+
* because the breadth is already there at depth 1. The two obvious designs are both wrong:
|
|
76
|
+
* damping the seed reports "nothing is affected" when a logger changes, while not damping it
|
|
77
|
+
* reports most of the codebase.
|
|
78
|
+
*
|
|
79
|
+
* So the size is reported instead of being forced down. A consumer that would otherwise bundle
|
|
80
|
+
* every impacted spec can see that this is a CONTRACT change — review the symbol's contract and
|
|
81
|
+
* its tests, not two hundred call sites — which is both the useful reading and the one that keeps
|
|
82
|
+
* the token economy intact.
|
|
83
|
+
*/
|
|
84
|
+
seedIsHub?: {
|
|
85
|
+
callers: number;
|
|
86
|
+
threshold: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Impact of changing a set of symbols, as the set of specs that could be affected.
|
|
91
|
+
*
|
|
92
|
+
* DIRECTION MATTERS. `implements` runs code -> spec and is followed FORWARD; `calls` runs caller ->
|
|
93
|
+
* callee and must be followed BACKWARD, because the things at risk when you change `f` are the
|
|
94
|
+
* things that CALL `f`, not the things `f` calls. Walking calls forward would answer a different
|
|
95
|
+
* question and quietly return the wrong set.
|
|
96
|
+
*
|
|
97
|
+
* Anchor-only impact (no call hops) is a strict subset of this, so the bounded walk can only ever
|
|
98
|
+
* ADD to what the previous implementation returned.
|
|
99
|
+
*/
|
|
100
|
+
export declare function impactedBy(graph: RtmGraph, changedSymbolQNames: string[], opts?: ImpactOptions): string[];
|
|
101
|
+
export declare function explainImpact(graph: RtmGraph, changedSymbolQNames: string[], opts?: ImpactOptions): ImpactExplanation;
|