@dzhechkov/harness-core 0.8.6 → 0.8.11
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/.dz-manifest.json +361 -97
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/codex-invoke.d.ts +73 -0
- package/dist/codex-invoke.d.ts.map +1 -0
- package/dist/codex-invoke.js +80 -0
- package/dist/codex-invoke.js.map +1 -0
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +63 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +113 -16
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/event-chain.d.ts +30 -0
- package/dist/event-chain.d.ts.map +1 -1
- package/dist/event-chain.js +24 -0
- package/dist/event-chain.js.map +1 -1
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +92 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +24 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/mutation-gate.d.ts +39 -36
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +111 -5
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +186 -7
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +27 -5
- package/dist/plugin.js.map +1 -1
- package/dist/recommend.d.ts +4 -5
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +110 -45
- package/dist/recommend.js.map +1 -1
- package/dist/registry.d.ts +32 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +165 -9
- package/dist/registry.js.map +1 -1
- package/dist/run-records.d.ts +3 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +18 -0
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +95 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +274 -2
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/skill-selection.d.ts +72 -0
- package/dist/skill-selection.d.ts.map +1 -0
- package/dist/skill-selection.js +76 -0
- package/dist/skill-selection.js.map +1 -0
- package/dist/stem.d.ts +12 -0
- package/dist/stem.d.ts.map +1 -0
- package/dist/stem.js +89 -0
- package/dist/stem.js.map +1 -0
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/telemetry-vocabulary.d.ts +7 -0
- package/dist/telemetry-vocabulary.d.ts.map +1 -1
- package/dist/telemetry-vocabulary.js +29 -0
- package/dist/telemetry-vocabulary.js.map +1 -1
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +7 -6
- package/sbom.json +772 -112
- package/src/amendment-trace.ts +12 -1
- package/src/codex-invoke.ts +138 -0
- package/src/compounding.ts +300 -1
- package/src/discrimination-gate.ts +183 -19
- package/src/eta.ts +590 -0
- package/src/event-chain.ts +41 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +110 -4
- package/src/index.ts +73 -6
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/mutation-gate.ts +165 -5
- package/src/operations.ts +207 -7
- package/src/patterns.ts +252 -43
- package/src/plugin.ts +27 -5
- package/src/recommend.ts +116 -46
- package/src/registry.ts +144 -11
- package/src/run-records.ts +23 -0
- package/src/score.ts +361 -3
- package/src/setup.ts +20 -17
- package/src/skill-selection.ts +111 -0
- package/src/stem.ts +87 -0
- package/src/target-integrations.ts +225 -0
- package/src/telemetry-vocabulary.ts +36 -0
- package/src/vector-tier.ts +44 -14
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type LessonForm = 'specific' | 'class';
|
|
4
|
+
export type LessonMatchedForm = 'specific' | 'class' | 'both';
|
|
5
|
+
|
|
6
|
+
export interface LessonFormsInput {
|
|
7
|
+
readonly specific: string;
|
|
8
|
+
readonly classForm?: string;
|
|
9
|
+
readonly classAdvisory?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LessonFormRankedHit<T> {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly value: T;
|
|
15
|
+
readonly matchedForm: LessonForm;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MergedLessonFormHit<T> {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly value: T;
|
|
21
|
+
readonly matchedForm: LessonMatchedForm;
|
|
22
|
+
readonly score: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeLessonForms(specific: string, classInput?: unknown): LessonFormsInput {
|
|
26
|
+
if (classInput === undefined || (typeof classInput === 'string' && classInput.trim() === '')) {
|
|
27
|
+
return { specific };
|
|
28
|
+
}
|
|
29
|
+
if (typeof classInput !== 'string') {
|
|
30
|
+
return { specific, classAdvisory: 'class form must be text; the specific lesson was kept' };
|
|
31
|
+
}
|
|
32
|
+
return { specific, classForm: classInput.trim() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizedText(value: string): string {
|
|
36
|
+
return value.normalize('NFC').toLocaleLowerCase('und').replace(/\s+/gu, ' ').trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapedLiteral(value: string): string {
|
|
40
|
+
return normalizedText(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateClassTemplate(
|
|
44
|
+
specific: string,
|
|
45
|
+
classTemplate: string,
|
|
46
|
+
): { readonly ok: true } | { readonly ok: false; readonly reason: string } {
|
|
47
|
+
const slot = /:([A-Za-z][A-Za-z0-9_-]*)/g;
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
let slots = 0;
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
for (const match of classTemplate.matchAll(slot)) {
|
|
52
|
+
const index = match.index ?? 0;
|
|
53
|
+
const literal = classTemplate.slice(cursor, index);
|
|
54
|
+
if (literal.includes(':')) return { ok: false, reason: 'invalid :slot name' };
|
|
55
|
+
parts.push(escapedLiteral(literal), '(.+?)');
|
|
56
|
+
slots += 1;
|
|
57
|
+
cursor = index + match[0].length;
|
|
58
|
+
}
|
|
59
|
+
const tail = classTemplate.slice(cursor);
|
|
60
|
+
if (tail.includes(':')) return { ok: false, reason: 'invalid :slot name' };
|
|
61
|
+
if (slots === 0) return { ok: false, reason: 'class form must contain at least one :slot' };
|
|
62
|
+
parts.push(escapedLiteral(tail));
|
|
63
|
+
try {
|
|
64
|
+
return new RegExp(`^${parts.join('')}$`, 'u').test(normalizedText(specific))
|
|
65
|
+
? { ok: true }
|
|
66
|
+
: { ok: false, reason: 'class form does not syntactically cover the specific lesson' };
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: false, reason: 'class form contains an invalid template' };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function lessonPairIdOf(specific: string, classTemplate: string, ts: string): string {
|
|
73
|
+
const digest = createHash('sha256')
|
|
74
|
+
.update(`${specific}\u0000${classTemplate}\u0000${ts}`)
|
|
75
|
+
.digest('hex')
|
|
76
|
+
.slice(0, 16);
|
|
77
|
+
return `lesson-pair:${digest}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function mergeLessonMatchedForms(
|
|
81
|
+
left: LessonMatchedForm | undefined,
|
|
82
|
+
right: LessonMatchedForm | undefined,
|
|
83
|
+
): LessonMatchedForm | undefined {
|
|
84
|
+
if (left === undefined) return right;
|
|
85
|
+
if (right === undefined || left === right) return left;
|
|
86
|
+
return 'both';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function mergeLessonFormHits<T>(
|
|
90
|
+
specificHits: readonly LessonFormRankedHit<T>[],
|
|
91
|
+
classHits: readonly LessonFormRankedHit<T>[],
|
|
92
|
+
limit: number,
|
|
93
|
+
): MergedLessonFormHit<T>[] {
|
|
94
|
+
interface Acc { value: T; specific: boolean; classForm: boolean; score: number }
|
|
95
|
+
const merged = new Map<string, Acc>();
|
|
96
|
+
const add = (hit: LessonFormRankedHit<T>, rank: number): void => {
|
|
97
|
+
const current = merged.get(hit.key) ?? { value: hit.value, specific: false, classForm: false, score: 0 };
|
|
98
|
+
if (hit.matchedForm === 'specific') current.specific = true;
|
|
99
|
+
else current.classForm = true;
|
|
100
|
+
current.score += 1 / (60 + rank + 1);
|
|
101
|
+
merged.set(hit.key, current);
|
|
102
|
+
};
|
|
103
|
+
specificHits.forEach(add);
|
|
104
|
+
classHits.forEach(add);
|
|
105
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : merged.size;
|
|
106
|
+
return [...merged.entries()]
|
|
107
|
+
.sort((a, b) => b[1].score - a[1].score || a[0].localeCompare(b[0]))
|
|
108
|
+
.slice(0, safeLimit)
|
|
109
|
+
.map(([key, hit]) => ({
|
|
110
|
+
key,
|
|
111
|
+
value: hit.value,
|
|
112
|
+
matchedForm: hit.specific && hit.classForm ? 'both' : hit.classForm ? 'class' : 'specific',
|
|
113
|
+
score: hit.score,
|
|
114
|
+
}));
|
|
115
|
+
}
|
package/src/mutation-gate.ts
CHANGED
|
@@ -108,6 +108,10 @@ export interface MutationObservation {
|
|
|
108
108
|
readonly rebaselineExitCode?: number | null;
|
|
109
109
|
/** named no-exit reason for the restored-tree attribution run, when it produced none. */
|
|
110
110
|
readonly rebaselineFailureReason?: string;
|
|
111
|
+
/** parsed failing files from a RED restored-tree run; absent when no red rebaseline ran. */
|
|
112
|
+
readonly rebaselineAttribution?: BaselineAttribution;
|
|
113
|
+
/** bounded log proving an internal runner failure received at most one retry. */
|
|
114
|
+
readonly internalAttemptLog?: string;
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
export interface MutationEntryResult {
|
|
@@ -135,6 +139,60 @@ export interface MutationEntryResult {
|
|
|
135
139
|
readonly detail: string;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
// ── Internal runner crash containment — exactly one retry, both attempts observable ───────────
|
|
143
|
+
|
|
144
|
+
export type InternalRunnerAttemptOutcome = 'completed' | 'runner-internal-error';
|
|
145
|
+
|
|
146
|
+
export interface InternalRunnerAttempt {
|
|
147
|
+
readonly attempt: 1 | 2;
|
|
148
|
+
readonly outcome: InternalRunnerAttemptOutcome;
|
|
149
|
+
readonly detail: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface InternalRunnerRetryResult<T> {
|
|
153
|
+
/** The completed attempt's value. null means both attempts threw internally. */
|
|
154
|
+
readonly value: T | null;
|
|
155
|
+
readonly attempts: readonly InternalRunnerAttempt[];
|
|
156
|
+
/** Closed by construction: a runner receives either zero retries or exactly one. */
|
|
157
|
+
readonly internalRetries: 0 | 1;
|
|
158
|
+
/** Named reason consumed by the existing no-exit → INCONCLUSIVE arm. */
|
|
159
|
+
readonly failureReason?: `runner-internal-error: ${string}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function internalRunnerErrorHead(error: unknown): string {
|
|
163
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
164
|
+
const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() || 'unknown internal runner error';
|
|
165
|
+
return Array.from(firstLine).slice(0, 160).join('');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Run one internal runner invocation. Only a THROWN internal error is retried; normal green/red
|
|
170
|
+
* observations and ordinary no-exit observations are values and therefore never retried.
|
|
171
|
+
*/
|
|
172
|
+
export function runWithOneInternalRetry<T>(runner: () => T): InternalRunnerRetryResult<T> {
|
|
173
|
+
const attempts: InternalRunnerAttempt[] = [];
|
|
174
|
+
for (const attempt of [1, 2] as const) {
|
|
175
|
+
try {
|
|
176
|
+
const value = runner();
|
|
177
|
+
attempts.push({ attempt, outcome: 'completed', detail: `attempt ${attempt}: completed` });
|
|
178
|
+
return { value, attempts, internalRetries: attempt === 1 ? 0 : 1 };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const head = internalRunnerErrorHead(error);
|
|
181
|
+
attempts.push({
|
|
182
|
+
attempt,
|
|
183
|
+
outcome: 'runner-internal-error',
|
|
184
|
+
detail: `attempt ${attempt}: runner-internal-error: ${head}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
value: null,
|
|
190
|
+
attempts,
|
|
191
|
+
internalRetries: 1,
|
|
192
|
+
failureReason: 'runner-internal-error: persistent after 2/2 attempts',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
138
196
|
// ── Registry parsing — declarative DATA, validated loudly ─────────────────────────────────────
|
|
139
197
|
|
|
140
198
|
const SAFE_ID = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
|
@@ -495,21 +553,114 @@ export function classifyRunFailure(rawOutput: string): RunFailureClassification
|
|
|
495
553
|
|
|
496
554
|
// ── Baseline (rule 3's runnability half) ──────────────────────────────────────────────────────
|
|
497
555
|
|
|
556
|
+
export type BaselineAttributionSource = 'node-test' | 'vitest' | 'unparseable';
|
|
557
|
+
|
|
558
|
+
export interface BaselineAttribution {
|
|
559
|
+
readonly parsedFrom: BaselineAttributionSource;
|
|
560
|
+
/** Package-relative failing paths, in first-seen order. */
|
|
561
|
+
readonly failingFiles: readonly string[];
|
|
562
|
+
/** Failing paths that match a registry file exactly (or by package-relative suffix). */
|
|
563
|
+
readonly covered: readonly string[];
|
|
564
|
+
/** Failing paths with no matching registry file. */
|
|
565
|
+
readonly extraneous: readonly string[];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function normaliseReportedFile(raw: string): string | null {
|
|
569
|
+
let value = raw.trim().replace(/^['"]|['"]$/g, '').replace(/:\d+(?::\d+)?$/, '');
|
|
570
|
+
value = value.replace(/\\/g, '/');
|
|
571
|
+
const testSegment = value.lastIndexOf('/test/');
|
|
572
|
+
if (testSegment >= 0) value = value.slice(testSegment + 1);
|
|
573
|
+
if (!value.includes('/') || !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(value)) return null;
|
|
574
|
+
return value;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Parse the failing FILE paths already exposed by supported node --test and vitest shapes. */
|
|
578
|
+
export function attributeBaselineRedness(
|
|
579
|
+
rawOutput: string,
|
|
580
|
+
registryFiles: readonly string[],
|
|
581
|
+
): BaselineAttribution {
|
|
582
|
+
const output = stripSgr(rawOutput);
|
|
583
|
+
const files: string[] = [];
|
|
584
|
+
const add = (raw: string): void => {
|
|
585
|
+
const file = normaliseReportedFile(raw);
|
|
586
|
+
if (file !== null && !files.includes(file)) files.push(file);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const vitestMatches = [...output.matchAll(/^\s*FAIL\s+(\S+)/gm)];
|
|
590
|
+
for (const match of vitestMatches) add(match[1] ?? '');
|
|
591
|
+
|
|
592
|
+
const tapMatches = [...output.matchAll(/^not ok \d+\s+-\s+(.+)$/gm)];
|
|
593
|
+
for (const match of tapMatches) add(match[1] ?? '');
|
|
594
|
+
for (const match of output.matchAll(/^\s*location:\s*['"]([^'"]+)['"]\s*$/gm)) {
|
|
595
|
+
add(match[1] ?? '');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const parsedFrom: BaselineAttributionSource = files.length === 0
|
|
599
|
+
? 'unparseable'
|
|
600
|
+
: vitestMatches.length > 0 ? 'vitest' : 'node-test';
|
|
601
|
+
if (parsedFrom === 'unparseable') {
|
|
602
|
+
return { parsedFrom, failingFiles: [], covered: [], extraneous: [] };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const normalisedRegistry = registryFiles
|
|
606
|
+
.map((file) => normaliseReportedFile(file) ?? file.replace(/\\/g, '/'));
|
|
607
|
+
const covered = files.filter((file) => normalisedRegistry.some((registryFile) =>
|
|
608
|
+
file === registryFile || file.endsWith(`/${registryFile}`) || registryFile.endsWith(`/${file}`)));
|
|
609
|
+
const extraneous = files.filter((file) => !covered.includes(file));
|
|
610
|
+
return { parsedFrom, failingFiles: files, covered, extraneous };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export type BaselineFailureReason =
|
|
614
|
+
| 'runner-internal-error'
|
|
615
|
+
| 'runner-no-exit'
|
|
616
|
+
| 'extraneous-red-in-allowlist'
|
|
617
|
+
| 'baseline-red-covered-files'
|
|
618
|
+
| 'baseline-red-files-unparseable';
|
|
619
|
+
|
|
498
620
|
export interface BaselineResult {
|
|
499
621
|
readonly ok: boolean;
|
|
500
622
|
readonly detail: string;
|
|
623
|
+
readonly reason?: BaselineFailureReason;
|
|
501
624
|
}
|
|
502
625
|
|
|
503
626
|
/**
|
|
504
627
|
* A RED baseline in the scratch copy is a SETUP error, never a mutation result: every subsequent
|
|
505
628
|
* "red under mutation" would be noise, and every "green" a lie about an unrunnable copy.
|
|
506
629
|
*/
|
|
507
|
-
export function classifyBaseline(
|
|
630
|
+
export function classifyBaseline(
|
|
631
|
+
exitCode: number | null,
|
|
632
|
+
runFailureReason?: string,
|
|
633
|
+
attribution?: BaselineAttribution,
|
|
634
|
+
): BaselineResult {
|
|
508
635
|
if (exitCode === 0) return { ok: true, detail: 'baseline suite green in the scratch copy' };
|
|
509
636
|
if (exitCode === null) {
|
|
510
|
-
|
|
637
|
+
const internal = runFailureReason?.startsWith('runner-internal-error:') === true;
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
reason: internal ? 'runner-internal-error' : 'runner-no-exit',
|
|
641
|
+
detail: `baseline INCONCLUSIVE — suite produced no exit code (${runFailureReason ?? 'unknown timeout/spawn failure'}) — the copy is not runnable; do not read this as a mutation result`,
|
|
642
|
+
};
|
|
511
643
|
}
|
|
512
|
-
|
|
644
|
+
if (attribution === undefined || attribution.parsedFrom === 'unparseable') {
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
reason: 'baseline-red-files-unparseable',
|
|
648
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — failing files: unparseable from runner output — the broken copy cannot prove anything`,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const failing = `failing files: ${attribution.failingFiles.join(', ')}`;
|
|
652
|
+
if (attribution.extraneous.length > 0 && attribution.covered.length === 0) {
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
reason: 'extraneous-red-in-allowlist',
|
|
656
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — extraneous red in the testCommand allowlist; the registry entries themselves are not disproven`,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
reason: 'baseline-red-covered-files',
|
|
662
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — broken-copy baseline redness touches registry-covered files; fix the copy before evaluating mutations`,
|
|
663
|
+
};
|
|
513
664
|
}
|
|
514
665
|
|
|
515
666
|
// ── Classification (rules 1 + 2, and the drop decision) ───────────────────────────────────────
|
|
@@ -549,7 +700,7 @@ export function classifyBaseline(exitCode: number | null, runFailureReason?: str
|
|
|
549
700
|
* while the contract still holds is the early warning, reported loudly so a human re-pins
|
|
550
701
|
* `observed` or investigates — silently normalising it would erase the signal.
|
|
551
702
|
*/
|
|
552
|
-
|
|
703
|
+
function classifyMutationOutcomeWithoutAttemptLog(obs: MutationObservation): MutationEntryResult {
|
|
553
704
|
const e = obs.entry;
|
|
554
705
|
const minFailing = e.minFailing ?? 1;
|
|
555
706
|
const base = {
|
|
@@ -653,12 +804,15 @@ export function classifyMutationOutcome(obs: MutationObservation): MutationEntry
|
|
|
653
804
|
// not come back green, so the suite is flaky and an unrelated neighbour may be what went red.
|
|
654
805
|
// Not attributable ⇒ INCONCLUSIVE (a failure, never a pass).
|
|
655
806
|
if (obs.rebaselineExitCode !== undefined && obs.rebaselineExitCode !== 0) {
|
|
807
|
+
const failing = obs.rebaselineAttribution === undefined || obs.rebaselineAttribution.parsedFrom === 'unparseable'
|
|
808
|
+
? 'failing files: unparseable from runner output'
|
|
809
|
+
: `failing files: ${obs.rebaselineAttribution.failingFiles.join(', ')}`;
|
|
656
810
|
return {
|
|
657
811
|
...base,
|
|
658
812
|
applied: true,
|
|
659
813
|
verdict: 'INCONCLUSIVE',
|
|
660
814
|
drop: false,
|
|
661
|
-
detail: `suite red under the mutation BUT the restored baseline did not reproduce green (${obs.rebaselineExitCode === null ? `no exit code: ${obs.rebaselineFailureReason ?? 'unknown timeout / spawn failure'}` : `exit ${obs.rebaselineExitCode}`}) —
|
|
815
|
+
detail: `suite red under the mutation BUT the restored baseline did not reproduce green (${obs.rebaselineExitCode === null ? `no exit code: ${obs.rebaselineFailureReason ?? 'unknown timeout / spawn failure'}` : `exit ${obs.rebaselineExitCode}`}) — mutation did not revert / flaky restored-tree route; ${failing}; the redness is not attributable to the protection`,
|
|
662
816
|
};
|
|
663
817
|
}
|
|
664
818
|
|
|
@@ -698,6 +852,12 @@ export function classifyMutationOutcome(obs: MutationObservation): MutationEntry
|
|
|
698
852
|
};
|
|
699
853
|
}
|
|
700
854
|
|
|
855
|
+
export function classifyMutationOutcome(obs: MutationObservation): MutationEntryResult {
|
|
856
|
+
const result = classifyMutationOutcomeWithoutAttemptLog(obs);
|
|
857
|
+
if (obs.internalAttemptLog === undefined) return result;
|
|
858
|
+
return { ...result, detail: `${result.detail}; ${obs.internalAttemptLog}` };
|
|
859
|
+
}
|
|
860
|
+
|
|
701
861
|
/** Verdicts that fail the gate. INCONCLUSIVE and NOT_APPLIED fail (inconclusive ≠ pass). */
|
|
702
862
|
const FAILING_VERDICTS: ReadonlySet<MutationVerdict> = new Set(['UNDEFENDED', 'RECEIPT_MISMATCH', 'NOT_APPLIED', 'BELOW_MIN', 'MUTATION_UNPARSEABLE', 'MUTATION_LOAD_FATAL', 'OVER_FAILING', 'INCONCLUSIVE']);
|
|
703
863
|
|
package/src/operations.ts
CHANGED
|
@@ -57,6 +57,17 @@ import { describeSkillLoadFailure, discoverSkillIds, loadSkillFromDir } from './
|
|
|
57
57
|
import type { SkillApplyFailure, SkillLoadFailure } from './skills.js';
|
|
58
58
|
import { TARGETS } from './targets.js';
|
|
59
59
|
import type { TargetName } from './targets.js';
|
|
60
|
+
import {
|
|
61
|
+
TARGET_INTEGRATIONS,
|
|
62
|
+
aggregateIntegrationManifests,
|
|
63
|
+
notRequestedOutcomes,
|
|
64
|
+
refusedOutcome,
|
|
65
|
+
staticPolicyOutcomes,
|
|
66
|
+
type IntegrationOutcome,
|
|
67
|
+
type IntegrationManifestSource,
|
|
68
|
+
} from './target-integrations.js';
|
|
69
|
+
import { applyIntegrationFragments, IntegrationApplyError, type IntegrationApplyFault } from './integration-apply.js';
|
|
70
|
+
import { verifyTargetIntegration, type IntegrationProcessPort } from './integrations-verify.js';
|
|
60
71
|
import {
|
|
61
72
|
AGENTS_MD_BUDGET_WARN_FRACTION,
|
|
62
73
|
CODEX_PROJECT_DOC_MAX_BYTES,
|
|
@@ -87,6 +98,24 @@ export interface InitOptions {
|
|
|
87
98
|
readonly select?: readonly string[];
|
|
88
99
|
/** When set, generate platform-specific enrichment files alongside SKILL.md. */
|
|
89
100
|
readonly enrich?: boolean;
|
|
101
|
+
/** Exact content-bound digest printed by the first integration-aware run. */
|
|
102
|
+
readonly allowIntegrations?: string;
|
|
103
|
+
/** Explicit hook opt-out; maps hooks to not-requested. */
|
|
104
|
+
readonly noHooks?: boolean;
|
|
105
|
+
/** Explicit skills-only opt-out, even when selected skills carry manifests. */
|
|
106
|
+
readonly noIntegrations?: boolean;
|
|
107
|
+
/** Live verification is load-bearing; requesting this flag makes requested integrations refuse. */
|
|
108
|
+
readonly noVerify?: boolean;
|
|
109
|
+
/** Injectable process boundary for deterministic integration tests. */
|
|
110
|
+
readonly integrationProcessPort?: IntegrationProcessPort;
|
|
111
|
+
/** Test-only fault at the carrier/ownership-journal durability boundary. */
|
|
112
|
+
readonly integrationApplyFault?: IntegrationApplyFault;
|
|
113
|
+
/**
|
|
114
|
+
* Install-level manifest sources. The CLI supplies this only on the first
|
|
115
|
+
* per-directory run so companion planning, authorization, and probing happen
|
|
116
|
+
* exactly once across every discovered skill pack.
|
|
117
|
+
*/
|
|
118
|
+
readonly integrationManifestSources?: readonly IntegrationManifestSource[];
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
/** Per-skill outcome of {@link runInit}. */
|
|
@@ -130,6 +159,10 @@ export interface InitReport {
|
|
|
130
159
|
* any per-skill loop.
|
|
131
160
|
*/
|
|
132
161
|
readonly applyFailures: readonly SkillApplyFailure[];
|
|
162
|
+
/** Exactly two ordered companion outcomes: MCP, then hooks. */
|
|
163
|
+
readonly integrations: readonly [IntegrationOutcome, IntegrationOutcome];
|
|
164
|
+
/** Safe digest used by `--allow-integrations`; absent when no manifest exists. */
|
|
165
|
+
readonly integrationDigest?: string;
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
// ---------------------------------------------------------------------------
|
|
@@ -266,6 +299,8 @@ export interface AgentsMdInitOptions {
|
|
|
266
299
|
readonly projectRoot: string;
|
|
267
300
|
/** When set, install only these skill ids (a preset selection). */
|
|
268
301
|
readonly select?: readonly string[] | undefined;
|
|
302
|
+
readonly noHooks?: boolean;
|
|
303
|
+
readonly noIntegrations?: boolean;
|
|
269
304
|
}
|
|
270
305
|
|
|
271
306
|
/** The parts of a single-file managed-Markdown target that vary by filename. */
|
|
@@ -367,6 +402,28 @@ function runInitSingleFileMd(options: AgentsMdInitOptions, config: SingleFileMdC
|
|
|
367
402
|
}
|
|
368
403
|
const missing = selection === undefined ? [] : selection.filter((id) => !discovered.has(id));
|
|
369
404
|
|
|
405
|
+
let aggregate = { manifest: undefined, digest: undefined } as ReturnType<typeof aggregateIntegrationManifests>;
|
|
406
|
+
try {
|
|
407
|
+
if (options.noIntegrations !== true) {
|
|
408
|
+
aggregate = aggregateIntegrationManifests(picked.map(({ id, skillsDir }) => ({ skillId: id, skillDir: skillsDir })));
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const remediation = error instanceof Error ? error.message : String(error);
|
|
412
|
+
return {
|
|
413
|
+
target: config.target,
|
|
414
|
+
skillsDir: joinedDir,
|
|
415
|
+
projectRoot: options.projectRoot,
|
|
416
|
+
skills: [],
|
|
417
|
+
missing,
|
|
418
|
+
failures: [],
|
|
419
|
+
applyFailures: [],
|
|
420
|
+
integrations: [
|
|
421
|
+
refusedOutcome(config.target, 'mcp', 'MANIFEST_INVALID', remediation),
|
|
422
|
+
refusedOutcome(config.target, 'hooks', 'MANIFEST_INVALID', remediation),
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
370
427
|
// Skip-and-collect: one unloadable skill must not discard the whole aggregation.
|
|
371
428
|
const failures: SkillLoadFailure[] = [];
|
|
372
429
|
const loaded: { id: string; section: string }[] = [];
|
|
@@ -397,7 +454,19 @@ function runInitSingleFileMd(options: AgentsMdInitOptions, config: SingleFileMdC
|
|
|
397
454
|
|
|
398
455
|
// No apply failures are possible here: the single write is outside every per-skill
|
|
399
456
|
// loop, so a write error propagates as itself rather than being attributed to a skill.
|
|
400
|
-
return {
|
|
457
|
+
return {
|
|
458
|
+
target: config.target,
|
|
459
|
+
skillsDir: joinedDir,
|
|
460
|
+
projectRoot: options.projectRoot,
|
|
461
|
+
skills,
|
|
462
|
+
missing,
|
|
463
|
+
failures,
|
|
464
|
+
applyFailures: [],
|
|
465
|
+
integrations: options.noIntegrations === true
|
|
466
|
+
? notRequestedOutcomes(config.target)
|
|
467
|
+
: staticPolicyOutcomes(config.target, aggregate.manifest, options.noHooks === true),
|
|
468
|
+
...(aggregate.digest !== undefined ? { integrationDigest: aggregate.digest } : {}),
|
|
469
|
+
};
|
|
401
470
|
}
|
|
402
471
|
|
|
403
472
|
/**
|
|
@@ -546,6 +615,8 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
|
|
|
546
615
|
skillsDirs: [options.skillsDir],
|
|
547
616
|
projectRoot: options.projectRoot,
|
|
548
617
|
select: options.select,
|
|
618
|
+
...(options.noHooks !== undefined ? { noHooks: options.noHooks } : {}),
|
|
619
|
+
...(options.noIntegrations !== undefined ? { noIntegrations: options.noIntegrations } : {}),
|
|
549
620
|
});
|
|
550
621
|
}
|
|
551
622
|
// gemini is likewise a flattening single-file target — aggregate into ONE root
|
|
@@ -555,6 +626,8 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
|
|
|
555
626
|
skillsDirs: [options.skillsDir],
|
|
556
627
|
projectRoot: options.projectRoot,
|
|
557
628
|
select: options.select,
|
|
629
|
+
...(options.noHooks !== undefined ? { noHooks: options.noHooks } : {}),
|
|
630
|
+
...(options.noIntegrations !== undefined ? { noIntegrations: options.noIntegrations } : {}),
|
|
558
631
|
});
|
|
559
632
|
}
|
|
560
633
|
|
|
@@ -568,6 +641,78 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
|
|
|
568
641
|
const missing = selection === undefined
|
|
569
642
|
? []
|
|
570
643
|
: selection.filter((id) => !discovered.includes(id));
|
|
644
|
+
const manifestSources: readonly IntegrationManifestSource[] = options.integrationManifestSources
|
|
645
|
+
?? ids.map((id) => ({ skillId: id, skillDir: options.skillsDir }));
|
|
646
|
+
let aggregate = { manifest: undefined, digest: undefined } as ReturnType<typeof aggregateIntegrationManifests>;
|
|
647
|
+
try {
|
|
648
|
+
if (options.noIntegrations !== true) aggregate = aggregateIntegrationManifests(manifestSources);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const remediation = error instanceof Error ? error.message : String(error);
|
|
651
|
+
return {
|
|
652
|
+
target: options.target,
|
|
653
|
+
skillsDir: options.skillsDir,
|
|
654
|
+
projectRoot: options.projectRoot,
|
|
655
|
+
skills: [],
|
|
656
|
+
missing,
|
|
657
|
+
failures: [],
|
|
658
|
+
applyFailures: [],
|
|
659
|
+
integrations: [
|
|
660
|
+
refusedOutcome(options.target, 'mcp', 'MANIFEST_INVALID', remediation),
|
|
661
|
+
refusedOutcome(options.target, 'hooks', 'MANIFEST_INVALID', remediation),
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
let integrations: readonly [IntegrationOutcome, IntegrationOutcome] = notRequestedOutcomes(options.target);
|
|
667
|
+
let eligibleClaudePlan: ReturnType<(typeof TARGET_INTEGRATIONS)['claude-code']['plan']> | undefined;
|
|
668
|
+
if (aggregate.manifest !== undefined && options.noIntegrations !== true) {
|
|
669
|
+
if (options.target !== 'claude-code') {
|
|
670
|
+
integrations = staticPolicyOutcomes(options.target, aggregate.manifest, options.noHooks === true);
|
|
671
|
+
} else {
|
|
672
|
+
const mcpRequested = Object.keys(aggregate.manifest.mcpServers ?? {}).length > 0;
|
|
673
|
+
const hooksRequested = (aggregate.manifest.hooks?.length ?? 0) > 0 && options.noHooks !== true;
|
|
674
|
+
let mcpOutcome: IntegrationOutcome = { target: options.target, component: 'mcp', status: 'not-requested', registrations: [] };
|
|
675
|
+
if (mcpRequested) {
|
|
676
|
+
if (options.noVerify === true) {
|
|
677
|
+
mcpOutcome = refusedOutcome(options.target, 'mcp', 'NO_QUALIFYING_LIVE_RECEIPT', '--no-verify cannot authorize integration emission; rerun with live verification enabled');
|
|
678
|
+
} else if (aggregate.digest === undefined || options.allowIntegrations !== aggregate.digest) {
|
|
679
|
+
mcpOutcome = refusedOutcome(options.target, 'mcp', 'INTEGRATION_AUTHORIZATION_REQUIRED', `rerun with --allow-integrations ${aggregate.digest ?? '<missing-digest>'}`);
|
|
680
|
+
} else {
|
|
681
|
+
const plan = TARGET_INTEGRATIONS['claude-code'].plan(aggregate.manifest, { target: 'claude-code' });
|
|
682
|
+
const refusal = plan.refusals.find((row) => row.component === 'mcp');
|
|
683
|
+
if (refusal !== undefined) {
|
|
684
|
+
mcpOutcome = refusedOutcome(options.target, 'mcp', refusal.reasonCode, refusal.remediation);
|
|
685
|
+
} else {
|
|
686
|
+
const preflight = verifyTargetIntegration({
|
|
687
|
+
target: 'claude-code',
|
|
688
|
+
component: 'mcp',
|
|
689
|
+
projectRoot: options.projectRoot,
|
|
690
|
+
phase: 'preflight',
|
|
691
|
+
...(options.integrationProcessPort !== undefined ? { processPort: options.integrationProcessPort } : {}),
|
|
692
|
+
});
|
|
693
|
+
if (!preflight.ok) {
|
|
694
|
+
mcpOutcome = refusedOutcome(options.target, 'mcp', preflight.reasonCode ?? 'LIVE_PROBE_FAILED', preflight.remediation ?? 'live preflight did not qualify');
|
|
695
|
+
} else {
|
|
696
|
+
eligibleClaudePlan = plan;
|
|
697
|
+
mcpOutcome = {
|
|
698
|
+
target: options.target,
|
|
699
|
+
component: 'mcp',
|
|
700
|
+
status: 'emitted',
|
|
701
|
+
registrations: [],
|
|
702
|
+
carrier: { scope: 'project', path: '.mcp.json' },
|
|
703
|
+
...(preflight.runtimeVersion !== undefined ? { runtimeVersion: preflight.runtimeVersion } : {}),
|
|
704
|
+
...(preflight.evidenceVersion !== undefined ? { evidenceVersion: preflight.evidenceVersion } : {}),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const hookOutcome: IntegrationOutcome = hooksRequested
|
|
711
|
+
? refusedOutcome(options.target, 'hooks', 'NO_ACTIVATION_RECEIPT', 'record a nonce canary and negative-control activation receipt')
|
|
712
|
+
: { target: options.target, component: 'hooks', status: 'not-requested', registrations: [] };
|
|
713
|
+
integrations = [mcpOutcome, hookOutcome];
|
|
714
|
+
}
|
|
715
|
+
}
|
|
571
716
|
// Skip-and-collect (D1): one unparseable SKILL.md must not discard the whole install.
|
|
572
717
|
// Same shape as `runVerify`'s long-standing per-id try/catch below.
|
|
573
718
|
//
|
|
@@ -602,7 +747,59 @@ export async function runInit(options: InitOptions): Promise<InitReport> {
|
|
|
602
747
|
applyFailures.push({ id, reason: error instanceof Error ? error.message : String(error) });
|
|
603
748
|
}
|
|
604
749
|
}
|
|
605
|
-
|
|
750
|
+
if (eligibleClaudePlan !== undefined && integrations[0].status === 'emitted') {
|
|
751
|
+
try {
|
|
752
|
+
applyIntegrationFragments({
|
|
753
|
+
projectRoot: options.projectRoot,
|
|
754
|
+
fragments: eligibleClaudePlan.fragments,
|
|
755
|
+
...(options.integrationApplyFault !== undefined ? { injectFault: options.integrationApplyFault } : {}),
|
|
756
|
+
});
|
|
757
|
+
const registrations = Object.keys(aggregate.manifest?.mcpServers ?? {});
|
|
758
|
+
const observed = [];
|
|
759
|
+
let failedObservation: ReturnType<typeof verifyTargetIntegration> | undefined;
|
|
760
|
+
for (const registrationId of registrations) {
|
|
761
|
+
const result = verifyTargetIntegration({
|
|
762
|
+
target: 'claude-code',
|
|
763
|
+
component: 'mcp',
|
|
764
|
+
projectRoot: options.projectRoot,
|
|
765
|
+
registrationId,
|
|
766
|
+
phase: 'post-write',
|
|
767
|
+
...(options.integrationProcessPort !== undefined ? { processPort: options.integrationProcessPort } : {}),
|
|
768
|
+
});
|
|
769
|
+
if (!result.ok) { failedObservation = result; break; }
|
|
770
|
+
observed.push(...result.registrations);
|
|
771
|
+
}
|
|
772
|
+
if (failedObservation !== undefined) {
|
|
773
|
+
integrations = [
|
|
774
|
+
{
|
|
775
|
+
...refusedOutcome(options.target, 'mcp', failedObservation.reasonCode ?? 'POST_WRITE_REGISTRATION_NOT_OBSERVED', failedObservation.remediation ?? 'registration was written but not observed'),
|
|
776
|
+
carrier: { scope: 'project', path: '.mcp.json' },
|
|
777
|
+
applied: true,
|
|
778
|
+
},
|
|
779
|
+
integrations[1],
|
|
780
|
+
];
|
|
781
|
+
} else {
|
|
782
|
+
integrations = [{ ...integrations[0], registrations: observed }, integrations[1]];
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const reasonCode = error instanceof IntegrationApplyError ? error.reasonCode : 'APPLY_FAILED';
|
|
786
|
+
integrations = [{
|
|
787
|
+
...refusedOutcome(options.target, 'mcp', reasonCode, error instanceof Error ? error.message : String(error)),
|
|
788
|
+
...(error instanceof IntegrationApplyError && error.applied ? { applied: true } : {}),
|
|
789
|
+
}, integrations[1]];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return {
|
|
793
|
+
target: options.target,
|
|
794
|
+
skillsDir: options.skillsDir,
|
|
795
|
+
projectRoot: options.projectRoot,
|
|
796
|
+
skills,
|
|
797
|
+
missing,
|
|
798
|
+
failures,
|
|
799
|
+
applyFailures,
|
|
800
|
+
integrations,
|
|
801
|
+
...(aggregate.digest !== undefined ? { integrationDigest: aggregate.digest } : {}),
|
|
802
|
+
};
|
|
606
803
|
}
|
|
607
804
|
|
|
608
805
|
// ---------------------------------------------------------------------------
|
|
@@ -1186,9 +1383,12 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
1186
1383
|
// Silent when a log is absent or has never been chained: an unchained file is legal (FR-5), not a
|
|
1187
1384
|
// fault, and reporting it would train the reader to ignore this line.
|
|
1188
1385
|
try {
|
|
1189
|
-
const { verifyEventChainText, classifyChainDefects, EVENT_CHAIN_SCOPE } = await import('./event-chain.js');
|
|
1190
|
-
|
|
1191
|
-
|
|
1386
|
+
const { verifyEventChainText, classifyChainDefects, EVENT_CHAIN_SCOPE, CHAINED_JOURNALS } = await import('./event-chain.js');
|
|
1387
|
+
// W0-chain (bc4ee35c): enumerated from THE registry, never from a list kept here. The inline
|
|
1388
|
+
// array this replaces is why a journal could be given a chain and still be checked by nobody —
|
|
1389
|
+
// the mechanism present, the coverage absent, and no red anywhere to say so.
|
|
1390
|
+
for (const journal of CHAINED_JOURNALS) {
|
|
1391
|
+
const p = join(root, journal.rel);
|
|
1192
1392
|
if (!existsSync(p)) continue;
|
|
1193
1393
|
const text = readFileSync(p, 'utf-8');
|
|
1194
1394
|
const v = verifyEventChainText(text);
|
|
@@ -1203,7 +1403,7 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
1203
1403
|
// file and false of the present, and a red nobody can act on is a red nobody reads.
|
|
1204
1404
|
if (age.inRun.length === 0 && age.runRecords > 0) {
|
|
1205
1405
|
checks.push({
|
|
1206
|
-
name: `evidence chain (.
|
|
1406
|
+
name: `evidence chain (${journal.rel})`,
|
|
1207
1407
|
ok: true,
|
|
1208
1408
|
// The COUNT carries the meaning, and is printed first for that reason: "1 record forms an
|
|
1209
1409
|
// unbroken run" is true and says almost nothing, while 998 says a great deal. Naming the
|
|
@@ -1216,7 +1416,7 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
1216
1416
|
continue;
|
|
1217
1417
|
}
|
|
1218
1418
|
checks.push({
|
|
1219
|
-
name: `evidence chain (.
|
|
1419
|
+
name: `evidence chain (${journal.rel})`,
|
|
1220
1420
|
ok: false,
|
|
1221
1421
|
detail: `${named} — with NO sound records after them: learning verdicts computed from this log are unsafe. Scope: ${EVENT_CHAIN_SCOPE}`,
|
|
1222
1422
|
});
|