@holmes-lab/holmes-kit 0.2.1 → 0.3.1
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 +55 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
- package/dist/holmes/mcp/elicit-approval.js +30 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +636 -35
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/server.js +13 -10
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { MaintenanceAnalysis } from './maintenance-analyze';
|
|
2
|
+
/**
|
|
3
|
+
* Persisted maintenance evidence — the durable half of the graph-first protocol.
|
|
4
|
+
*
|
|
5
|
+
* An analysis on its own is a prediction that evaporates when the call returns. Calibration is only
|
|
6
|
+
* possible if the prediction is kept AND the eventual outcome is written next to it, so this module
|
|
7
|
+
* owns both the artifact format and the arithmetic that scores it. Every number it reports is
|
|
8
|
+
* derived from recorded outcomes; where nothing was recorded it reports `insufficient-data` rather
|
|
9
|
+
* than a confident-looking zero.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EVIDENCE_ARTIFACT_SCHEMA = "maintenance-evidence/1.4";
|
|
12
|
+
/** Default directory, relative to the project root. */
|
|
13
|
+
export declare const EVIDENCE_DIR: string;
|
|
14
|
+
export interface EvidenceOutcome {
|
|
15
|
+
recordedAt: string;
|
|
16
|
+
actualFiles: string[];
|
|
17
|
+
actualSymbols: string[];
|
|
18
|
+
actualTests: string[];
|
|
19
|
+
/** Absent when nobody has judged the classification: unscored, NOT scored as wrong. */
|
|
20
|
+
actualClassification?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface EvidenceArtifact {
|
|
23
|
+
artifactSchema: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
recordedAt: string;
|
|
26
|
+
analysisSchema: string;
|
|
27
|
+
extractorVersion: string;
|
|
28
|
+
classification: string;
|
|
29
|
+
confidence: number;
|
|
30
|
+
graphBasis: MaintenanceAnalysis['graphBasis'];
|
|
31
|
+
predicted: {
|
|
32
|
+
files: string[];
|
|
33
|
+
primaryFiles: string[];
|
|
34
|
+
symbols: string[];
|
|
35
|
+
tests: string[];
|
|
36
|
+
/**
|
|
37
|
+
* @implements A-SPEC-277
|
|
38
|
+
* sha256 of each predicted file AS ANALYSED. HEAD alone cannot see an uncommitted edit to the
|
|
39
|
+
* very file under analysis, which would leave the evidence looking current while describing
|
|
40
|
+
* content that no longer exists. Computed by the caller (reading files is I/O; this module is
|
|
41
|
+
* pure), keyed by the same repo-relative path as `files`.
|
|
42
|
+
*/
|
|
43
|
+
fileDigests: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* @implements A-SPEC-276
|
|
47
|
+
* What the analysis did NOT establish. A pre-edit gate that reads only the prediction would treat
|
|
48
|
+
* "we saw no impact" and "we could not look" as the same sentence; they are opposite claims. The
|
|
49
|
+
* artifact is the durable evidence, so the uncertainty travels with it.
|
|
50
|
+
*/
|
|
51
|
+
uncertainty: {
|
|
52
|
+
unknowns: string[];
|
|
53
|
+
coverage: {
|
|
54
|
+
scannedFiles: number;
|
|
55
|
+
skippedFiles: string[];
|
|
56
|
+
unsupportedLanguages: string[];
|
|
57
|
+
};
|
|
58
|
+
/** The caller walk stopped at a bound: the transitive set is a lower bound, not a complete one. */
|
|
59
|
+
impactTruncated: boolean;
|
|
60
|
+
testScopeTier: string;
|
|
61
|
+
/** Files reachable only through a call edge — affected, but never analysed as edit candidates. */
|
|
62
|
+
impactedFiles: string[];
|
|
63
|
+
/**
|
|
64
|
+
* @implements A-SPEC-279
|
|
65
|
+
* Whether each constraint axis was EXAMINED. False means the input was missing, never that the
|
|
66
|
+
* axis came back clean — the distinction the gate acts on.
|
|
67
|
+
*/
|
|
68
|
+
securityAssessed: boolean;
|
|
69
|
+
compatibilityAssessed: boolean;
|
|
70
|
+
};
|
|
71
|
+
outcome?: EvidenceOutcome;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A digest reaches the filesystem as a filename, and it arrives from an MCP caller. Refuse anything
|
|
75
|
+
* that is not a bare lowercase sha256 hex BEFORE joining it onto a path — an accepted `../` would
|
|
76
|
+
* turn "record an outcome" into an arbitrary-path write.
|
|
77
|
+
*/
|
|
78
|
+
export declare function digestFilename(digest: string): string;
|
|
79
|
+
/** Project an analysis into the durable prediction record. Pure: the caller supplies the clock. */
|
|
80
|
+
export declare function artifactFrom(analysis: MaintenanceAnalysis, recordedAt: string, fileDigests?: Record<string, string>): EvidenceArtifact;
|
|
81
|
+
/**
|
|
82
|
+
* Persist an artifact, addressed by its digest. Re-running the same analysis is a re-run, not a
|
|
83
|
+
* reason to forget what actually happened, so an outcome already recorded at this digest survives.
|
|
84
|
+
*/
|
|
85
|
+
export declare function writeArtifact(dir: string, artifact: EvidenceArtifact): string;
|
|
86
|
+
export interface ArtifactRead {
|
|
87
|
+
artifacts: EvidenceArtifact[];
|
|
88
|
+
/** Filenames that could not be parsed — surfaced, never silently dropped from the denominator. */
|
|
89
|
+
unreadable: string[];
|
|
90
|
+
/** Artifacts written by a different artifact schema, excluded from any arithmetic. */
|
|
91
|
+
skippedSchema: number;
|
|
92
|
+
}
|
|
93
|
+
export declare function readArtifacts(dir: string): ArtifactRead;
|
|
94
|
+
export interface OutcomeInput {
|
|
95
|
+
actualFiles?: string[];
|
|
96
|
+
actualSymbols?: string[];
|
|
97
|
+
actualTests?: string[];
|
|
98
|
+
actualClassification?: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Attach what actually happened to a prediction that was actually made. An unknown digest is
|
|
102
|
+
* refused: inventing the prediction alongside the outcome would let the record score itself.
|
|
103
|
+
*/
|
|
104
|
+
export declare function recordOutcome(dir: string, digest: string, outcome: OutcomeInput, recordedAt: string): EvidenceArtifact;
|
|
105
|
+
export interface CalibrationBin {
|
|
106
|
+
lower: number;
|
|
107
|
+
upper: number;
|
|
108
|
+
n: number;
|
|
109
|
+
meanConfidence: number | null;
|
|
110
|
+
observedAccuracy: number | null;
|
|
111
|
+
status: 'measured' | 'insufficient-data';
|
|
112
|
+
}
|
|
113
|
+
export interface Calibration {
|
|
114
|
+
schema: string;
|
|
115
|
+
/** Predictions that carry a judged classification — the only ones that can be scored. */
|
|
116
|
+
n: number;
|
|
117
|
+
/**
|
|
118
|
+
* @implements A-SPEC-434
|
|
119
|
+
* Artifacts with no JUDGED CLASSIFICATION — which is not the same as no outcome, and used to be
|
|
120
|
+
* called `withoutOutcome`. Recording `actualFiles` for a correct prediction reported
|
|
121
|
+
* `n: 0, withoutOutcome: 1, falsePositives: 0, falseNegatives: 0`: the outcome was there, the file
|
|
122
|
+
* scores were computed from it, and the name said otherwise. Only a classification can be scored
|
|
123
|
+
* for confidence, so this counts what cannot enter the bins — nothing more.
|
|
124
|
+
*/
|
|
125
|
+
withoutClassification: number;
|
|
126
|
+
bins: CalibrationBin[];
|
|
127
|
+
brierScore: number | null;
|
|
128
|
+
falsePositives: number;
|
|
129
|
+
falseNegatives: number;
|
|
130
|
+
minSamples: number;
|
|
131
|
+
status: 'measured' | 'insufficient-data';
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Reliability bins plus a Brier score. `minSamples` is the honesty knob: below it a bin reports
|
|
135
|
+
* `insufficient-data` and a null accuracy, because one sample in a bin yields a 0 or a 1 that looks
|
|
136
|
+
* like a measurement and is really just noise.
|
|
137
|
+
*/
|
|
138
|
+
export declare function computeCalibration(artifacts: readonly EvidenceArtifact[], opts?: {
|
|
139
|
+
minSamples?: number;
|
|
140
|
+
}): Calibration;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EVIDENCE_DIR = exports.EVIDENCE_ARTIFACT_SCHEMA = void 0;
|
|
37
|
+
exports.digestFilename = digestFilename;
|
|
38
|
+
exports.artifactFrom = artifactFrom;
|
|
39
|
+
exports.writeArtifact = writeArtifact;
|
|
40
|
+
exports.readArtifacts = readArtifacts;
|
|
41
|
+
exports.recordOutcome = recordOutcome;
|
|
42
|
+
exports.computeCalibration = computeCalibration;
|
|
43
|
+
// @implements A-SPEC-271
|
|
44
|
+
// @implements A-SPEC-268
|
|
45
|
+
const fs = __importStar(require("node:fs"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
/**
|
|
48
|
+
* Persisted maintenance evidence — the durable half of the graph-first protocol.
|
|
49
|
+
*
|
|
50
|
+
* An analysis on its own is a prediction that evaporates when the call returns. Calibration is only
|
|
51
|
+
* possible if the prediction is kept AND the eventual outcome is written next to it, so this module
|
|
52
|
+
* owns both the artifact format and the arithmetic that scores it. Every number it reports is
|
|
53
|
+
* derived from recorded outcomes; where nothing was recorded it reports `insufficient-data` rather
|
|
54
|
+
* than a confident-looking zero.
|
|
55
|
+
*/
|
|
56
|
+
// @implements A-SPEC-434 — the calibration output renamed a field, so a consumer can detect it.
|
|
57
|
+
exports.EVIDENCE_ARTIFACT_SCHEMA = 'maintenance-evidence/1.4';
|
|
58
|
+
/** Default directory, relative to the project root. */
|
|
59
|
+
exports.EVIDENCE_DIR = path.join('.ax', 'evidence', 'maintenance');
|
|
60
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/;
|
|
61
|
+
/**
|
|
62
|
+
* A digest reaches the filesystem as a filename, and it arrives from an MCP caller. Refuse anything
|
|
63
|
+
* that is not a bare lowercase sha256 hex BEFORE joining it onto a path — an accepted `../` would
|
|
64
|
+
* turn "record an outcome" into an arbitrary-path write.
|
|
65
|
+
*/
|
|
66
|
+
function digestFilename(digest) {
|
|
67
|
+
const bare = digest.startsWith('sha256:') ? digest.slice(7) : digest;
|
|
68
|
+
if (!DIGEST_PATTERN.test(bare)) {
|
|
69
|
+
throw new Error(`Refusing an evidence digest that is not a bare sha256 hex: ${JSON.stringify(digest)}`);
|
|
70
|
+
}
|
|
71
|
+
return `${bare}.json`;
|
|
72
|
+
}
|
|
73
|
+
function sortedUnique(values) {
|
|
74
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
75
|
+
}
|
|
76
|
+
/** Project an analysis into the durable prediction record. Pure: the caller supplies the clock. */
|
|
77
|
+
function artifactFrom(analysis, recordedAt, fileDigests = {}) {
|
|
78
|
+
return {
|
|
79
|
+
artifactSchema: exports.EVIDENCE_ARTIFACT_SCHEMA,
|
|
80
|
+
digest: analysis.evidenceDigest,
|
|
81
|
+
recordedAt,
|
|
82
|
+
analysisSchema: analysis.schemaVersion,
|
|
83
|
+
extractorVersion: analysis.extractorVersion,
|
|
84
|
+
classification: analysis.classification,
|
|
85
|
+
confidence: analysis.confidence,
|
|
86
|
+
graphBasis: analysis.graphBasis,
|
|
87
|
+
predicted: {
|
|
88
|
+
files: sortedUnique(analysis.impacts.files),
|
|
89
|
+
// @implements A-SPEC-271 — kept so a later slice can score per-tier precision over history.
|
|
90
|
+
primaryFiles: [...analysis.impacts.tiers.primary],
|
|
91
|
+
symbols: sortedUnique([...analysis.impacts.direct, ...analysis.impacts.transitive]),
|
|
92
|
+
tests: sortedUnique(analysis.relevantTests),
|
|
93
|
+
fileDigests: { ...fileDigests },
|
|
94
|
+
},
|
|
95
|
+
uncertainty: {
|
|
96
|
+
unknowns: [...analysis.unknowns],
|
|
97
|
+
coverage: {
|
|
98
|
+
scannedFiles: analysis.coverage.scannedFiles,
|
|
99
|
+
skippedFiles: [...analysis.coverage.skippedFiles],
|
|
100
|
+
unsupportedLanguages: [...analysis.coverage.unsupportedLanguages],
|
|
101
|
+
},
|
|
102
|
+
impactTruncated: analysis.impactBounds.stoppedAt.length > 0,
|
|
103
|
+
testScopeTier: analysis.testScope.tier,
|
|
104
|
+
impactedFiles: [...analysis.impacts.impactedFiles],
|
|
105
|
+
securityAssessed: analysis.constraints.security.assessed,
|
|
106
|
+
compatibilityAssessed: analysis.constraints.compatibility.assessed,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function writeAtomic(file, body) {
|
|
111
|
+
// Same-directory temp + rename: a reader either sees the previous artifact or the new one, never
|
|
112
|
+
// a half-written object. The temp name carries the pid so concurrent writers cannot collide.
|
|
113
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(temp, body, { encoding: 'utf8' });
|
|
116
|
+
fs.renameSync(temp, file);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
if (fs.existsSync(temp))
|
|
120
|
+
fs.rmSync(temp, { force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Persist an artifact, addressed by its digest. Re-running the same analysis is a re-run, not a
|
|
125
|
+
* reason to forget what actually happened, so an outcome already recorded at this digest survives.
|
|
126
|
+
*/
|
|
127
|
+
function writeArtifact(dir, artifact) {
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
const file = path.join(dir, digestFilename(artifact.digest));
|
|
130
|
+
let merged = artifact;
|
|
131
|
+
if (fs.existsSync(file)) {
|
|
132
|
+
try {
|
|
133
|
+
const existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
134
|
+
if (existing.outcome && !artifact.outcome)
|
|
135
|
+
merged = { ...artifact, outcome: existing.outcome };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// An unreadable predecessor is replaced by a valid record; `readArtifacts` is what reports
|
|
139
|
+
// corruption, and refusing to write here would strand the directory in its broken state.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
writeAtomic(file, `${JSON.stringify(merged, null, 2)}\n`);
|
|
143
|
+
return file;
|
|
144
|
+
}
|
|
145
|
+
function readArtifacts(dir) {
|
|
146
|
+
if (!fs.existsSync(dir))
|
|
147
|
+
return { artifacts: [], unreadable: [], skippedSchema: 0 };
|
|
148
|
+
const artifacts = [];
|
|
149
|
+
const unreadable = [];
|
|
150
|
+
let skippedSchema = 0;
|
|
151
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
152
|
+
if (!name.endsWith('.json'))
|
|
153
|
+
continue;
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
unreadable.push(name);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (parsed?.artifactSchema !== exports.EVIDENCE_ARTIFACT_SCHEMA) {
|
|
163
|
+
skippedSchema++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
artifacts.push(parsed);
|
|
167
|
+
}
|
|
168
|
+
return { artifacts, unreadable, skippedSchema };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Attach what actually happened to a prediction that was actually made. An unknown digest is
|
|
172
|
+
* refused: inventing the prediction alongside the outcome would let the record score itself.
|
|
173
|
+
*/
|
|
174
|
+
function recordOutcome(dir, digest, outcome, recordedAt) {
|
|
175
|
+
const file = path.join(dir, digestFilename(digest));
|
|
176
|
+
if (!fs.existsSync(file)) {
|
|
177
|
+
throw new Error(`Refusing to record an outcome for an unknown analysis digest: ${digest}`);
|
|
178
|
+
}
|
|
179
|
+
const artifact = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
180
|
+
const updated = {
|
|
181
|
+
...artifact,
|
|
182
|
+
outcome: {
|
|
183
|
+
recordedAt,
|
|
184
|
+
actualFiles: sortedUnique(outcome.actualFiles ?? []),
|
|
185
|
+
actualSymbols: sortedUnique(outcome.actualSymbols ?? []),
|
|
186
|
+
actualTests: sortedUnique(outcome.actualTests ?? []),
|
|
187
|
+
...(outcome.actualClassification ? { actualClassification: outcome.actualClassification } : {}),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
writeAtomic(file, `${JSON.stringify(updated, null, 2)}\n`);
|
|
191
|
+
return updated;
|
|
192
|
+
}
|
|
193
|
+
const BIN_EDGES = [0, 0.2, 0.4, 0.6, 0.8, 1];
|
|
194
|
+
/**
|
|
195
|
+
* Reliability bins plus a Brier score. `minSamples` is the honesty knob: below it a bin reports
|
|
196
|
+
* `insufficient-data` and a null accuracy, because one sample in a bin yields a 0 or a 1 that looks
|
|
197
|
+
* like a measurement and is really just noise.
|
|
198
|
+
*/
|
|
199
|
+
function computeCalibration(artifacts, opts = {}) {
|
|
200
|
+
const minSamples = Math.max(1, Math.floor(opts.minSamples ?? 5));
|
|
201
|
+
const scored = artifacts.filter((a) => a.outcome?.actualClassification !== undefined);
|
|
202
|
+
const withoutClassification = artifacts.length - scored.length; // @implements A-SPEC-434
|
|
203
|
+
let falsePositives = 0;
|
|
204
|
+
let falseNegatives = 0;
|
|
205
|
+
for (const artifact of artifacts) {
|
|
206
|
+
// File-level FP/FN need only a recorded file set, so they are counted over every artifact whose
|
|
207
|
+
// outcome names one — independent of whether the classification was judged.
|
|
208
|
+
if (!artifact.outcome)
|
|
209
|
+
continue;
|
|
210
|
+
const actual = new Set(artifact.outcome.actualFiles);
|
|
211
|
+
const predicted = new Set(artifact.predicted.files);
|
|
212
|
+
for (const file of predicted)
|
|
213
|
+
if (!actual.has(file))
|
|
214
|
+
falsePositives++;
|
|
215
|
+
for (const file of actual)
|
|
216
|
+
if (!predicted.has(file))
|
|
217
|
+
falseNegatives++;
|
|
218
|
+
}
|
|
219
|
+
const correctness = scored.map((a) => ({
|
|
220
|
+
confidence: a.confidence,
|
|
221
|
+
correct: a.outcome.actualClassification === a.classification,
|
|
222
|
+
}));
|
|
223
|
+
const bins = [];
|
|
224
|
+
for (let i = 0; i < BIN_EDGES.length - 1; i++) {
|
|
225
|
+
const lower = BIN_EDGES[i];
|
|
226
|
+
const upper = BIN_EDGES[i + 1];
|
|
227
|
+
const isLast = i === BIN_EDGES.length - 2;
|
|
228
|
+
const members = correctness.filter((c) => c.confidence >= lower && (isLast ? c.confidence <= upper : c.confidence < upper));
|
|
229
|
+
const enough = members.length >= minSamples;
|
|
230
|
+
bins.push({
|
|
231
|
+
lower,
|
|
232
|
+
upper,
|
|
233
|
+
n: members.length,
|
|
234
|
+
meanConfidence: enough ? members.reduce((sum, c) => sum + c.confidence, 0) / members.length : null,
|
|
235
|
+
observedAccuracy: enough ? members.filter((c) => c.correct).length / members.length : null,
|
|
236
|
+
status: enough ? 'measured' : 'insufficient-data',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const brierScore = correctness.length === 0
|
|
240
|
+
? null
|
|
241
|
+
: correctness.reduce((sum, c) => sum + (c.confidence - (c.correct ? 1 : 0)) ** 2, 0) / correctness.length;
|
|
242
|
+
return {
|
|
243
|
+
schema: exports.EVIDENCE_ARTIFACT_SCHEMA,
|
|
244
|
+
n: correctness.length,
|
|
245
|
+
withoutClassification,
|
|
246
|
+
bins,
|
|
247
|
+
brierScore,
|
|
248
|
+
falsePositives,
|
|
249
|
+
falseNegatives,
|
|
250
|
+
minSamples,
|
|
251
|
+
status: correctness.length >= minSamples ? 'measured' : 'insufficient-data',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -12,20 +12,23 @@ const validate_args_1 = require("./validate-args");
|
|
|
12
12
|
// @implements A-SPEC-100.2
|
|
13
13
|
const store = new spec_store_1.LocalMarkdownRepository(process.env.HOLMES_SPECS ?? '.ax/specs');
|
|
14
14
|
// @implements A-SPEC-263.1 — the elicitation approval channel's wiring. The capability arrives at
|
|
15
|
-
// initialize, AFTER this factory runs, so it is consulted lazily at CALL time
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
15
|
+
// initialize, AFTER this factory runs, so it is consulted lazily at CALL time.
|
|
16
|
+
// @implements A-SPEC-497.1 — the three fates, told apart at THIS seam (the only place the SDK error
|
|
17
|
+
// is visible): a RequestTimeout rejection is `expired` and earns a name in the refusal; every other
|
|
18
|
+
// failure — no capability, transport error, malformed answer — folds to `silent`, whose refusal
|
|
19
|
+
// stays byte-identical to the pre-elicitation one. Diagnostics, if ever needed, go to stderr only
|
|
20
|
+
// (stdout is the protocol channel).
|
|
21
|
+
const { ELICIT_TIMEOUT_MS, buildElicitRequest, classifyElicitError, interpretElicitResult } = require('./elicit-approval');
|
|
20
22
|
const elicit = async (req) => {
|
|
21
23
|
try {
|
|
22
24
|
if (!server.getClientCapabilities()?.elicitation)
|
|
23
|
-
return
|
|
24
|
-
const r = await server.elicitInput(buildElicitRequest(req), { timeout:
|
|
25
|
-
|
|
25
|
+
return { kind: 'silent' };
|
|
26
|
+
const r = await server.elicitInput(buildElicitRequest(req), { timeout: ELICIT_TIMEOUT_MS });
|
|
27
|
+
const d = interpretElicitResult(r);
|
|
28
|
+
return d === null ? { kind: 'silent' } : { kind: 'answered', decision: d };
|
|
26
29
|
}
|
|
27
|
-
catch {
|
|
28
|
-
return
|
|
30
|
+
catch (e) {
|
|
31
|
+
return classifyElicitError(e);
|
|
29
32
|
}
|
|
30
33
|
};
|
|
31
34
|
const handlers = (0, handlers_1.makeHandlers)(store, {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TOOL_SCHEMAS = exports.HOOK_ENFORCED_TOOLS = void 0;
|
|
4
|
+
// @implements A-SPEC-277
|
|
5
|
+
// @implements A-SPEC-269
|
|
6
|
+
// @implements A-SPEC-267
|
|
4
7
|
const spec_types_1 = require("../spec/spec-types");
|
|
5
8
|
const phase_1 = require("../guardrail/phase");
|
|
6
9
|
const str = (description) => ({ type: 'string', description });
|
|
7
10
|
const num = (description) => ({ type: 'number', description });
|
|
11
|
+
const bool = (description) => ({ type: 'boolean', description });
|
|
8
12
|
const strArray = (description) => ({ type: 'array', items: { type: 'string' }, description });
|
|
9
13
|
// @implements A-SPEC-189 §10 (round 11) — this said "must equal `git rev-parse --show-toplevel`",
|
|
10
14
|
// and A-SPEC-128 had already replaced `assertRepoTopLevel` with `resolveProjectRoot`: measured,
|
|
@@ -208,6 +212,73 @@ exports.TOOL_SCHEMAS = {
|
|
|
208
212
|
required: ['root', 'issue'],
|
|
209
213
|
},
|
|
210
214
|
},
|
|
215
|
+
maintenance_analyze: {
|
|
216
|
+
// @implements A-SPEC-360 — the disclaimer is measured, not hedged: eight computed signals were
|
|
217
|
+
// correlated against delivered accuracy on two corpora and none agreed even in sign.
|
|
218
|
+
// @implements A-SPEC-437 — and the numbers in it are kept current. They were -0.062/-0.016 when
|
|
219
|
+
// first written; re-measuring on the shipped path gave -0.309/-0.212, which is a different claim
|
|
220
|
+
// (no signal versus an inverted one) and the caller reads this string, not the source.
|
|
221
|
+
description: 'Graph-first maintenance analysis: classify a request as an existing change, new requirement, or ambiguous; rank code/spec targets; compute call/contract/test impact; attach bounded git rationale, coverage gaps, build basis, and a reproducible evidence digest. The returned `confidence` summarizes WHICH KINDS of evidence were present (direct symbols, relevant tests, history), and is NOT A PROBABILITY and not an estimate of accuracy: re-measured 2026-08-29 on the 30-case clean slice of each corpus it is ANTI-PREDICTIVE — the correlation with delivered localization accuracy is -0.309 here and -0.212 on a second corpus, and the HIGHEST band is the worst on both (on the second corpus the two cases scored 0.85 found nothing at all). An earlier run put it at -0.062/-0.016; the sign has been stable and the magnitude has grown. Do not weight answers by it. Read the candidates, unknowns and coverage instead; the bands and their sample sizes are in review/confidence-calibration.ts. With the opt-in cloud semantic tier active, also read: `sem-verify(…)` evidence lines (display-only calibration — never a hard filter), `semCos` on rankedImpact rows, and `semanticAlternates`: the top-3 semantic matches the lexical emission missed — on lexically-poor requests the true target is often there (measured ×1.25–×2.25 reach gain), so weigh them as candidates too.',
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: 'object',
|
|
224
|
+
properties: {
|
|
225
|
+
root: ROOT_ANY,
|
|
226
|
+
request: str('Issue, change request, or new requirement to analyze against the CPG, RTM, tests, and history.'),
|
|
227
|
+
historyLimit: num('Maximum matching git history records to include (default 20, clamped to 0..100).'),
|
|
228
|
+
contextBudget: num('Token budget for the top graph seed context bundle (default 2000, clamped to 100..10000).'),
|
|
229
|
+
changedFiles: { type: 'array', items: { type: 'string' }, description: 'Optional changed-file basis (e.g. a base..head diff). Required to assess the architecture axis (.ax/decisions and C-SPEC documents), which the code scanner never sees. Omit it and that axis is reported as NOT assessed rather than as empty.' },
|
|
230
|
+
persist: bool('Write the analysis to .ax/evidence/maintenance as a versioned artifact addressed by its evidence digest. Default false: the tool is read-only unless persistence is explicitly requested.'),
|
|
231
|
+
groundTruth: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
description: 'Optional replay ground truth (e.g. the files/symbols/tests a historical commit actually changed). Supplying it scores the graph-off / current / enhanced ablation arms; omitting it reports those metrics as null rather than as zero.',
|
|
234
|
+
properties: {
|
|
235
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Source files the change actually touched.' },
|
|
236
|
+
symbols: { type: 'array', items: { type: 'string' }, description: 'Qualified symbol names the change actually touched.' },
|
|
237
|
+
tests: { type: 'array', items: { type: 'string' }, description: 'Test files the change actually touched.' },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ['root', 'request'],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
maintenance_outcome: {
|
|
245
|
+
description: 'Record what a change actually touched against a previously persisted maintenance analysis, so the prediction can later be scored. The digest must name an analysis this project already stored.',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
root: ROOT_ANY,
|
|
250
|
+
digest: str('The evidenceDigest of the persisted analysis being scored (sha256:<64 hex>).'),
|
|
251
|
+
actualFiles: { type: 'array', items: { type: 'string' }, description: 'Source files the change actually touched.' },
|
|
252
|
+
actualSymbols: { type: 'array', items: { type: 'string' }, description: 'Qualified symbol names the change actually touched.' },
|
|
253
|
+
actualTests: { type: 'array', items: { type: 'string' }, description: 'Test files the change actually touched.' },
|
|
254
|
+
actualClassification: str('The judged classification (existing-requirement-change | new-requirement | ambiguous). Omit it to record files without scoring the confidence — an unjudged record counts as unscored, never as wrong.'),
|
|
255
|
+
},
|
|
256
|
+
required: ['root', 'digest'],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
maintenance_calibration: {
|
|
260
|
+
description: 'Measure confidence calibration over persisted maintenance analyses that have a recorded outcome: per-confidence-bin observed accuracy, Brier score, and file-level false positives/negatives. Bins below minSamples report insufficient-data rather than a noisy score.',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
root: ROOT_ANY,
|
|
265
|
+
minSamples: num('Minimum samples before a bin (or the whole record) is reported as measured (default 5).'),
|
|
266
|
+
},
|
|
267
|
+
required: ['root'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
impact_gate_check: {
|
|
271
|
+
description: 'Pre-edit impact evidence verdict: given an edit target and (optionally) the digest of a persisted analysis, return allow | widen | refuse with every reason that applied and a remediation. Read-only and advisory — it reports a judgement, it does not block anything.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties: {
|
|
275
|
+
root: ROOT_ANY,
|
|
276
|
+
target: str('Repo-relative path the session intends to edit.'),
|
|
277
|
+
digest: str('evidenceDigest of the persisted maintenance analysis backing this edit (sha256:<64 hex>). Omit it to ask whether ANY evidence exists — the answer is then a refusal naming what to run.'),
|
|
278
|
+
},
|
|
279
|
+
required: ['root', 'target'],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
211
282
|
rtm_impact: {
|
|
212
283
|
description: 'Given changed symbol qualified-names, return the impacted SPEC node ids reachable through @implements/depends_on edges ({ impacted }).',
|
|
213
284
|
inputSchema: {
|
|
@@ -46,7 +46,9 @@ const node_child_process_1 = require("node:child_process");
|
|
|
46
46
|
// @implements A-SPEC-205
|
|
47
47
|
function cleanSubprocessEnv(env = process.env) {
|
|
48
48
|
const cleaned = { ...env };
|
|
49
|
-
|
|
49
|
+
// @implements A-SPEC-477 — the semantic key is egress consent; a child process that inherits
|
|
50
|
+
// it could exfiltrate without the gate ever seeing a command mention the name.
|
|
51
|
+
const SCRUB_KEYS = new Set(['HOLMES_APPROVAL', 'HOLMES_LEDGER_KEY', 'HOLMES_ROLE', 'HOLMES_ENV_CHANNEL', 'HOLMES_SEMANTIC_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY']);
|
|
50
52
|
for (const k of Object.keys(cleaned)) {
|
|
51
53
|
if (SCRUB_KEYS.has(k.toUpperCase())) {
|
|
52
54
|
delete cleaned[k];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ScannedFile } from '../cpg/cpg-scanner';
|
|
2
|
+
/**
|
|
3
|
+
* What the same question looks like with no knowledge graph at all.
|
|
4
|
+
*
|
|
5
|
+
* Goal Phase B calls this the only path that turns "better than a human" from a claim into a
|
|
6
|
+
* measurement, and operating principle 10 requires the comparison before the phrase may be used.
|
|
7
|
+
* Every number in this work so far compares Holmes to *earlier Holmes*; none of them says whether
|
|
8
|
+
* any of the machinery beats plain word matching.
|
|
9
|
+
*
|
|
10
|
+
* So this arm is deliberately impoverished, and each omission is the point rather than an oversight:
|
|
11
|
+
* no spec-anchor bonus, no citation recovery, no graph expansion, no corpus IDF. Term hits against
|
|
12
|
+
* symbol names and the path, and nothing else — a stand-in for grepping the tree for the words in
|
|
13
|
+
* the ticket, which is the floor a developer without tooling actually works from.
|
|
14
|
+
*
|
|
15
|
+
* It is a floor, not a model of an expert. A skilled engineer who knows the codebase would do better
|
|
16
|
+
* than this, and beating it is necessary rather than sufficient for the claim Phase B is about.
|
|
17
|
+
*/
|
|
18
|
+
export interface BaselineHit {
|
|
19
|
+
file: string;
|
|
20
|
+
score: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function rankBaseline(issueText: string, scanned: readonly ScannedFile[], topN: number): BaselineHit[];
|
|
23
|
+
/**
|
|
24
|
+
* @implements A-SPEC-368
|
|
25
|
+
* The request as an outsider would have written it: with any spec citation removed.
|
|
26
|
+
*
|
|
27
|
+
* The replay feeds commit subjects in as the "issue", and 88.3% of this repository's subjects name a
|
|
28
|
+
* spec outright (`fix(S-266): …`) against 0% of the second corpus's (A-SPEC-367). Citation is the
|
|
29
|
+
* strongest signal the scorer has — a +20 bonus against a lexical hit worth 3 — so a large share of
|
|
30
|
+
* the measured advantage may rest on a commit-message convention that no incoming bug report will
|
|
31
|
+
* follow.
|
|
32
|
+
*
|
|
33
|
+
* Removing it is the direct test. Both readings get reported: a maintainer working inside a
|
|
34
|
+
* Holmes-governed project really does cite specs, and an outside reporter really does not, so the
|
|
35
|
+
* cited and uncited numbers bound what a user should expect rather than one replacing the other.
|
|
36
|
+
*/
|
|
37
|
+
export declare function stripCitations(text: string): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rankBaseline = rankBaseline;
|
|
4
|
+
exports.stripCitations = stripCitations;
|
|
5
|
+
const localize_1 = require("../rtm/localize");
|
|
6
|
+
const tokensOf = (s) => new Set((0, localize_1.significantTerms)(s));
|
|
7
|
+
function rankBaseline(issueText, scanned, topN) {
|
|
8
|
+
const terms = new Set((0, localize_1.significantTerms)(issueText));
|
|
9
|
+
if (terms.size === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const hits = [];
|
|
12
|
+
for (const f of scanned) {
|
|
13
|
+
let score = 0;
|
|
14
|
+
for (const sym of f.symbols) {
|
|
15
|
+
const st = tokensOf(sym.qualifiedName);
|
|
16
|
+
for (const t of terms)
|
|
17
|
+
if (st.has(t))
|
|
18
|
+
score += 3;
|
|
19
|
+
}
|
|
20
|
+
const pt = tokensOf(f.sourcePath);
|
|
21
|
+
for (const t of terms)
|
|
22
|
+
if (pt.has(t))
|
|
23
|
+
score += 1;
|
|
24
|
+
if (score > 0)
|
|
25
|
+
hits.push({ file: f.sourcePath, score });
|
|
26
|
+
}
|
|
27
|
+
return hits.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file)).slice(0, topN);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @implements A-SPEC-368
|
|
31
|
+
* The request as an outsider would have written it: with any spec citation removed.
|
|
32
|
+
*
|
|
33
|
+
* The replay feeds commit subjects in as the "issue", and 88.3% of this repository's subjects name a
|
|
34
|
+
* spec outright (`fix(S-266): …`) against 0% of the second corpus's (A-SPEC-367). Citation is the
|
|
35
|
+
* strongest signal the scorer has — a +20 bonus against a lexical hit worth 3 — so a large share of
|
|
36
|
+
* the measured advantage may rest on a commit-message convention that no incoming bug report will
|
|
37
|
+
* follow.
|
|
38
|
+
*
|
|
39
|
+
* Removing it is the direct test. Both readings get reported: a maintainer working inside a
|
|
40
|
+
* Holmes-governed project really does cite specs, and an outside reporter really does not, so the
|
|
41
|
+
* cited and uncited numbers bound what a user should expect rather than one replacing the other.
|
|
42
|
+
*/
|
|
43
|
+
function stripCitations(text) {
|
|
44
|
+
// Every spelling the citation extractor recognises — A-SPEC-12, S-262.1, REQ-9, T-SPEC-3 — plus
|
|
45
|
+
// the conventional `fix(S-266):` wrapper, whose parentheses would otherwise leave `()` behind.
|
|
46
|
+
return text
|
|
47
|
+
.replace(/\(\s*(?:[AHT]-SPEC|REQ|S)-\d+(?:\.\d+)?\s*\)/gi, '')
|
|
48
|
+
.replace(/\b(?:[AHT]-SPEC|REQ|S)-\d+(?:\.\d+)?\b/gi, '')
|
|
49
|
+
.replace(/\s{2,}/g, ' ')
|
|
50
|
+
.trim();
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CapturedStdinViolation {
|
|
2
|
+
file: string;
|
|
3
|
+
/** The variable holding the captured output. */
|
|
4
|
+
variable: string;
|
|
5
|
+
/** 1-indexed line of the `input:` that receives it. */
|
|
6
|
+
line: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function capturedStdinViolations(source: string, file: string): CapturedStdinViolation[];
|