@holmes-lab/holmes-kit 0.1.6 → 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 +14 -0
- package/README.md +13 -9
- 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 +5 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A NAMED reference point: "the tree as it was when this label was taken".
|
|
3
|
+
*
|
|
4
|
+
* This is the piece that makes change analysis possible without version control, and it adds no new
|
|
5
|
+
* machinery — `HashCache` already persists sha256-per-file and `ScanFileCache` stores the same hash
|
|
6
|
+
* beside each file's symbols. What was missing is a NAME, so a caller can ask "what changed since the
|
|
7
|
+
* last verified state" instead of "what changed since HEAD~1".
|
|
8
|
+
*
|
|
9
|
+
* That naming is also why this is an improvement INSIDE a git project. `HEAD~1` is a proxy for "what I
|
|
10
|
+
* have been working on" that depends entirely on commit habits — ten commits or none change the
|
|
11
|
+
* answer. `last-green`, written only when a run actually passed, does not.
|
|
12
|
+
*
|
|
13
|
+
* Storage is derived state under `.ax/state/`: reconstructable, machine-local, gitignored. It is not
|
|
14
|
+
* project content and must never be committed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BASELINE_DIR: string;
|
|
17
|
+
export interface Baseline {
|
|
18
|
+
label: string;
|
|
19
|
+
/** ISO timestamp the baseline was taken. Informational — never an input to a change set. */
|
|
20
|
+
takenAt: string;
|
|
21
|
+
/** Project-relative POSIX path -> content hash. */
|
|
22
|
+
files: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @implements A-SPEC-134
|
|
26
|
+
* The tamper-evidence digest a baseline is anchored by: sha256 of the key-sorted files map. NOT the
|
|
27
|
+
* whole record (takenAt is a timestamp, not content) — only the file→hash mapping the scope narrows
|
|
28
|
+
* against. Editing any file entry changes this digest, and the anchor on the append-only chain will
|
|
29
|
+
* no longer match. Pure, key-order-insensitive.
|
|
30
|
+
*/
|
|
31
|
+
export declare function baselineDigest(files: Record<string, string>): string;
|
|
32
|
+
/** Write (or replace) a labelled baseline. One label holds exactly one reference point. */
|
|
33
|
+
export declare function writeBaseline(root: string, label: string, files: Record<string, string>): Baseline;
|
|
34
|
+
/**
|
|
35
|
+
* @implements A-SPEC-134
|
|
36
|
+
* Is this baseline still the one that was recorded? Recompute its digest from the on-disk files and
|
|
37
|
+
* confirm a `baseline-recorded` chain entry carries that (label, digest). A hand-edit changes the
|
|
38
|
+
* digest and no entry matches → false. A missing baseline or missing/unreadable chain → false:
|
|
39
|
+
* cannot-verify is not permission to narrow (the caller degrades to full scope).
|
|
40
|
+
*/
|
|
41
|
+
export declare function isBaselineTrusted(root: string, label: string, ledgerFile: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Read a baseline, or null when there is none.
|
|
44
|
+
*
|
|
45
|
+
* ABSENT AND EMPTY ARE DIFFERENT. Empty means "nothing existed when this was taken"; absent means
|
|
46
|
+
* "there is no reference point at all". Returning an empty baseline for a missing one would let a
|
|
47
|
+
* caller narrow its scope against a reference it never took — the silently-wrong direction. A corrupt
|
|
48
|
+
* or unreadable file is treated as absent for the same reason a cold cache is: it must degrade the
|
|
49
|
+
* run, never abort it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readBaseline(root: string, label: string): Baseline | null;
|
|
52
|
+
/**
|
|
53
|
+
* May this run become the reference point for "since the last verified state"?
|
|
54
|
+
*
|
|
55
|
+
* ONLY a run that actually executed and passed. A red suite must not become the baseline — every
|
|
56
|
+
* later scope would then be narrowed against a state nobody verified — and a skipped run is not
|
|
57
|
+
* evidence of anything at all. This is exactly the condition that already gates writing test
|
|
58
|
+
* evidence, extracted so both branches are pinned by a test rather than only the negative one: a
|
|
59
|
+
* fixture project has no runnable suite, so the passing branch is otherwise never exercised.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shouldRecordBaseline(result: {
|
|
62
|
+
passed: boolean;
|
|
63
|
+
skipped: boolean;
|
|
64
|
+
}): boolean;
|
|
65
|
+
/** Every label that exists, sorted. */
|
|
66
|
+
export declare function listBaselines(root: string): string[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What changed since a reference point" — the one question that genuinely needed version control,
|
|
3
|
+
* behind a port with two adapters so it does not need it any more.
|
|
4
|
+
*
|
|
5
|
+
* Git remains the better answer where it exists: it detects renames and it knows about commits. The
|
|
6
|
+
* snapshot adapter answers the same question from content hashes, which the scan already computes, so
|
|
7
|
+
* a project that has never run `git init` keeps change-impact, scoped regression and review.
|
|
8
|
+
*
|
|
9
|
+
* The contract that keeps this honest is `changes(): ChangeSet | null`. An unavailable source returns
|
|
10
|
+
* NULL, not empty sets: empty means "nothing changed", and a caller that read a missing reference
|
|
11
|
+
* point as "nothing changed" would narrow its scope against a baseline it never took — silently, and
|
|
12
|
+
* in the dangerous direction. Null forces a decision, and the decision the harness takes elsewhere
|
|
13
|
+
* (`computeTestScope` with no anchored tests) is to widen to full scope with a stated reason.
|
|
14
|
+
*/
|
|
15
|
+
export interface ChangeSet {
|
|
16
|
+
added: string[];
|
|
17
|
+
modified: string[];
|
|
18
|
+
deleted: string[];
|
|
19
|
+
renamed: {
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
export interface ChangeSourceInfo {
|
|
25
|
+
kind: 'git' | 'snapshot';
|
|
26
|
+
/** What the comparison is against: a git range, or a baseline label. */
|
|
27
|
+
baseline: string;
|
|
28
|
+
/** False for the snapshot adapter — a rename appears as delete + add. */
|
|
29
|
+
detectsRenames: boolean;
|
|
30
|
+
available: boolean;
|
|
31
|
+
/** Why it is unavailable. Present only when `available` is false. */
|
|
32
|
+
reason?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ChangeSource {
|
|
35
|
+
changes(): ChangeSet | null;
|
|
36
|
+
describe(): ChangeSourceInfo;
|
|
37
|
+
}
|
|
38
|
+
declare const EMPTY: ChangeSet;
|
|
39
|
+
export declare const isDerivedState: (relPath: string) => boolean;
|
|
40
|
+
/** Same digest the scan caches use, so a baseline and a scan cache never disagree about a file. */
|
|
41
|
+
export declare const hashOf: (content: string) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Content hashes for every file in the tree, keyed by project-relative POSIX path.
|
|
44
|
+
*
|
|
45
|
+
* Derived state under `.ax/` is excluded (see `isDerivedState`) along with the scanner's vendor/cache
|
|
46
|
+
* skips, so a run never reports its own bookkeeping as project churn. Authored specs under
|
|
47
|
+
* `.ax/specs/**` ARE included — they are project content.
|
|
48
|
+
*
|
|
49
|
+
* Files that cannot be read are skipped rather than failing the walk — an unreadable file is not
|
|
50
|
+
* evidence of a change.
|
|
51
|
+
*/
|
|
52
|
+
export declare function hashTree(root: string, opts?: {
|
|
53
|
+
isIgnored?: (relPath: string) => boolean;
|
|
54
|
+
}): Record<string, string>;
|
|
55
|
+
/** Compare a baseline's hashes against the tree's. Pure — the adapters supply the two maps. */
|
|
56
|
+
export declare function diffHashes(before: Record<string, string>, after: Record<string, string>): ChangeSet;
|
|
57
|
+
/** Changes since a labelled baseline. Available on any directory, with or without version control. */
|
|
58
|
+
export declare class SnapshotChangeSource implements ChangeSource {
|
|
59
|
+
private readonly root;
|
|
60
|
+
private readonly label;
|
|
61
|
+
private readonly opts;
|
|
62
|
+
constructor(root: string, label: string, opts?: {
|
|
63
|
+
isIgnored?: (relPath: string) => boolean;
|
|
64
|
+
verify?: string;
|
|
65
|
+
});
|
|
66
|
+
/** Trusted iff a baseline exists AND (no verify requested OR its digest is anchored on the chain). */
|
|
67
|
+
private trustedBaseline;
|
|
68
|
+
changes(): ChangeSet | null;
|
|
69
|
+
describe(): ChangeSourceInfo;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Changes across a git range.
|
|
73
|
+
*
|
|
74
|
+
* THE TOP-LEVEL GUARD LIVES HERE, and only here. `gitChangedFiles` yields paths relative to the git
|
|
75
|
+
* top-level while `scan(root, root)` tags them relative to `root`; when those disagree the keys
|
|
76
|
+
* silently diverge and impact sets come back wrong. That hazard belongs to this adapter — it was
|
|
77
|
+
* previously imposed on every tool, including ones that never look at a diff, which is what made the
|
|
78
|
+
* whole harness demand a repository.
|
|
79
|
+
*/
|
|
80
|
+
export declare class GitChangeSource implements ChangeSource {
|
|
81
|
+
private readonly root;
|
|
82
|
+
private readonly base;
|
|
83
|
+
private readonly head;
|
|
84
|
+
private readonly topLevel?;
|
|
85
|
+
constructor(root: string, base: string, head: string);
|
|
86
|
+
changes(): ChangeSet | null;
|
|
87
|
+
describe(): ChangeSourceInfo;
|
|
88
|
+
}
|
|
89
|
+
export { EMPTY as EMPTY_CHANGE_SET };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A DOCUMENTED SUBSET of `.gitignore`, for the path where git cannot be asked.
|
|
3
|
+
*
|
|
4
|
+
* Why `.gitignore` and not a Holmes-specific ignore file: a project that carries two ignore files can
|
|
5
|
+
* have them disagree, and then no one can say which one is true. One file, interpreted by whoever is
|
|
6
|
+
* available — git when there is a repository, this matcher when there is not.
|
|
7
|
+
*
|
|
8
|
+
* WHAT MAKES THIS SAFE IS THE FAILURE DIRECTION. A construct outside the subset is collected in
|
|
9
|
+
* `unsupportedPatterns()` and its files are treated as NOT ignored. Including a file and saying so
|
|
10
|
+
* costs a reviewer some noise; excluding it silently drops real source out of the graph, out of
|
|
11
|
+
* clustering, and out of every answer built on them — the same failure the scanner's vendor-skip set
|
|
12
|
+
* was written to avoid.
|
|
13
|
+
*
|
|
14
|
+
* Supported: comments and blanks, leading-`/` anchoring, trailing-`/` directory-only, `*`, `?`, `**`,
|
|
15
|
+
* character classes (`[abc]`, `[a-z]`, `[!abc]`), and `!` negation with the last matching rule
|
|
16
|
+
* winning. Not supported: backslash escapes and nested per-directory `.gitignore` files — both
|
|
17
|
+
* declared through `unsupportedPatterns()` and `limitations()`.
|
|
18
|
+
*
|
|
19
|
+
* Character classes were added because a scan of a real Python target reported exactly one
|
|
20
|
+
* unsupported pattern, `*.py[cod]` — the most common idiom in a Python .gitignore. Leaving the most
|
|
21
|
+
* common construct in the "reported but not honoured" bucket was not defensible.
|
|
22
|
+
*/
|
|
23
|
+
export interface IgnoreMatcher {
|
|
24
|
+
/** True when a project-relative POSIX path is excluded by the supported rules. */
|
|
25
|
+
isIgnored(relPath: string): boolean;
|
|
26
|
+
/** Patterns this matcher could not interpret. Their files are treated as NOT ignored. */
|
|
27
|
+
unsupportedPatterns(): string[];
|
|
28
|
+
/** Format-level limits of this implementation, stated so a caller never has to infer them. */
|
|
29
|
+
limitations(): string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function parseIgnore(content: string): IgnoreMatcher;
|
|
32
|
+
/** Load the project-root `.gitignore`. A missing file ignores nothing. */
|
|
33
|
+
export declare function loadIgnore(root: string): IgnoreMatcher;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare function cleanSubprocessEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
2
|
+
/**
|
|
3
|
+
* Where a project begins, for a harness that must not require version control.
|
|
4
|
+
*
|
|
5
|
+
* The anchor is `.ax/` — the directory holding the spec chain, the ledgers and the derived state,
|
|
6
|
+
* i.e. Holmes-Kit's own single source of truth. That is the correct anchor even where git exists:
|
|
7
|
+
* the path keys every graph node is built from are relative to the project, and the project is
|
|
8
|
+
* defined by its governance directory, not by whether someone happened to run `git init`.
|
|
9
|
+
*
|
|
10
|
+
* WHAT THIS REPLACES. `assertRepoTopLevel` demanded that `root` equal `git rev-parse --show-toplevel`
|
|
11
|
+
* and threw otherwise — applied uniformly, including to tools that never look at a diff. Measured, that
|
|
12
|
+
* made `cpg_scan`, `issue_localize`, `rtm_check(root)`, `test_run` and `review_scope` all throw on a
|
|
13
|
+
* tree with no `.git`, while `reverse_scan` (which asks git only for a candidacy refinement) carried on
|
|
14
|
+
* and reported `isGit: false`.
|
|
15
|
+
*
|
|
16
|
+
* The hazard that guard addressed is REAL but narrow: `gitChangedFiles` yields paths relative to the
|
|
17
|
+
* git top-level while `scan(root, root)` tags them relative to `root`, so a disagreement silently
|
|
18
|
+
* produces wrong impact sets. That is a property of the GIT CHANGE SOURCE. This resolver therefore
|
|
19
|
+
* REPORTS the pair and enforces nothing; the git adapter refuses on disagreement, and a tool that
|
|
20
|
+
* merely walks the tree proceeds.
|
|
21
|
+
*/
|
|
22
|
+
export interface ProjectRoot {
|
|
23
|
+
/** Absolute, symlink-resolved project root. */
|
|
24
|
+
root: string;
|
|
25
|
+
/** How the root was decided: an `.ax` marker, or the caller's own directory. */
|
|
26
|
+
marker: '.ax' | 'given';
|
|
27
|
+
/** Present only inside a git working tree. Reported for the caller to judge; never enforced here. */
|
|
28
|
+
git?: {
|
|
29
|
+
topLevel: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** The marker directory that makes a directory a Holmes-Kit project. */
|
|
33
|
+
export declare const MARKER = ".ax";
|
|
34
|
+
export declare function resolveProjectRoot(dir: string): ProjectRoot;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write `@implements` anchors back into source files — the step that actually puts a brownfield tree
|
|
3
|
+
* inside the traceability graph.
|
|
4
|
+
*
|
|
5
|
+
* PLAN AND APPLY ARE SEPARATE. An anchor is an authorship claim, so the edits must be inspectable
|
|
6
|
+
* before anything is written, and applying them is opt-in: `applyAnchors` is dry-run by DEFAULT.
|
|
7
|
+
* Anchoring to a non-approved A-SPEC is refused here as well as by the PreToolUse gate — a
|
|
8
|
+
* reverse-engineered draft is a description of code, and description does not confer approval.
|
|
9
|
+
*/
|
|
10
|
+
export interface AnchorMapping {
|
|
11
|
+
/** Repo-root-relative POSIX path of the file to anchor. */
|
|
12
|
+
file: string;
|
|
13
|
+
/** Target A-SPEC id. Must be approved. */
|
|
14
|
+
aspec: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AnchorEdit extends AnchorMapping {
|
|
17
|
+
/** The exact line to insert, including its trailing newline. */
|
|
18
|
+
insert: string;
|
|
19
|
+
/** 0-based line index the insert goes at (1 when the file opens with a shebang). */
|
|
20
|
+
atLine: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RefusedAnchor extends AnchorMapping {
|
|
23
|
+
reason: string;
|
|
24
|
+
}
|
|
25
|
+
export interface AnchorPlan {
|
|
26
|
+
edits: AnchorEdit[];
|
|
27
|
+
refused: RefusedAnchor[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute the anchor edits for a mapping, refusing anything that must not be written.
|
|
31
|
+
*
|
|
32
|
+
* `approvedAspecs` is the caller's list of APPROVED A-SPEC ids — passed in rather than read here so
|
|
33
|
+
* the function stays pure with respect to the spec store and can be tested without one.
|
|
34
|
+
*/
|
|
35
|
+
export declare function planAnchors(root: string, mapping: AnchorMapping[], approvedAspecs: string[]): AnchorPlan;
|
|
36
|
+
export interface ApplyResult {
|
|
37
|
+
applied: number;
|
|
38
|
+
dryRun: boolean;
|
|
39
|
+
/** Edits that WOULD be applied — only meaningful on a dry run. */
|
|
40
|
+
would: number;
|
|
41
|
+
/** Files that could not be written, with the reason. */
|
|
42
|
+
failed: RefusedAnchor[];
|
|
43
|
+
}
|
|
44
|
+
/** Apply an anchor plan. DRY-RUN BY DEFAULT: writing requires an explicit `dryRun: false`. */
|
|
45
|
+
export declare function applyAnchors(root: string, edits: AnchorEdit[], opts?: {
|
|
46
|
+
dryRun?: boolean;
|
|
47
|
+
}): ApplyResult;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
2
|
+
/** A directory with fewer files than this is folded into its nearest ancestor before classification. */
|
|
3
|
+
export declare const MIN_FILES = 3;
|
|
4
|
+
/** Above this a cohesive cluster is flagged `oversized` — reported, never split (H-SPEC-126). */
|
|
5
|
+
export declare const MAX_FILES = 25;
|
|
6
|
+
/**
|
|
7
|
+
* Cohesion threshold: ANY resolved internal call makes a directory cohesive.
|
|
8
|
+
*
|
|
9
|
+
* Derived, not chosen. Re-measured from the graph's own `calls` edges across a Python target and this
|
|
10
|
+
* repository, independent directories measure 0.00 and every cohesive one at least 0.17 — the classes
|
|
11
|
+
* separate at exactly zero. Zero is safe as a cutoff precisely because edge resolution is
|
|
12
|
+
* precision-first: an ambiguous callee resolves to nothing, so a single internal edge is evidence
|
|
13
|
+
* rather than a guess. Exported and named so a target with an unusual layout can raise it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const COHESION_MIN = 1;
|
|
16
|
+
export type Cohesion = 'cohesive' | 'independent' | 'unmeasured';
|
|
17
|
+
export interface Cluster {
|
|
18
|
+
/** Directory the cluster was derived from (POSIX, repo-root-relative). */
|
|
19
|
+
dir: string;
|
|
20
|
+
/** Member files, sorted. A `per-file` split cluster holds exactly one. */
|
|
21
|
+
files: string[];
|
|
22
|
+
/** Total symbols across the member files. */
|
|
23
|
+
symbols: number;
|
|
24
|
+
/** Resolved calls from one file in the directory to another, counted before any split. */
|
|
25
|
+
internalCalls: number;
|
|
26
|
+
/** internalCalls per file in the directory — the measured density behind the split decision. */
|
|
27
|
+
density: number;
|
|
28
|
+
cohesion: Cohesion;
|
|
29
|
+
split: 'whole' | 'per-file';
|
|
30
|
+
/** True when the cluster is rooted in a test directory: reported separately, never an A-SPEC candidate. */
|
|
31
|
+
isTest: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Existing test files this slice's code is covered by, attached by name evidence (see test-map).
|
|
34
|
+
* Populated by `reverseScan`; empty from `clusterFiles`, which sees only production files.
|
|
35
|
+
*/
|
|
36
|
+
testFiles: string[];
|
|
37
|
+
/** A cohesive cluster larger than MAX_FILES: kept whole, but its blast radius is coarse. */
|
|
38
|
+
oversized: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ClusterOptions {
|
|
41
|
+
minFiles?: number;
|
|
42
|
+
maxFiles?: number;
|
|
43
|
+
cohesionMin?: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function clusterFiles(files: ScannedFile[], opts?: ClusterOptions): Cluster[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
import { Cluster } from './cluster';
|
|
3
|
+
import { AnchorMapping } from './anchor';
|
|
4
|
+
import { SliceEvidence } from './surface';
|
|
5
|
+
/**
|
|
6
|
+
* Exported so the approval-blocker judge can recognise an untouched draft by the marker this file
|
|
7
|
+
* plants, rather than re-typing the literal. A copy would drift silently the first time this string
|
|
8
|
+
* changes — and a silent divergence is the very class of defect REQ-182 exists to close.
|
|
9
|
+
*/
|
|
10
|
+
export declare const INFERRED = "<!-- inferred: not recoverable from code \u2014 a human writes this -->";
|
|
11
|
+
export interface DraftRefusal {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DraftResult {
|
|
16
|
+
ok: true;
|
|
17
|
+
/** The emitted documents, in chain order: H-SPEC, A-SPEC, T-SPEC. */
|
|
18
|
+
specs: Spec[];
|
|
19
|
+
/** The anchor mapping these drafts imply — plan only; nothing is written by drafting. */
|
|
20
|
+
anchorPlan: AnchorMapping[];
|
|
21
|
+
/** Identity of the cluster these drafts came from, recorded in frontmatter so a re-run recognises them. */
|
|
22
|
+
clusterKey: string;
|
|
23
|
+
}
|
|
24
|
+
/** The identity of a cluster: its directory, or the file itself when the directory split per file. */
|
|
25
|
+
export declare function clusterKeyOf(cluster: Cluster): string;
|
|
26
|
+
/**
|
|
27
|
+
* The first id number above every number already in use, across all spec kinds.
|
|
28
|
+
*
|
|
29
|
+
* Kind-agnostic on purpose: the three drafted documents share one number (H-SPEC-N / A-SPEC-N /
|
|
30
|
+
* T-SPEC-N), so the base must be free for every kind at once. Deterministic given the same spec set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function nextIdBase(existingIds: string[]): number;
|
|
33
|
+
export declare function draftSpecs(cluster: Cluster, parentReqId: string | undefined, idBase: string | number, evidence?: SliceEvidence): DraftResult | DraftRefusal;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect the wiring forms a syntactic scan cannot follow — DI decorators, dynamic `require`/`import`,
|
|
3
|
+
* string-key dispatch, `eval`-like execution, config-driven wiring.
|
|
4
|
+
*
|
|
5
|
+
* REPORTED, NEVER RESOLVED. An indicator says "there is a relationship here that static evidence
|
|
6
|
+
* cannot recover"; it never becomes an edge or a cluster membership. The correct outcome for such a
|
|
7
|
+
* file is that it stays visibly unresolved, not that it gets attached to a guessed slice.
|
|
8
|
+
*
|
|
9
|
+
* The pattern sets are LANGUAGE-SCOPED and only exist for languages that were calibrated. Applying
|
|
10
|
+
* one language's regexes to another put nearly half of one subject's files under two indicators
|
|
11
|
+
* during calibration — and an indicator firing on most of a codebase teaches the reader to ignore it,
|
|
12
|
+
* which is worse than not shipping it. Languages with no calibrated set report nothing, and
|
|
13
|
+
* `dynamicWiringSupported` makes that gap visible instead of letting a silent zero read as "clean".
|
|
14
|
+
*/
|
|
15
|
+
export type Indicator = 'dynamic-import' | 'di-decorator' | 'string-key-dispatch' | 'eval-like' | 'config-driven';
|
|
16
|
+
/** True when this file's language has a calibrated indicator set. */
|
|
17
|
+
export declare function dynamicWiringSupported(file: string): boolean;
|
|
18
|
+
export declare function detectDynamicWiring(code: string, file: string): Indicator[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
2
|
+
import { Cluster } from './cluster';
|
|
3
|
+
import { Indicator } from './dynamic-wiring';
|
|
4
|
+
import { TestMapping } from './test-map';
|
|
5
|
+
import { SurfaceSymbol } from './surface';
|
|
6
|
+
/**
|
|
7
|
+
* Read-only inventory of a brownfield tree: what is there, how it partitions into A-SPEC-sized
|
|
8
|
+
* candidates, and — the part that decides whether any of this is trustworthy — what the scan could
|
|
9
|
+
* NOT resolve.
|
|
10
|
+
*
|
|
11
|
+
* WRITES NOTHING, and that is a design constraint rather than an incidental property. In particular
|
|
12
|
+
* the default scan does not persist an N7 cache into the target: a cache file is a write, and the one
|
|
13
|
+
* claim this tool makes is that inventorying a codebase leaves it byte-identical. A caller that wants
|
|
14
|
+
* incremental warmth injects its own cached scanner via `opts.scan`, choosing where that cache lives.
|
|
15
|
+
*/
|
|
16
|
+
export interface Candidacy {
|
|
17
|
+
/** Files that are reverse-engineering candidates. */
|
|
18
|
+
files: ScannedFile[];
|
|
19
|
+
/**
|
|
20
|
+
* Test files, which the scanner deliberately excludes from the production graph. Collected here
|
|
21
|
+
* because per-A-SPEC test evidence is read from anchors INSIDE them, so the reverse flow has to be
|
|
22
|
+
* able to propose those anchors — otherwise adoption starts with every test outside the graph.
|
|
23
|
+
*/
|
|
24
|
+
testFiles: string[];
|
|
25
|
+
/** How many files the scanner ingested before the tracked-file filter. */
|
|
26
|
+
scanned: number;
|
|
27
|
+
/** How many scanned files were dropped as untracked/ignored. */
|
|
28
|
+
droppedUntracked: number;
|
|
29
|
+
/** False when the target is not a git repository — the filter did not apply and the report says so. */
|
|
30
|
+
isGit: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `.gitignore` patterns the non-git matcher could not interpret. Their files were KEPT: including a
|
|
33
|
+
* file and saying so is safe, dropping it silently is not.
|
|
34
|
+
*/
|
|
35
|
+
ignoreUnsupportedPatterns: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Only git-tracked files are candidates.
|
|
39
|
+
*
|
|
40
|
+
* Ignored code is by definition not part of the shipped system, and treating it as governable
|
|
41
|
+
* produces specs for scratch work, build output and local experiments. A non-git tree accepts
|
|
42
|
+
* everything and reports `isGit: false`, which is honest rather than silently wrong.
|
|
43
|
+
*/
|
|
44
|
+
export declare function candidates(root: string, scan: (root: string) => ScannedFile[]): Candidacy;
|
|
45
|
+
export interface FlaggedFile {
|
|
46
|
+
file: string;
|
|
47
|
+
indicators: Indicator[];
|
|
48
|
+
}
|
|
49
|
+
export interface ReverseScanReport {
|
|
50
|
+
root: string;
|
|
51
|
+
isGit: boolean;
|
|
52
|
+
/** Files ingested by the scanner, before the tracked-file filter. */
|
|
53
|
+
scanned: number;
|
|
54
|
+
droppedUntracked: number;
|
|
55
|
+
/** Files that are reverse-engineering candidates. */
|
|
56
|
+
candidates: number;
|
|
57
|
+
anchored: number;
|
|
58
|
+
unanchored: number;
|
|
59
|
+
/** A-SPEC candidates. */
|
|
60
|
+
clusters: Cluster[];
|
|
61
|
+
/** Clusters rooted in a test directory — a T-SPEC's business, never an A-SPEC candidate. */
|
|
62
|
+
testClusters: Cluster[];
|
|
63
|
+
/**
|
|
64
|
+
* Cluster key → public-surface candidates, for `reverse_draft` to state as evidence.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately NOT part of the `reverse_scan` tool response: that response enters context on every
|
|
67
|
+
* call, and the surface measures ~5KB per cluster on the calibration target. The handler strips it.
|
|
68
|
+
*/
|
|
69
|
+
surfaceByCluster: Record<string, SurfaceSymbol[]>;
|
|
70
|
+
coverage: {
|
|
71
|
+
/** Candidate files placed in a production cluster by directory evidence. */
|
|
72
|
+
structurallyResolved: number;
|
|
73
|
+
/** Candidate files that landed in a test cluster instead. */
|
|
74
|
+
inTestClusters: number;
|
|
75
|
+
/**
|
|
76
|
+
* Files in clusters whose language has no call-edge extraction. Their boundary was NOT decided by
|
|
77
|
+
* measured cohesion, so a human must confirm it — see Cluster.cohesion === 'unmeasured'.
|
|
78
|
+
*/
|
|
79
|
+
cohesionUnmeasured: number;
|
|
80
|
+
/** Existing test files attached to a slice by name evidence — the per-A-SPEC evidence link. */
|
|
81
|
+
testsMatched: number;
|
|
82
|
+
/** Test files that could not be attached: an explicit gap, never a guessed attachment. */
|
|
83
|
+
testsUnmatched: number;
|
|
84
|
+
/** Files carrying at least one dynamic-wiring indicator: relationships static evidence cannot follow. */
|
|
85
|
+
withDynamicIndicators: number;
|
|
86
|
+
indicators: Partial<Record<Indicator, number>>;
|
|
87
|
+
/** Extensions present in the tree for which no indicator set is calibrated — a zero here means "not looked for". */
|
|
88
|
+
uncalibratedIndicatorLanguages: string[];
|
|
89
|
+
/** Ignore patterns the non-git path could not interpret — their files were included. */
|
|
90
|
+
ignoreUnsupportedPatterns: string[];
|
|
91
|
+
};
|
|
92
|
+
/** The flagged files themselves, sorted, so a reviewer can check the indicators rather than trust the count. */
|
|
93
|
+
needsHumanConfirmation: FlaggedFile[];
|
|
94
|
+
/** Existing tests attached to slices, and the ones that could not be — see test-map. */
|
|
95
|
+
tests: TestMapping;
|
|
96
|
+
}
|
|
97
|
+
export interface ReverseScanOptions {
|
|
98
|
+
/** Scan strategy. Defaults to a NON-PERSISTING scan so the target tree is never written to. */
|
|
99
|
+
scan?: (root: string) => ScannedFile[];
|
|
100
|
+
/** Cap on the flagged-file list carried in the report. The counts above it stay complete. */
|
|
101
|
+
maxFlagged?: number;
|
|
102
|
+
}
|
|
103
|
+
export declare function reverseScan(root: string, opts?: ReverseScanOptions): ReverseScanReport;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
2
|
+
import { CodeSymbol } from '../cpg/language-parser';
|
|
3
|
+
/**
|
|
4
|
+
* The evidence a reverse-engineered draft may state as fact, and the rendering of it.
|
|
5
|
+
*
|
|
6
|
+
* The word throughout is SURFACE, never CONTRACT. A scanner can testify that a symbol is defined at
|
|
7
|
+
* the top level of a file; it cannot testify that the symbol is something this slice promises to
|
|
8
|
+
* anyone. Naming the output `contract` would let the distinction rot away in a later edit, and the
|
|
9
|
+
* whole point of A-SPEC-181 is that filling a section with evidence is not the same as deciding it.
|
|
10
|
+
*
|
|
11
|
+
* Pure: this module reads no files and knows nothing about the spec store, so every rule below is
|
|
12
|
+
* exercisable from a literal.
|
|
13
|
+
*/
|
|
14
|
+
export interface SurfaceSymbol {
|
|
15
|
+
/** repo-root-relative POSIX path, exactly as `ScannedFile.sourcePath` carries it. */
|
|
16
|
+
file: string;
|
|
17
|
+
name: string;
|
|
18
|
+
kind: CodeSymbol['kind'];
|
|
19
|
+
}
|
|
20
|
+
/** Everything one slice's draft is allowed to state as fact. */
|
|
21
|
+
export interface SliceEvidence {
|
|
22
|
+
surface: SurfaceSymbol[];
|
|
23
|
+
/** Existing test files attached to this slice by name evidence (= `Cluster.testFiles`). */
|
|
24
|
+
testFiles: string[];
|
|
25
|
+
/** Test files in the whole repository that could not be attached to ANY slice. */
|
|
26
|
+
testsUnmatched: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Total characters the rendered symbol list may occupy.
|
|
30
|
+
*
|
|
31
|
+
* Chosen from measurement, not taste. On the calibration target the 15 clusters hold 164, 132, 68,
|
|
32
|
+
* 53, 11 and ten single-symbol surfaces; listing EVERY one whole costs 11,460 characters (~2.9K
|
|
33
|
+
* tokens) for the entire adoption of a 203-file repository. So the bound exists to stop a
|
|
34
|
+
* pathological monorepo cluster, NOT to trim ordinary ones: at 4,000 every cluster on the target
|
|
35
|
+
* fits whole except the largest, which loses ~15% and says so.
|
|
36
|
+
*
|
|
37
|
+
* A tighter bound was tried first (1,200) and dropped 119 of 164 names — 73% of the largest slice —
|
|
38
|
+
* to save ~2K tokens across a whole adoption. That trade sends the human back to the scan report,
|
|
39
|
+
* which is the toil this REQ exists to remove.
|
|
40
|
+
*/
|
|
41
|
+
export declare const SURFACE_BUDGET = 4000;
|
|
42
|
+
/**
|
|
43
|
+
* Longest name that may be quoted. A longer one is dropped rather than truncated: a partially
|
|
44
|
+
* printed identifier cannot be searched for, so it is worse than an honest omission count.
|
|
45
|
+
*/
|
|
46
|
+
export declare const NAME_MAX = 80;
|
|
47
|
+
/**
|
|
48
|
+
* The top-level, non-underscore symbols of the given files — the PUBLIC SURFACE CANDIDATES.
|
|
49
|
+
*
|
|
50
|
+
* Two mechanical filters, no natural-language judgement:
|
|
51
|
+
*
|
|
52
|
+
* 1. NESTED, decided by line range WITHIN THE SAME FILE: a symbol `s` is nested when some other
|
|
53
|
+
* symbol `o` of that same file satisfies `o.startLine < s.startLine && o.endLine >= s.endLine`.
|
|
54
|
+
* `>=` (not `>`) because a nested definition may end on the same line as its enclosing one.
|
|
55
|
+
* `<` (not `<=`) on the start because two symbols reported at the same start line cannot be
|
|
56
|
+
* ordered by containment — refusing to decide keeps both rather than deleting a real symbol.
|
|
57
|
+
* The comparison is per-file: a wide range in one file must not swallow a narrow one in another.
|
|
58
|
+
* 2. PRIVATE, decided by a leading underscore. `build_briefing` keeps its inner underscores; only
|
|
59
|
+
* the first character is consulted.
|
|
60
|
+
*
|
|
61
|
+
* Sorted by file then start line so the same scan always renders byte-identically.
|
|
62
|
+
*/
|
|
63
|
+
export declare function surfaceOf(files: ScannedFile[]): SurfaceSymbol[];
|
|
64
|
+
/**
|
|
65
|
+
* The surface as a per-file list, bounded.
|
|
66
|
+
*
|
|
67
|
+
* Four rules, the same ones A-SPEC-183 arrived at for the gate's denial text:
|
|
68
|
+
* (1) a name is printed WHOLE or not at all; (2) if nothing fits, report a COUNT instead of a list;
|
|
69
|
+
* (3) the bound is on the TOTAL, not per item; (4) no item passes unconditionally — the first entry
|
|
70
|
+
* is budget-checked like every other, so one absurd path cannot blow the bound.
|
|
71
|
+
*/
|
|
72
|
+
export declare function renderSurface(surface: SurfaceSymbol[]): string;
|
|
73
|
+
/**
|
|
74
|
+
* The tests already attached to this slice, and — always — how many the repository could not attach
|
|
75
|
+
* to any slice at all.
|
|
76
|
+
*
|
|
77
|
+
* The unmatched count is not decoration. Measured on the calibration target the matcher attaches 46
|
|
78
|
+
* of 86 test files (53%), and the 40 it refuses are mostly its own rule's limit, not unrelated
|
|
79
|
+
* tests. A list printed without that number reads as "these are the tests for this slice".
|
|
80
|
+
*/
|
|
81
|
+
export declare function renderTestPoints(ev: SliceEvidence): string;
|
|
82
|
+
/**
|
|
83
|
+
* A completion criterion that is checkable as written, which is why this section — alone among the
|
|
84
|
+
* three A-SPEC-181 fills — does not keep a TODO and therefore does not block approval.
|
|
85
|
+
*
|
|
86
|
+
* Clause 3 is NEVER dropped when the slice has no attached test. Dropping it would leave 1 and 2
|
|
87
|
+
* standing alone and read as "done"; the absence of a regression control is this slice's largest
|
|
88
|
+
* risk and belongs in the completion criterion, not in silence. Measured on the calibration target:
|
|
89
|
+
* 11 of 15 clusters are in exactly that state.
|
|
90
|
+
*/
|
|
91
|
+
export declare function renderDoneWhen(files: string[], testFiles: string[]): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Cluster } from './cluster';
|
|
2
|
+
/**
|
|
3
|
+
* Attach a brownfield project's EXISTING tests to the slices they cover.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: per-A-SPEC test evidence (`scanTestAnchors` → `computeTestScope` →
|
|
6
|
+
* `writeTestEvidence`) is read from `@implements` anchors inside TEST files, but the scanner
|
|
7
|
+
* deliberately keeps test files out of the production graph — so reverse engineering clustered the
|
|
8
|
+
* production code and left every test unanchored, and the evidence link had to be wired by hand.
|
|
9
|
+
* Measured on a real pytest target, that was 76 files of manual work standing between adoption and
|
|
10
|
+
* any per-slice test evidence at all.
|
|
11
|
+
*
|
|
12
|
+
* The mapping is by NAME EVIDENCE ONLY, and unmatched is a first-class outcome. A test whose subject
|
|
13
|
+
* cannot be identified, or whose subject lives in two different slices, is REPORTED rather than
|
|
14
|
+
* attached: an anchor is an authorship claim, and attaching one on directory proximity would be the
|
|
15
|
+
* guessing this whole slice refuses to do everywhere else.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Candidate subjects a test file's name could be naming, STRONGEST FIRST.
|
|
19
|
+
*
|
|
20
|
+
* The first element is the whole name with its test convention stripped. The rest drop leading
|
|
21
|
+
* `_`-separated segments one at a time, because a test name routinely carries a suite qualifier
|
|
22
|
+
* ahead of its subject — measured on a real Python target, 62 of 74 tests were
|
|
23
|
+
* `tests/golden/test_golden_<subject>.py`, where `golden_` names the suite. A fixed vocabulary of
|
|
24
|
+
* qualifiers ("golden", "unit", "integration", …) would be a guess about someone else's conventions;
|
|
25
|
+
* offering weaker candidates and demanding a UNIQUE production match lets the target's own file names
|
|
26
|
+
* decide which reading is right. An empty list means the name identifies no subject at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare function subjectStems(testPath: string, qualifiers: ReadonlySet<string>): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Leading name segments that the corpus itself shows to be SUITE QUALIFIERS rather than subjects.
|
|
31
|
+
*
|
|
32
|
+
* A qualifier is a word many of a project's tests start with (`golden_`, `unit_`, `integration_`),
|
|
33
|
+
* which is a property of the corpus and therefore measurable — as opposed to a vocabulary guessed in
|
|
34
|
+
* advance, which would encode one project's conventions as everyone's. The thresholds keep a
|
|
35
|
+
* coincidence from qualifying: at least three files AND at least a tenth of the suite.
|
|
36
|
+
*/
|
|
37
|
+
export declare function qualifierSegments(testFiles: string[]): Set<string>;
|
|
38
|
+
/**
|
|
39
|
+
* @implements A-SPEC-185
|
|
40
|
+
* How strong the name evidence was, strongest first.
|
|
41
|
+
*
|
|
42
|
+
* `exact` — the subject IS the production file's name.
|
|
43
|
+
* `prefix` — the production name STARTS with the whole subject, at a segment boundary. Nothing the
|
|
44
|
+
* test said about its subject was discarded.
|
|
45
|
+
* `shortened` — one trailing segment of the subject was dropped to reach the production name. Part
|
|
46
|
+
* of what the test said WAS discarded, which is why this is the weakest reading.
|
|
47
|
+
*
|
|
48
|
+
* Recorded on every match because the readings are not equally trustworthy: a reviewer should see
|
|
49
|
+
* which one carried an anchor, and a false anchor found later should be traceable to the rule that
|
|
50
|
+
* produced it.
|
|
51
|
+
*/
|
|
52
|
+
export type MatchStrength = 'exact' | 'prefix' | 'shortened';
|
|
53
|
+
export interface TestMatch {
|
|
54
|
+
/** The test file, repo-root-relative. */
|
|
55
|
+
file: string;
|
|
56
|
+
/** Key of the cluster it covers. */
|
|
57
|
+
clusterKey: string;
|
|
58
|
+
/** The production file whose name it matched — the evidence for the mapping. */
|
|
59
|
+
subject: string;
|
|
60
|
+
/** Which reading of the name produced this match. */
|
|
61
|
+
via: MatchStrength;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* How many trailing segments the `shortened` reading may drop.
|
|
65
|
+
*
|
|
66
|
+
* ONE, and the value is measured rather than chosen. At two, the calibration target gains four more
|
|
67
|
+
* matches and two of them are the false anchor this file already warns about above:
|
|
68
|
+
* `test_golden_tool_dispatch_e2e.py` lands on `src/core/tool.py`. The more segments a reading
|
|
69
|
+
* discards, the more generic the remainder becomes — and a generic word is the kind that happens to
|
|
70
|
+
* be unique by accident.
|
|
71
|
+
*/
|
|
72
|
+
export declare const MAX_TRAILING_DROP = 1;
|
|
73
|
+
/**
|
|
74
|
+
* Segment separators recognised WHEN WEAKENING a subject — `_` for python-style names, `-` for
|
|
75
|
+
* JS/TS-style ones.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately not shared with `subjectStems`/`qualifierSegments`: widening the split there would
|
|
78
|
+
* change which leading segments count as suite qualifiers, and that could move matches that already
|
|
79
|
+
* exist. The weakening steps only ever run where the exact reading found nothing, so they can be
|
|
80
|
+
* broader without disturbing anything.
|
|
81
|
+
*/
|
|
82
|
+
export declare const SEGMENT_SEPARATOR: RegExp;
|
|
83
|
+
export interface UnmatchedTest {
|
|
84
|
+
file: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
}
|
|
87
|
+
export interface TestMapping {
|
|
88
|
+
matched: TestMatch[];
|
|
89
|
+
unmatched: UnmatchedTest[];
|
|
90
|
+
}
|
|
91
|
+
export declare function mapTestsToClusters(testFiles: string[], clusters: Cluster[]): TestMapping;
|