codeloop-mcp-server 0.1.50 → 0.1.52
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/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +8 -0
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/evidence/anti_rationalisation.d.ts +34 -0
- package/dist/evidence/anti_rationalisation.d.ts.map +1 -0
- package/dist/evidence/anti_rationalisation.js +85 -0
- package/dist/evidence/anti_rationalisation.js.map +1 -0
- package/dist/evidence/change_coverage.d.ts +59 -0
- package/dist/evidence/change_coverage.d.ts.map +1 -0
- package/dist/evidence/change_coverage.js +422 -0
- package/dist/evidence/change_coverage.js.map +1 -0
- package/dist/evidence/change_manifest.d.ts +94 -0
- package/dist/evidence/change_manifest.d.ts.map +1 -0
- package/dist/evidence/change_manifest.js +830 -0
- package/dist/evidence/change_manifest.js.map +1 -0
- package/dist/evidence/loop_state.d.ts +53 -0
- package/dist/evidence/loop_state.d.ts.map +1 -0
- package/dist/evidence/loop_state.js +147 -0
- package/dist/evidence/loop_state.js.map +1 -0
- package/dist/evidence/verify_staleness.d.ts +9 -0
- package/dist/evidence/verify_staleness.d.ts.map +1 -0
- package/dist/evidence/verify_staleness.js +180 -0
- package/dist/evidence/verify_staleness.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +374 -19
- package/dist/index.js.map +1 -1
- package/dist/runners/empty_state_detector.d.ts +33 -0
- package/dist/runners/empty_state_detector.d.ts.map +1 -0
- package/dist/runners/empty_state_detector.js +304 -0
- package/dist/runners/empty_state_detector.js.map +1 -0
- package/dist/runners/maestro.d.ts +13 -0
- package/dist/runners/maestro.d.ts.map +1 -1
- package/dist/runners/maestro.js +37 -1
- package/dist/runners/maestro.js.map +1 -1
- package/dist/runners/modal_detector.d.ts +60 -0
- package/dist/runners/modal_detector.d.ts.map +1 -0
- package/dist/runners/modal_detector.js +160 -0
- package/dist/runners/modal_detector.js.map +1 -0
- package/dist/runners/python_tests.d.ts +26 -0
- package/dist/runners/python_tests.d.ts.map +1 -0
- package/dist/runners/python_tests.js +181 -0
- package/dist/runners/python_tests.js.map +1 -0
- package/dist/runners/rust_tests.d.ts +28 -0
- package/dist/runners/rust_tests.d.ts.map +1 -0
- package/dist/runners/rust_tests.js +76 -0
- package/dist/runners/rust_tests.js.map +1 -0
- package/dist/tools/c7_slug.d.ts +14 -0
- package/dist/tools/c7_slug.d.ts.map +1 -0
- package/dist/tools/c7_slug.js +21 -0
- package/dist/tools/c7_slug.js.map +1 -0
- package/dist/tools/diagnose.d.ts.map +1 -1
- package/dist/tools/diagnose.js +13 -0
- package/dist/tools/diagnose.js.map +1 -1
- package/dist/tools/gate_check.d.ts +2 -1
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +74 -32
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/is_ui_project.d.ts +23 -0
- package/dist/tools/is_ui_project.d.ts.map +1 -0
- package/dist/tools/is_ui_project.js +42 -0
- package/dist/tools/is_ui_project.js.map +1 -0
- package/dist/tools/plan_change_journey.d.ts +41 -0
- package/dist/tools/plan_change_journey.d.ts.map +1 -0
- package/dist/tools/plan_change_journey.js +131 -0
- package/dist/tools/plan_change_journey.js.map +1 -0
- package/dist/tools/verify.d.ts +28 -0
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +272 -8
- package/dist/tools/verify.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"critical_floors.d.ts","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"critical_floors.d.ts","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,aAAa,EAkC1C,CAAC"}
|
|
@@ -60,5 +60,13 @@ export const CRITICAL_FLOORS = [
|
|
|
60
60
|
min_version: "0.1.50",
|
|
61
61
|
reason: "WPF / desktop final-mile fixes — pre-0.1.50 builds returned `(undefined, undefined)` when codeloop_interact was called with `text` / `role` / `automation_id` selectors instead of raw x/y on Windows desktop targets (every WPF tab/list-item click missed because the runner had no UIA tree walker for selectors), accepted weak cross-run design_compare matches and scored unrelated screens at 0% (e.g. `10-led-bom_*` paired with `led-design-bom-add-component`), wrote screenshot artifacts under the user's HOME folder when project_dir was omitted on Cursor-launched MCP servers, returned empty arrays from codeloop_discover_screens for desktop projects (designs/desktop/*.png never surfaced), classified MSB3027 / MSB3021 file-locked build errors as `issue_unclassified` (agents looped on the same locked-EXE forever), lacked `codeloop doctor --prune-artifacts` to clean up old corrupt-PNG runs, and surfaced bare 'App window not found' errors with no candidates / next_step diagnostic when the priority ladder exhausted",
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
min_version: "0.1.51",
|
|
65
|
+
reason: "Full auto-loop + modal handling — pre-0.1.51 builds let the agent silently skip codeloop_verify between edits (no post-edit hooks, no staleness directive in tool responses), only ran ONE platform stack on multi-stack monorepos (.NET backend + React frontend / Django + Next.js / Tauri all silently skipped half the codebase), had no Python or Rust verify runners, didn't fire withInitHint on visual_review / design_compare / interaction_replay / generate_dev_report (so fresh workspaces could complete a full visual cycle without ever calling codeloop_init_project), spun the auto-fix loop forever with no server-side iteration cap (no escalate at 15 gate / 8 diagnose attempts), didn't auto-run Maestro flows in verify, and IGNORED MODALS during recording — every Save / Confirm-delete / EULA / browser beforeunload prompt was clicked-through-or-skipped, blocking the rest of the user_journey arc and silently dragging gate confidence down. 0.1.51 closes all of these and adds codeloop_capture_all_screens + codeloop_handle_modal so the loop is finally hands-off.",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
min_version: "0.1.52",
|
|
69
|
+
reason: "Change-aware verification — pre-0.1.52 builds were CHANGE-BLIND: the auto-fix loop happily reached 100% confidence on a recording session that NEVER exercised the new feature the user just added. The Photometry-DB E2E #9 transcript shipped a new Product Code DataGrid column, a new buttons-below-title layout, a new ProductCode property, and an EF migration column — and the gate passed with 11/11 green having only run a 5-click navigation tour on an empty database. 0.1.52 ships C1 (per-run change_manifest.json built from the git diff vs the last verified SHA + uncommitted/untracked files, with feature-shape parsers for XAML / HTML / JSX / C# / TS / Dart / Swift / Kt / EF migrations / SQL DDL plus structural-layout delta detection), C2 (codeloop_plan_change_journey emits a per-manifest-entry interaction script with a HARD seed-first preamble), C3 (change_coverage_evidence blocker gate at 1.0 threshold cross-references the manifest against interaction log + screenshots + replay frames + build/runtime logs), C4 (empty-state seeding enforcement — codeloop_interact appends a HARD directive when the agent targets a row/cell with no prior commit/seed action), C5 (verify cross-checks tasks_completed claims against the manifest and surfaces orphan claims as warnings), C6 (anti-rationalisation directive in gate_check continue_fixing block + recent_thinking phrase scan that surfaces 'comprehensive verification confirms' / 'further interaction would be redundant' style stalls), and C7 (target_change_entry on codeloop_interact + codeloop_capture_screenshot anchors evidence to manifest entries via a deterministic --c7-<slug> filename suffix the C3 gate matches without fuzzy logic).",
|
|
70
|
+
},
|
|
63
71
|
];
|
|
64
72
|
//# sourceMappingURL=critical_floors.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"critical_floors.js","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,ufAAuf;KAC1f;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,4hBAA4hB;KACriB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,yvBAAyvB;KAClwB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,kxBAAkxB;KACrxB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0/BAA0/B;KAC7/B;CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"critical_floors.js","sourceRoot":"","sources":["../../src/auth/critical_floors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAoB;IAC9C;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,ufAAuf;KAC1f;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,4hBAA4hB;KACriB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,yvBAAyvB;KAClwB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,kxBAAkxB;KACrxB;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0/BAA0/B;KAC7/B;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,0iCAA0iC;KAC7iC;IACD;QACE,WAAW,EAAE,QAAQ;QACrB,MAAM,EACJ,gqDAAgqD;KACnqD;CACF,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1.52 C6 — Anti-rationalisation directive + recent_thinking scan.
|
|
3
|
+
*
|
|
4
|
+
* The Photometry-DB E2E #9 transcript ended at 100% confidence with the
|
|
5
|
+
* model narrating polite reasons not to keep iterating: "comprehensive
|
|
6
|
+
* verification confirms ready for production", "the new features are
|
|
7
|
+
* already implemented; further interaction would be redundant",
|
|
8
|
+
* "extensive UI exploration completed". Each phrase is a stand-in for
|
|
9
|
+
* "I will skip the change-aware verification step the C3 gate
|
|
10
|
+
* explicitly requires".
|
|
11
|
+
*
|
|
12
|
+
* This module ships the canonical FORBIDDEN list and a scanner that the
|
|
13
|
+
* gate_check tool can run over an optional `recent_thinking` payload
|
|
14
|
+
* supplied by the agent. When a forbidden phrase fires, the scanner
|
|
15
|
+
* surfaces the matched fragment so the gate's continue_fixing
|
|
16
|
+
* postscript can call out the rationalisation by name and force the
|
|
17
|
+
* agent to take a concrete next step instead of restating the excuse.
|
|
18
|
+
*/
|
|
19
|
+
export declare const C6_FORBIDDEN_PHRASES: Array<{
|
|
20
|
+
regex: RegExp;
|
|
21
|
+
reason: string;
|
|
22
|
+
}>;
|
|
23
|
+
export interface RationalisationHit {
|
|
24
|
+
matched_text: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function scanRecentThinking(text: string | undefined | null): RationalisationHit[];
|
|
28
|
+
/**
|
|
29
|
+
* Renders the C6 anti-rationalisation directive lines for inclusion in
|
|
30
|
+
* the gate_check continue_fixing postscript. Pure formatting — the
|
|
31
|
+
* caller decides where to inject this in the larger directive block.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildAntiRationalisationDirective(hits: RationalisationHit[]): string;
|
|
34
|
+
//# sourceMappingURL=anti_rationalisation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anti_rationalisation.d.ts","sourceRoot":"","sources":["../../src/evidence/anti_rationalisation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA6BzE,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,kBAAkB,EAAE,CAQxF;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,kBAAkB,EAAE,GACzB,MAAM,CAyBR"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1.52 C6 — Anti-rationalisation directive + recent_thinking scan.
|
|
3
|
+
*
|
|
4
|
+
* The Photometry-DB E2E #9 transcript ended at 100% confidence with the
|
|
5
|
+
* model narrating polite reasons not to keep iterating: "comprehensive
|
|
6
|
+
* verification confirms ready for production", "the new features are
|
|
7
|
+
* already implemented; further interaction would be redundant",
|
|
8
|
+
* "extensive UI exploration completed". Each phrase is a stand-in for
|
|
9
|
+
* "I will skip the change-aware verification step the C3 gate
|
|
10
|
+
* explicitly requires".
|
|
11
|
+
*
|
|
12
|
+
* This module ships the canonical FORBIDDEN list and a scanner that the
|
|
13
|
+
* gate_check tool can run over an optional `recent_thinking` payload
|
|
14
|
+
* supplied by the agent. When a forbidden phrase fires, the scanner
|
|
15
|
+
* surfaces the matched fragment so the gate's continue_fixing
|
|
16
|
+
* postscript can call out the rationalisation by name and force the
|
|
17
|
+
* agent to take a concrete next step instead of restating the excuse.
|
|
18
|
+
*/
|
|
19
|
+
export const C6_FORBIDDEN_PHRASES = [
|
|
20
|
+
{
|
|
21
|
+
regex: /\b(no need|already exercised|already covered) to (re-?test|re-?verify|re-?run)\b/i,
|
|
22
|
+
reason: "Claims existing evidence covers the new diff. The C3 gate measures coverage of the diff, not of the whole app — old evidence does not credit new entries.",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
regex: /\b(comprehensive|thorough|extensive|exhaustive)\s+(verification|coverage|testing|exploration)\s+(confirms|shows|demonstrates|already)\b/i,
|
|
26
|
+
reason: "Generic 'comprehensive verification' boilerplate. Cite the specific manifest entries you exercised, not the verb 'comprehensive'.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
regex: /\bfurther\s+(interaction|testing|verification)\s+(would be|is)\s+(redundant|unnecessary|impractical|not (feasible|needed))\b/i,
|
|
30
|
+
reason: "Claims further interaction is unnecessary. The C3 gate disagrees — every unexercised manifest entry is a concrete next step.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
regex: /\b(implementation|feature|change)\s+(?:is|has been|already)\s+(?:complete|implemented|verified)\b.*\bno (specific|further) (?:user )?interaction\b/i,
|
|
34
|
+
reason: "Confuses 'feature implemented' with 'feature exercised in the recording'. The gate requires the latter.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
regex: /\bcode review (confirms|shows|verifies)\s+(the|new)?\s*(features?|changes?)\s+(are|is)\s+(working|correct|ready)\b/i,
|
|
38
|
+
reason: "Substitutes static code reading for the runtime UI exercise the gate requires.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
regex: /\b(declaring|marking|treating)\s+(the )?task\s+(complete|done|ready)\s+(despite|even though|regardless)\b/i,
|
|
42
|
+
reason: "Declares the task complete while admitting gates are failing. Don't.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
regex: /\bgrid (is )?empty\b.*\b(can'?t|cannot|unable to)\s+(test|verify|exercise)\b/i,
|
|
46
|
+
reason: "Empty grid is the C4 directive's exact case — seed data, then exercise. Do not skip the entry.",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
export function scanRecentThinking(text) {
|
|
50
|
+
if (!text || typeof text !== "string")
|
|
51
|
+
return [];
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const { regex, reason } of C6_FORBIDDEN_PHRASES) {
|
|
54
|
+
const m = regex.exec(text);
|
|
55
|
+
if (m)
|
|
56
|
+
out.push({ matched_text: m[0], reason });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Renders the C6 anti-rationalisation directive lines for inclusion in
|
|
62
|
+
* the gate_check continue_fixing postscript. Pure formatting — the
|
|
63
|
+
* caller decides where to inject this in the larger directive block.
|
|
64
|
+
*/
|
|
65
|
+
export function buildAntiRationalisationDirective(hits) {
|
|
66
|
+
const header = [
|
|
67
|
+
"[CodeLoop C6] ANTI-RATIONALISATION DIRECTIVE — your next message MUST NOT make any of these claims:",
|
|
68
|
+
" • 'Comprehensive verification confirms ready for production'",
|
|
69
|
+
" • 'Further interaction would be redundant / impractical'",
|
|
70
|
+
" • 'The features are already implemented; no specific user interaction needed'",
|
|
71
|
+
" • 'Code review confirms the new features are working' (without runtime exercise)",
|
|
72
|
+
" • 'The grid is empty; can't test the new column' (use C4: seed data first)",
|
|
73
|
+
" • 'No need to re-verify' / 'Already covered'",
|
|
74
|
+
"Each manifest entry the change_coverage_evidence gate flags as unexercised is a CONCRETE next step. Drive each one via codeloop_interact (NOT prose). If a step truly cannot be exercised by tools you have, call codeloop_escalate — do NOT rationalise around it.",
|
|
75
|
+
].join("\n");
|
|
76
|
+
if (hits.length === 0)
|
|
77
|
+
return header;
|
|
78
|
+
const hitLines = hits.map((h) => ` - matched "${h.matched_text}" — ${h.reason}`);
|
|
79
|
+
return (header +
|
|
80
|
+
"\n\n" +
|
|
81
|
+
`[CodeLoop C6] Detected ${hits.length} rationalisation phrase(s) in recent_thinking:\n` +
|
|
82
|
+
hitLines.join("\n") +
|
|
83
|
+
"\nRewrite without those phrases and execute the per-gate next steps above instead.");
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=anti_rationalisation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anti_rationalisation.js","sourceRoot":"","sources":["../../src/evidence/anti_rationalisation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAA6C;IAC5E;QACE,KAAK,EAAE,mFAAmF;QAC1F,MAAM,EAAE,2JAA2J;KACpK;IACD;QACE,KAAK,EAAE,0IAA0I;QACjJ,MAAM,EAAE,mIAAmI;KAC5I;IACD;QACE,KAAK,EAAE,+HAA+H;QACtI,MAAM,EAAE,8HAA8H;KACvI;IACD;QACE,KAAK,EAAE,qJAAqJ;QAC5J,MAAM,EAAE,yGAAyG;KAClH;IACD;QACE,KAAK,EAAE,qHAAqH;QAC5H,MAAM,EAAE,gFAAgF;KACzF;IACD;QACE,KAAK,EAAE,4GAA4G;QACnH,MAAM,EAAE,sEAAsE;KAC/E;IACD;QACE,KAAK,EAAE,+EAA+E;QACtF,MAAM,EAAE,gGAAgG;KACzG;CACF,CAAC;AAOF,MAAM,UAAU,kBAAkB,CAAC,IAA+B;IAChE,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACjD,MAAM,GAAG,GAAyB,EAAE,CAAC;IACrC,KAAK,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,oBAAoB,EAAE,CAAC;QACrD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iCAAiC,CAC/C,IAA0B;IAE1B,MAAM,MAAM,GAAG;QACb,qGAAqG;QACrG,gEAAgE;QAChE,4DAA4D;QAC5D,iFAAiF;QACjF,oFAAoF;QACpF,8EAA8E;QAC9E,gDAAgD;QAChD,qQAAqQ;KACtQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAErC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CACvB,CAAC,CAAC,EAAE,EAAE,CACJ,gBAAgB,CAAC,CAAC,YAAY,OAAO,CAAC,CAAC,MAAM,EAAE,CAClD,CAAC;IACF,OAAO,CACL,MAAM;QACN,MAAM;QACN,0BAA0B,IAAI,CAAC,MAAM,kDAAkD;QACvF,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QACnB,oFAAoF,CACrF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ChangeEntry, type ChangeManifest } from "./change_manifest.js";
|
|
2
|
+
/**
|
|
3
|
+
* 0.1.52 C3 — change_coverage_evidence gate.
|
|
4
|
+
*
|
|
5
|
+
* Cross-references the change manifest produced by C1 against the
|
|
6
|
+
* interaction_log.jsonl entries, captured screenshot filenames, and
|
|
7
|
+
* replay-frame analyses across every recent run. Every non-implicit
|
|
8
|
+
* manifest entry must have at least one "exercising event"; the
|
|
9
|
+
* default threshold is 1.0 (every entry exercised) but config can
|
|
10
|
+
* relax it.
|
|
11
|
+
*
|
|
12
|
+
* Implicit entries (currently `method_added`) don't need to be hit
|
|
13
|
+
* directly — they're considered exercised whenever the property they
|
|
14
|
+
* operate on is exercised. The reason: a feature-shaped method like
|
|
15
|
+
* `PropagateProductCodeAsync` runs as a side-effect of typing into the
|
|
16
|
+
* Product Code cell, so we'd otherwise demand an unsatisfiable
|
|
17
|
+
* "exercise the method by name" interaction.
|
|
18
|
+
*/
|
|
19
|
+
export interface ChangeCoverageConfig {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Minimum fraction of manifest entries that must be exercised to pass. Default 1.0 (HARD 100%). */
|
|
22
|
+
threshold: number;
|
|
23
|
+
/** Manifest entry kinds to skip (configurable per-project for genuine pure refactors). */
|
|
24
|
+
skip_kinds: ChangeEntry["kind"][];
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_CHANGE_COVERAGE_CONFIG: ChangeCoverageConfig;
|
|
27
|
+
export interface EntryStatus {
|
|
28
|
+
entry: ChangeEntry;
|
|
29
|
+
display_name: string;
|
|
30
|
+
exercised: boolean;
|
|
31
|
+
/** What evidence credited it. Empty when not exercised. */
|
|
32
|
+
evidence: string[];
|
|
33
|
+
/** True when the entry counts as "implicit" — passes when its sibling property is exercised. */
|
|
34
|
+
implicit: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface ChangeCoverageVerdict {
|
|
37
|
+
passed: boolean;
|
|
38
|
+
threshold: number;
|
|
39
|
+
manifest_run_id: string | null;
|
|
40
|
+
total_entries: number;
|
|
41
|
+
considered_entries: number;
|
|
42
|
+
exercised_entries: number;
|
|
43
|
+
/** entries the agent has not yet exercised. */
|
|
44
|
+
unexercised: EntryStatus[];
|
|
45
|
+
/** Per-entry status. */
|
|
46
|
+
per_entry: EntryStatus[];
|
|
47
|
+
/** Human-readable reason for the gate response. */
|
|
48
|
+
reason: string;
|
|
49
|
+
/** Concrete next_step the agent must execute when the gate fails. */
|
|
50
|
+
next_step: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function evaluateChangeCoverage(cwd: string, runId: string, config?: ChangeCoverageConfig): ChangeCoverageVerdict;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the change-coverage config block out of the project config,
|
|
55
|
+
* falling back to defaults. Tolerates missing fields and unknown keys.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveChangeCoverageConfig(raw: unknown): ChangeCoverageConfig;
|
|
58
|
+
export type { ChangeManifest };
|
|
59
|
+
//# sourceMappingURL=change_coverage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"change_coverage.d.ts","sourceRoot":"","sources":["../../src/evidence/change_coverage.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAG9B;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,oGAAoG;IACpG,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;CACnC;AAED,eAAO,MAAM,8BAA8B,EAAE,oBAI5C,CAAC;AAUF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gGAAgG;IAChG,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,wBAAwB;IACxB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;CACnB;AA0TD,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,oBAAqD,GAC5D,qBAAqB,CA4FvB;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,GACX,oBAAoB,CAgBtB;AAED,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getArtifactsBaseDir, getRunDir, listRuns } from "./artifacts.js";
|
|
4
|
+
import { loadMostRecentChangeManifest, manifestEntryDisplayName, } from "./change_manifest.js";
|
|
5
|
+
import { slugForTargetChangeEntry } from "../tools/c7_slug.js";
|
|
6
|
+
export const DEFAULT_CHANGE_COVERAGE_CONFIG = {
|
|
7
|
+
enabled: true,
|
|
8
|
+
threshold: 1.0,
|
|
9
|
+
skip_kinds: [],
|
|
10
|
+
};
|
|
11
|
+
function readJsonlSafe(path) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = readFileSync(path, "utf-8");
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const line of raw.split("\n")) {
|
|
16
|
+
const t = line.trim();
|
|
17
|
+
if (!t)
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
out.push(JSON.parse(t));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* skip malformed */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function flatten(entries) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const e of entries) {
|
|
35
|
+
const action = (e.action ?? "").toLowerCase();
|
|
36
|
+
const args = (e.input_args ?? {});
|
|
37
|
+
if (action === "sequence" && Array.isArray(args.steps)) {
|
|
38
|
+
for (const c of args.steps) {
|
|
39
|
+
out.push({ ...c, timestamp: c.timestamp ?? e.timestamp, success: c.success ?? e.success });
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (action === "maestro_flow" && Array.isArray(args.maestro_steps)) {
|
|
44
|
+
for (const c of args.maestro_steps) {
|
|
45
|
+
out.push({ ...c, timestamp: c.timestamp ?? e.timestamp, success: c.success ?? e.success });
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out.push(e);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function entryHaystack(e) {
|
|
54
|
+
const args = e.input_args ?? {};
|
|
55
|
+
const parts = [];
|
|
56
|
+
for (const k of [
|
|
57
|
+
"selector",
|
|
58
|
+
"text",
|
|
59
|
+
"aria_label",
|
|
60
|
+
"label",
|
|
61
|
+
"target",
|
|
62
|
+
"automationId",
|
|
63
|
+
"name",
|
|
64
|
+
"intent",
|
|
65
|
+
"description",
|
|
66
|
+
"purpose",
|
|
67
|
+
"step",
|
|
68
|
+
"screen_name",
|
|
69
|
+
"url",
|
|
70
|
+
"value",
|
|
71
|
+
"target_change_entry",
|
|
72
|
+
"automation_action",
|
|
73
|
+
]) {
|
|
74
|
+
const v = args[k];
|
|
75
|
+
if (typeof v === "string")
|
|
76
|
+
parts.push(v);
|
|
77
|
+
}
|
|
78
|
+
if (typeof e.detail === "string")
|
|
79
|
+
parts.push(e.detail);
|
|
80
|
+
if (typeof e.action === "string")
|
|
81
|
+
parts.push(e.action);
|
|
82
|
+
return parts.join(" ").toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
function tokenizeEntryName(name) {
|
|
85
|
+
// Split CamelCase + snake_case + kebab-case into discrete tokens so
|
|
86
|
+
// a manifest "ProductCode" matches an interaction text "Product Code"
|
|
87
|
+
// and vice versa. Tokens are lowercased and >= 2 chars.
|
|
88
|
+
const expanded = name
|
|
89
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
90
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
91
|
+
.replace(/[_\-./]/g, " ");
|
|
92
|
+
return expanded
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.split(/\s+/)
|
|
95
|
+
.filter((t) => t.length >= 2);
|
|
96
|
+
}
|
|
97
|
+
function collectExercisingEvents(cwd, runId) {
|
|
98
|
+
const baseDir = getArtifactsBaseDir(cwd);
|
|
99
|
+
const allLog = [];
|
|
100
|
+
const screenshotNames = [];
|
|
101
|
+
const replayAnalysisChunks = [];
|
|
102
|
+
const shellOutputs = [];
|
|
103
|
+
// Pull from this run + every sibling run in the project. The journey
|
|
104
|
+
// runs typically live in a separate run from the verify (matches how
|
|
105
|
+
// gate_check video / replay scoping works today).
|
|
106
|
+
const runIds = Array.from(new Set([runId, ...listRuns(baseDir)]));
|
|
107
|
+
for (const rid of runIds) {
|
|
108
|
+
const runDir = getRunDir(rid, baseDir);
|
|
109
|
+
if (!existsSync(runDir))
|
|
110
|
+
continue;
|
|
111
|
+
const logsDir = join(runDir, "logs");
|
|
112
|
+
if (existsSync(logsDir)) {
|
|
113
|
+
try {
|
|
114
|
+
for (const f of readdirSync(logsDir)) {
|
|
115
|
+
if (f === "interaction_log.jsonl" || (f.startsWith("interaction_log") && f.endsWith(".jsonl"))) {
|
|
116
|
+
allLog.push(...readJsonlSafe(join(logsDir, f)));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
/* skip */
|
|
122
|
+
}
|
|
123
|
+
// Build / dotnet / generic test logs may mention migration columns
|
|
124
|
+
// by name (the EF runtime prints "Applying migration X / new column
|
|
125
|
+
// Y" for example). We treat these as shell-style outputs the
|
|
126
|
+
// migration_column_added / migration_table_added classifier scans.
|
|
127
|
+
try {
|
|
128
|
+
for (const f of readdirSync(logsDir)) {
|
|
129
|
+
if (!f.endsWith(".log"))
|
|
130
|
+
continue;
|
|
131
|
+
try {
|
|
132
|
+
const txt = readFileSync(join(logsDir, f), "utf-8");
|
|
133
|
+
shellOutputs.push(txt);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* skip */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* skip */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const ssDir = join(runDir, "screenshots");
|
|
145
|
+
if (existsSync(ssDir)) {
|
|
146
|
+
try {
|
|
147
|
+
for (const f of readdirSync(ssDir)) {
|
|
148
|
+
if (f.endsWith(".png") || f.endsWith(".jpg"))
|
|
149
|
+
screenshotNames.push(f);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* skip */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// interaction_replay produces per-frame JSON / a summary file —
|
|
157
|
+
// pick up anything in replay/ or replay_frames/ for token search.
|
|
158
|
+
for (const sub of ["replay", "replay_frames"]) {
|
|
159
|
+
const d = join(runDir, sub);
|
|
160
|
+
if (!existsSync(d))
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
for (const f of readdirSync(d)) {
|
|
164
|
+
if (f.endsWith(".json") || f.endsWith(".txt") || f.endsWith(".md")) {
|
|
165
|
+
try {
|
|
166
|
+
replayAnalysisChunks.push(readFileSync(join(d, f), "utf-8"));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* skip */
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* skip */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const flat = flatten(allLog);
|
|
180
|
+
const resizeWindowEvents = flat.filter((e) => {
|
|
181
|
+
const action = (e.action ?? "").toLowerCase();
|
|
182
|
+
return (action === "resize_window" ||
|
|
183
|
+
action === "resize" ||
|
|
184
|
+
action === "set_window_size" ||
|
|
185
|
+
action === "rotate_device");
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
log: flat,
|
|
189
|
+
screenshotNames,
|
|
190
|
+
replayAnalysisText: replayAnalysisChunks.join("\n").toLowerCase(),
|
|
191
|
+
resizeWindowEvents,
|
|
192
|
+
shellOutputs,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Check whether a manifest entry's name appears in the haystack (interaction log + replay analysis + screenshots). */
|
|
196
|
+
function nameAppearsIn(haystack, name) {
|
|
197
|
+
const tokens = tokenizeEntryName(name);
|
|
198
|
+
if (tokens.length === 0)
|
|
199
|
+
return false;
|
|
200
|
+
// Require ALL tokens to appear (in any order) for multi-token names —
|
|
201
|
+
// this prevents "Product" alone matching unrelated screens.
|
|
202
|
+
// For single-token names, presence of that token is enough.
|
|
203
|
+
return tokens.every((t) => haystack.includes(t));
|
|
204
|
+
}
|
|
205
|
+
function evaluateEntry(entry, ctx) {
|
|
206
|
+
const evidence = [];
|
|
207
|
+
const logHaystack = ctx.log.map(entryHaystack).join(" | ");
|
|
208
|
+
const ssHaystack = ctx.screenshotNames.join(" | ").toLowerCase();
|
|
209
|
+
const replayHaystack = ctx.replayAnalysisText;
|
|
210
|
+
const shellHaystack = ctx.shellOutputs.join("\n").toLowerCase();
|
|
211
|
+
const fullUiHaystack = `${logHaystack} ${ssHaystack} ${replayHaystack}`;
|
|
212
|
+
switch (entry.kind) {
|
|
213
|
+
case "ui_element_added": {
|
|
214
|
+
if (nameAppearsIn(fullUiHaystack, entry.name)) {
|
|
215
|
+
evidence.push(`Name "${entry.name}" referenced by an interaction / screenshot / replay event`);
|
|
216
|
+
}
|
|
217
|
+
// Also accept a screenshot whose `target_change_entry` arg pinned
|
|
218
|
+
// this entry directly (C7 anchored screenshots) — either via the
|
|
219
|
+
// interaction-log arg OR via a filename suffix produced by the
|
|
220
|
+
// codeloop_capture_screenshot anchoring path.
|
|
221
|
+
const display = manifestEntryDisplayName(entry);
|
|
222
|
+
const anchored = ctx.log.some((e) => {
|
|
223
|
+
const arg = e.input_args?.target_change_entry;
|
|
224
|
+
return typeof arg === "string" && arg === display;
|
|
225
|
+
});
|
|
226
|
+
if (anchored)
|
|
227
|
+
evidence.push("C7 target_change_entry anchor present in interaction args");
|
|
228
|
+
const slug = slugForTargetChangeEntry(display);
|
|
229
|
+
const anchoredFilename = ctx.screenshotNames.some((n) => n.toLowerCase().includes(`--c7-${slug}`));
|
|
230
|
+
if (anchoredFilename)
|
|
231
|
+
evidence.push(`C7 anchored screenshot filename matched (slug "${slug}")`);
|
|
232
|
+
return { exercised: evidence.length > 0, evidence };
|
|
233
|
+
}
|
|
234
|
+
case "property_added": {
|
|
235
|
+
if (nameAppearsIn(fullUiHaystack, entry.name) || nameAppearsIn(fullUiHaystack, entry.class)) {
|
|
236
|
+
evidence.push(`Property "${entry.class}.${entry.name}" referenced by an interaction / screenshot / replay event`);
|
|
237
|
+
}
|
|
238
|
+
return { exercised: evidence.length > 0, evidence };
|
|
239
|
+
}
|
|
240
|
+
case "method_added": {
|
|
241
|
+
// Implicit — see header. We pass when the file or class shows up
|
|
242
|
+
// in the haystack, but the gate's main accounting treats this as
|
|
243
|
+
// implicit so the unexercised list never lists it directly.
|
|
244
|
+
if (nameAppearsIn(fullUiHaystack, entry.class) || nameAppearsIn(fullUiHaystack, entry.name)) {
|
|
245
|
+
evidence.push(`Method "${entry.class}.${entry.name}" referenced indirectly`);
|
|
246
|
+
}
|
|
247
|
+
return { exercised: evidence.length > 0, evidence };
|
|
248
|
+
}
|
|
249
|
+
case "migration_column_added": {
|
|
250
|
+
// Build / runtime logs typically print "Applying migration … add
|
|
251
|
+
// column ProductCode on Configurations". A direct sqlite-shell
|
|
252
|
+
// schema verify (action: "shell" or selector containing the
|
|
253
|
+
// column name) also counts.
|
|
254
|
+
if (nameAppearsIn(shellHaystack, entry.column) ||
|
|
255
|
+
nameAppearsIn(shellHaystack, entry.table) ||
|
|
256
|
+
nameAppearsIn(fullUiHaystack, entry.column)) {
|
|
257
|
+
evidence.push(`Migration column "${entry.table}.${entry.column}" referenced by build / interaction logs`);
|
|
258
|
+
}
|
|
259
|
+
return { exercised: evidence.length > 0, evidence };
|
|
260
|
+
}
|
|
261
|
+
case "migration_table_added": {
|
|
262
|
+
if (nameAppearsIn(shellHaystack, entry.table) || nameAppearsIn(fullUiHaystack, entry.table)) {
|
|
263
|
+
evidence.push(`Migration table "${entry.table}" referenced by build / interaction logs`);
|
|
264
|
+
}
|
|
265
|
+
return { exercised: evidence.length > 0, evidence };
|
|
266
|
+
}
|
|
267
|
+
case "layout_restructure": {
|
|
268
|
+
// Layout changes need a real test of the new layout. We require
|
|
269
|
+
// either a window resize / rotate event AND a follow-up
|
|
270
|
+
// screenshot, OR an explicit C7 anchor on a screenshot tied to
|
|
271
|
+
// this restructure file.
|
|
272
|
+
const fileToken = entry.file.split("/").pop()?.toLowerCase() ?? entry.file.toLowerCase();
|
|
273
|
+
const anchored = ctx.log.some((e) => {
|
|
274
|
+
const arg = e.input_args?.target_change_entry;
|
|
275
|
+
return typeof arg === "string" && arg === manifestEntryDisplayName(entry);
|
|
276
|
+
});
|
|
277
|
+
const resized = ctx.resizeWindowEvents.length > 0;
|
|
278
|
+
const screenshotMentionsFile = ctx.screenshotNames.some((n) => n.toLowerCase().includes(fileToken.replace(".xaml", "").replace(".html", "")));
|
|
279
|
+
if (anchored)
|
|
280
|
+
evidence.push("C7 target_change_entry anchor present");
|
|
281
|
+
if (resized && screenshotMentionsFile) {
|
|
282
|
+
evidence.push("Window resize event followed by a screenshot whose name references the changed file");
|
|
283
|
+
}
|
|
284
|
+
return { exercised: evidence.length > 0, evidence };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function nextStepFor(entry) {
|
|
289
|
+
const display = manifestEntryDisplayName(entry);
|
|
290
|
+
switch (entry.kind) {
|
|
291
|
+
case "ui_element_added":
|
|
292
|
+
if (entry.element === "datagrid_column") {
|
|
293
|
+
return `${display} (in ${entry.file}) — drive a codeloop_interact action="click" against a row's "${entry.name}" cell, then action="type" with realistic data, then capture_screenshot with target_change_entry="${display}". If the grid is empty, see the C4 empty-state directive.`;
|
|
294
|
+
}
|
|
295
|
+
if (entry.element === "button") {
|
|
296
|
+
return `${display} (in ${entry.file}) — drive codeloop_interact action="click" with text="${entry.name}", then capture_screenshot with target_change_entry="${display}". If a confirmation modal opens, call codeloop_handle_modal first.`;
|
|
297
|
+
}
|
|
298
|
+
if (entry.element === "menu_item") {
|
|
299
|
+
return `${display} (in ${entry.file}) — drive codeloop_interact action="click" against the menu opener, then click again against text="${entry.name}", then capture_screenshot with target_change_entry="${display}".`;
|
|
300
|
+
}
|
|
301
|
+
return `${display} (in ${entry.file}) — drive codeloop_interact action="click"/"type" against an element with this label, then capture_screenshot with target_change_entry="${display}".`;
|
|
302
|
+
case "property_added":
|
|
303
|
+
return `${display} (in ${entry.file}) — exercise via the UI control bound to this property (typically a textbox / column / toggle whose label resembles "${entry.name}"). Pass description="${display}" on codeloop_interact so the cross-check matches.`;
|
|
304
|
+
case "method_added":
|
|
305
|
+
return `${display} (in ${entry.file}) — implicit. Will be credited when the property/column it operates on is exercised. No direct interaction required.`;
|
|
306
|
+
case "migration_column_added":
|
|
307
|
+
return `${display} (in ${entry.file}) — verify by running codeloop_interact action="shell" with command "sqlite3 <db> '.schema ${entry.table}'" (or psql / dotnet ef migrations script equivalent). The schema dump must contain the column name.`;
|
|
308
|
+
case "migration_table_added":
|
|
309
|
+
return `${display} (in ${entry.file}) — verify by running a schema-dump shell command that references the table, or open a UI screen that loads/persists records of this table.`;
|
|
310
|
+
case "layout_restructure":
|
|
311
|
+
return `${display} — drive codeloop_interact action="resize_window" to a narrow size (e.g. 1024x600), then capture_screenshot with target_change_entry="${display}" so the C3 gate credits the restructure as exercised. If resize_window isn't available on your target platform, capture two screenshots at different window widths and reference this entry by name in the description arg.`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
export function evaluateChangeCoverage(cwd, runId, config = DEFAULT_CHANGE_COVERAGE_CONFIG) {
|
|
315
|
+
if (!config.enabled) {
|
|
316
|
+
return {
|
|
317
|
+
passed: true,
|
|
318
|
+
threshold: config.threshold,
|
|
319
|
+
manifest_run_id: null,
|
|
320
|
+
total_entries: 0,
|
|
321
|
+
considered_entries: 0,
|
|
322
|
+
exercised_entries: 0,
|
|
323
|
+
unexercised: [],
|
|
324
|
+
per_entry: [],
|
|
325
|
+
reason: "change_coverage_evidence is disabled in .codeloop/config.json (gates.change_coverage_evidence.enabled=false).",
|
|
326
|
+
next_step: "",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const { manifest, runId: manifestRunId } = loadMostRecentChangeManifest(cwd, runId);
|
|
330
|
+
if (!manifest || manifest.entries.length === 0) {
|
|
331
|
+
return {
|
|
332
|
+
passed: true,
|
|
333
|
+
threshold: config.threshold,
|
|
334
|
+
manifest_run_id: manifestRunId,
|
|
335
|
+
total_entries: 0,
|
|
336
|
+
considered_entries: 0,
|
|
337
|
+
exercised_entries: 0,
|
|
338
|
+
unexercised: [],
|
|
339
|
+
per_entry: [],
|
|
340
|
+
reason: manifest
|
|
341
|
+
? "Change manifest contains no feature-shaped entries — gate trivially satisfied (this is a pure refactor / docs / dependency bump)."
|
|
342
|
+
: "No change manifest available. Gate trivially satisfied — call codeloop_verify to produce one.",
|
|
343
|
+
next_step: "",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const ctx = collectExercisingEvents(cwd, runId);
|
|
347
|
+
const skipKinds = new Set(config.skip_kinds);
|
|
348
|
+
const perEntry = [];
|
|
349
|
+
for (const entry of manifest.entries) {
|
|
350
|
+
if (skipKinds.has(entry.kind))
|
|
351
|
+
continue;
|
|
352
|
+
const display = manifestEntryDisplayName(entry);
|
|
353
|
+
const implicit = entry.kind === "method_added";
|
|
354
|
+
const { exercised, evidence } = evaluateEntry(entry, ctx);
|
|
355
|
+
perEntry.push({
|
|
356
|
+
entry,
|
|
357
|
+
display_name: display,
|
|
358
|
+
exercised,
|
|
359
|
+
evidence,
|
|
360
|
+
implicit,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const considered = perEntry.filter((p) => !p.implicit);
|
|
364
|
+
const exercised = considered.filter((p) => p.exercised);
|
|
365
|
+
const unexercised = considered.filter((p) => !p.exercised);
|
|
366
|
+
const fraction = considered.length === 0 ? 1 : exercised.length / considered.length;
|
|
367
|
+
const passed = fraction >= config.threshold;
|
|
368
|
+
let reason;
|
|
369
|
+
let nextStep = "";
|
|
370
|
+
if (passed) {
|
|
371
|
+
reason = `Change coverage met: ${exercised.length}/${considered.length} manifest entries exercised (>= ${(config.threshold * 100).toFixed(0)}% threshold).`;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
reason =
|
|
375
|
+
`Change coverage NOT met: ${exercised.length}/${considered.length} manifest entries exercised ` +
|
|
376
|
+
`(${(fraction * 100).toFixed(1)}% — required ${(config.threshold * 100).toFixed(0)}%). ` +
|
|
377
|
+
`Unexercised entries:\n` +
|
|
378
|
+
unexercised
|
|
379
|
+
.slice(0, 10)
|
|
380
|
+
.map((u) => ` - ${u.display_name}`)
|
|
381
|
+
.join("\n") +
|
|
382
|
+
(unexercised.length > 10 ? `\n …and ${unexercised.length - 10} more.` : "");
|
|
383
|
+
nextStep =
|
|
384
|
+
"Per-entry next steps — drive each one via codeloop_interact (NOT raw osascript / PowerShell / xdotool) before re-gating:\n" +
|
|
385
|
+
unexercised
|
|
386
|
+
.slice(0, 8)
|
|
387
|
+
.map((u, i) => ` ${i + 1}. ${nextStepFor(u.entry)}`)
|
|
388
|
+
.join("\n");
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
passed,
|
|
392
|
+
threshold: config.threshold,
|
|
393
|
+
manifest_run_id: manifestRunId,
|
|
394
|
+
total_entries: manifest.entries.length,
|
|
395
|
+
considered_entries: considered.length,
|
|
396
|
+
exercised_entries: exercised.length,
|
|
397
|
+
unexercised,
|
|
398
|
+
per_entry: perEntry,
|
|
399
|
+
reason,
|
|
400
|
+
next_step: nextStep,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Resolve the change-coverage config block out of the project config,
|
|
405
|
+
* falling back to defaults. Tolerates missing fields and unknown keys.
|
|
406
|
+
*/
|
|
407
|
+
export function resolveChangeCoverageConfig(raw) {
|
|
408
|
+
if (!raw || typeof raw !== "object")
|
|
409
|
+
return { ...DEFAULT_CHANGE_COVERAGE_CONFIG };
|
|
410
|
+
const r = raw;
|
|
411
|
+
const out = { ...DEFAULT_CHANGE_COVERAGE_CONFIG };
|
|
412
|
+
if (typeof r.enabled === "boolean")
|
|
413
|
+
out.enabled = r.enabled;
|
|
414
|
+
if (typeof r.threshold === "number" && r.threshold >= 0 && r.threshold <= 1) {
|
|
415
|
+
out.threshold = r.threshold;
|
|
416
|
+
}
|
|
417
|
+
if (Array.isArray(r.skip_kinds)) {
|
|
418
|
+
out.skip_kinds = r.skip_kinds.filter((k) => ["ui_element_added", "property_added", "method_added", "migration_column_added", "migration_table_added", "layout_restructure"].includes(String(k)));
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=change_coverage.js.map
|