@deftai/directive-core 0.105.0 → 0.106.0
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/authz/classify.js +591 -34
- package/dist/check/named-cause.js +1 -1
- package/dist/forward-coverage/diff-coverage.d.ts +39 -0
- package/dist/forward-coverage/diff-coverage.js +112 -0
- package/dist/forward-coverage/diff-lines.d.ts +19 -0
- package/dist/forward-coverage/diff-lines.js +173 -0
- package/dist/forward-coverage/evaluate.d.ts +24 -9
- package/dist/forward-coverage/evaluate.js +125 -19
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +1 -1
- package/dist/intake/clause-derivation.d.ts +9 -0
- package/dist/intake/clause-derivation.js +30 -3
- package/dist/lifecycle-visible/evaluate.d.ts +98 -0
- package/dist/lifecycle-visible/evaluate.js +710 -0
- package/dist/lifecycle-visible/index.d.ts +2 -0
- package/dist/lifecycle-visible/index.js +2 -0
- package/dist/literal-acceptance/capture.js +3 -3
- package/dist/orchestration/judgment-policy.d.ts +13 -0
- package/dist/orchestration/judgment-policy.js +15 -0
- package/dist/orchestration/probe-session.js +13 -8
- package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
- package/dist/orchestration/verify-judgment-gates.js +43 -11
- package/dist/policy/ceremony-dial.js +7 -7
- package/dist/policy/host-hooks.d.ts +20 -0
- package/dist/policy/host-hooks.js +121 -2
- package/dist/policy/org-force-on-migration.js +2 -1
- package/dist/policy/plan-extensions.d.ts +1 -1
- package/dist/policy/plan-extensions.js +2 -0
- package/dist/policy/product-signal.js +6 -7
- package/dist/policy/require-human-merge.js +8 -6
- package/dist/policy/resolve.d.ts +7 -2
- package/dist/policy/resolve.js +25 -8
- package/dist/policy/value-feedback.js +7 -8
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +15 -1
- package/dist/preflight-cache/index.d.ts +1 -1
- package/dist/preflight-cache/index.js +1 -1
- package/dist/product-first-done-gate/evaluate.d.ts +4 -0
- package/dist/product-first-done-gate/evaluate.js +46 -5
- package/dist/release/closed-verb-gate.d.ts +12 -0
- package/dist/release/closed-verb-gate.js +51 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -0
- package/dist/release/pipeline.js +7 -0
- package/dist/release/types.d.ts +12 -0
- package/dist/render/constants.d.ts +2 -1
- package/dist/render/constants.js +2 -1
- package/dist/render/framework-commands.js +4 -0
- package/dist/render/spec-validate.js +23 -12
- package/dist/run-summary/types.d.ts +4 -0
- package/dist/scope/main.js +15 -5
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope-provenance/evaluate.js +3 -3
- package/dist/session/ac-pass-banking.d.ts +5 -0
- package/dist/session/ac-pass-banking.js +3 -2
- package/dist/session/process-cost.d.ts +32 -10
- package/dist/session/process-cost.js +189 -1
- package/dist/session/product-state-hash.js +41 -24
- package/dist/session/ritual-entrypoint.js +16 -1
- package/dist/session/session-start.d.ts +6 -0
- package/dist/session/session-start.js +32 -2
- package/dist/session/verify-session-ritual.d.ts +24 -1
- package/dist/session/verify-session-ritual.js +89 -6
- package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
- package/dist/swarm/finalize-cohort-cli.js +165 -35
- package/dist/swarm/finalize-cohort.d.ts +1 -0
- package/dist/swarm/finalize-cohort.js +14 -2
- package/dist/swarm/routing-set-cli.js +2 -0
- package/dist/swarm/routing-verify.d.ts +1 -1
- package/dist/swarm/routing-verify.js +2 -2
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/help/registry-data.d.ts +1 -1
- package/dist/triage/help/registry-data.js +1 -1
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
- package/dist/verify-env/agent-hook-readiness.js +12 -8
- package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
- package/dist/verify-env/agent-hooks-live-probe.js +26 -5
- package/dist/verify-env/agent-hooks.js +3 -4
- package/package.json +7 -3
|
@@ -20,7 +20,7 @@ const GATE_REMEDIES = {
|
|
|
20
20
|
"verify:test-boundary": "Move tests to the allowed placement or update plan.policy test-boundary allowlist",
|
|
21
21
|
"verify:scope-provenance": "Record approved scope provenance for the failing paths",
|
|
22
22
|
"verify:consumer-check-contract": "Align consumer Taskfile includes with the required gate graph",
|
|
23
|
-
"verify:forward-coverage": "Add tests
|
|
23
|
+
"verify:forward-coverage": "Add tests for new source files and uncovered changed branches (task verify:forward-coverage)",
|
|
24
24
|
"verify:scm-boundary": "Move SCM mutations off GraphQL-heavy paths or wait for rate-limit reset",
|
|
25
25
|
"verify:license-sync": "Sync LICENSE / package license fields",
|
|
26
26
|
"verify:agents-md-budget": "Trim AGENTS.md managed section or raise plan.policy.agentsMdBudget deliberately",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intersect an Istanbul coverage-final.json report with changed lines (#3514).
|
|
3
|
+
*
|
|
4
|
+
* The 90% default is per-change coverage of added/modified branches.
|
|
5
|
+
* The project vitest floor (75) is a collapse detector for the aggregate.
|
|
6
|
+
* They are not interchangeable.
|
|
7
|
+
*/
|
|
8
|
+
/** Per-commit coverage of added/modified branches. Not the 75 global floor. */
|
|
9
|
+
export declare const DEFAULT_DIFF_COVERAGE_THRESHOLD = 90;
|
|
10
|
+
export interface UncoveredChangedBranch {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly line: number;
|
|
13
|
+
readonly branchId: string;
|
|
14
|
+
readonly pathIndex: number;
|
|
15
|
+
readonly type: string;
|
|
16
|
+
}
|
|
17
|
+
export interface DiffCoverageReport {
|
|
18
|
+
readonly reportPresent: boolean;
|
|
19
|
+
readonly skippedReason: string | null;
|
|
20
|
+
readonly changedBranchTotal: number;
|
|
21
|
+
readonly changedBranchCovered: number;
|
|
22
|
+
readonly percent: number;
|
|
23
|
+
readonly threshold: number;
|
|
24
|
+
readonly uncovered: readonly UncoveredChangedBranch[];
|
|
25
|
+
readonly belowThreshold: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface DiffCoverageOptions {
|
|
28
|
+
readonly projectRoot: string;
|
|
29
|
+
readonly coverageReportPath: string;
|
|
30
|
+
readonly changedLinesByFile: ReadonlyMap<string, ReadonlySet<number>>;
|
|
31
|
+
readonly threshold?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Measure coverage of Istanbul branch paths whose line is in the diff.
|
|
35
|
+
* Missing or unreadable reports skip (warn-first); they are not a config error.
|
|
36
|
+
*/
|
|
37
|
+
export declare function evaluateDiffCoverage(options: DiffCoverageOptions): DiffCoverageReport;
|
|
38
|
+
export declare function hasDiffCoverageFindings(report: DiffCoverageReport): boolean;
|
|
39
|
+
//# sourceMappingURL=diff-coverage.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intersect an Istanbul coverage-final.json report with changed lines (#3514).
|
|
3
|
+
*
|
|
4
|
+
* The 90% default is per-change coverage of added/modified branches.
|
|
5
|
+
* The project vitest floor (75) is a collapse detector for the aggregate.
|
|
6
|
+
* They are not interchangeable.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { isAbsolute, relative } from "node:path";
|
|
10
|
+
/** Per-commit coverage of added/modified branches. Not the 75 global floor. */
|
|
11
|
+
export const DEFAULT_DIFF_COVERAGE_THRESHOLD = 90;
|
|
12
|
+
function toPosix(p) {
|
|
13
|
+
return p.replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
function normalizeReportPath(projectRoot, rawKey, file) {
|
|
16
|
+
const candidate = file.path && file.path.length > 0 ? file.path : rawKey;
|
|
17
|
+
const rel = isAbsolute(candidate) ? relative(projectRoot, candidate) : candidate;
|
|
18
|
+
return toPosix(rel);
|
|
19
|
+
}
|
|
20
|
+
function emptyReport(threshold, skippedReason, reportPresent) {
|
|
21
|
+
return {
|
|
22
|
+
reportPresent,
|
|
23
|
+
skippedReason,
|
|
24
|
+
changedBranchTotal: 0,
|
|
25
|
+
changedBranchCovered: 0,
|
|
26
|
+
percent: 100,
|
|
27
|
+
threshold,
|
|
28
|
+
uncovered: [],
|
|
29
|
+
belowThreshold: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function branchLine(meta, pathIndex) {
|
|
33
|
+
const loc = meta?.locations?.[pathIndex];
|
|
34
|
+
return loc?.start?.line ?? meta?.loc?.start?.line ?? meta?.line ?? 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Measure coverage of Istanbul branch paths whose line is in the diff.
|
|
38
|
+
* Missing or unreadable reports skip (warn-first); they are not a config error.
|
|
39
|
+
*/
|
|
40
|
+
export function evaluateDiffCoverage(options) {
|
|
41
|
+
const threshold = options.threshold ?? DEFAULT_DIFF_COVERAGE_THRESHOLD;
|
|
42
|
+
const reportPath = options.coverageReportPath;
|
|
43
|
+
if (!existsSync(reportPath)) {
|
|
44
|
+
return emptyReport(threshold, `coverage report missing at ${reportPath}`, false);
|
|
45
|
+
}
|
|
46
|
+
let rawReport;
|
|
47
|
+
try {
|
|
48
|
+
rawReport = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return emptyReport(threshold, `unreadable coverage report at ${reportPath}: ${String(err.message)}`, false);
|
|
52
|
+
}
|
|
53
|
+
const changedByPosix = new Map();
|
|
54
|
+
for (const [path, lines] of options.changedLinesByFile) {
|
|
55
|
+
changedByPosix.set(toPosix(path), lines);
|
|
56
|
+
}
|
|
57
|
+
const uncovered = [];
|
|
58
|
+
let total = 0;
|
|
59
|
+
let covered = 0;
|
|
60
|
+
for (const [rawKey, file] of Object.entries(rawReport)) {
|
|
61
|
+
const relPath = normalizeReportPath(options.projectRoot, rawKey, file);
|
|
62
|
+
const changedLines = changedByPosix.get(relPath);
|
|
63
|
+
if (changedLines === undefined || changedLines.size === 0) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!file.b) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const [branchId, hits] of Object.entries(file.b)) {
|
|
70
|
+
const meta = file.branchMap?.[branchId];
|
|
71
|
+
for (let pathIndex = 0; pathIndex < hits.length; pathIndex += 1) {
|
|
72
|
+
const count = hits[pathIndex] ?? 0;
|
|
73
|
+
const line = branchLine(meta, pathIndex);
|
|
74
|
+
if (!changedLines.has(line)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
total += 1;
|
|
78
|
+
if (count > 0) {
|
|
79
|
+
covered += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
uncovered.push({
|
|
83
|
+
path: relPath,
|
|
84
|
+
line,
|
|
85
|
+
branchId,
|
|
86
|
+
pathIndex,
|
|
87
|
+
type: meta?.type ?? "branch",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
uncovered.sort((a, b) => a.path.localeCompare(b.path) ||
|
|
93
|
+
a.line - b.line ||
|
|
94
|
+
a.branchId.localeCompare(b.branchId) ||
|
|
95
|
+
a.pathIndex - b.pathIndex);
|
|
96
|
+
const percent = total === 0 ? 100 : (covered / total) * 100;
|
|
97
|
+
const belowThreshold = total > 0 && percent + 1e-9 < threshold;
|
|
98
|
+
return {
|
|
99
|
+
reportPresent: true,
|
|
100
|
+
skippedReason: null,
|
|
101
|
+
changedBranchTotal: total,
|
|
102
|
+
changedBranchCovered: covered,
|
|
103
|
+
percent,
|
|
104
|
+
threshold,
|
|
105
|
+
uncovered,
|
|
106
|
+
belowThreshold,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function hasDiffCoverageFindings(report) {
|
|
110
|
+
return report.uncovered.length > 0 || report.belowThreshold;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=diff-coverage.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse unified diffs into added/modified new-file line numbers (#3514).
|
|
3
|
+
*/
|
|
4
|
+
/** Repo-relative path -> 1-indexed new-file line numbers introduced by the diff. */
|
|
5
|
+
export type ChangedLineMap = Map<string, Set<number>>;
|
|
6
|
+
/** Decode a Git C-quoted path (`"foo\\tbar"` → `foo\tbar`). */
|
|
7
|
+
export declare function unescapeGitQuotedPath(raw: string): string;
|
|
8
|
+
/** Strip git `a/` `b/` prefixes and quotes from a `+++` path. */
|
|
9
|
+
export declare function stripGitDiffPath(raw: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Collect 1-indexed lines added on the new side of a unified diff (`-U0` ok).
|
|
12
|
+
* Deletions-only hunks contribute nothing. `/dev/null` destinations are skipped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseUnifiedDiffAddedLines(diffText: string): ChangedLineMap;
|
|
15
|
+
/** Count physical lines in file content (no trailing empty split artifact). */
|
|
16
|
+
export declare function countFileLines(content: string): number;
|
|
17
|
+
/** Treat every line of an untracked file as added. */
|
|
18
|
+
export declare function allLinesChanged(lineCount: number): Set<number>;
|
|
19
|
+
//# sourceMappingURL=diff-lines.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse unified diffs into added/modified new-file line numbers (#3514).
|
|
3
|
+
*/
|
|
4
|
+
const HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
|
|
5
|
+
/** Git `core.quotePath` C-style escapes (quote.c). */
|
|
6
|
+
const GIT_C_ESCAPES = {
|
|
7
|
+
a: "\u0007",
|
|
8
|
+
b: "\b",
|
|
9
|
+
t: "\t",
|
|
10
|
+
n: "\n",
|
|
11
|
+
v: "\v",
|
|
12
|
+
f: "\f",
|
|
13
|
+
r: "\r",
|
|
14
|
+
'"': '"',
|
|
15
|
+
"\\": "\\",
|
|
16
|
+
};
|
|
17
|
+
function isOctalDigit(ch) {
|
|
18
|
+
return ch !== undefined && ch >= "0" && ch <= "7";
|
|
19
|
+
}
|
|
20
|
+
/** Read one 1-3 digit octal byte at `index` (first digit). */
|
|
21
|
+
function readOctalByte(inner, index) {
|
|
22
|
+
if (!isOctalDigit(inner[index])) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
let oct = inner[index] ?? "";
|
|
26
|
+
let next = index + 1;
|
|
27
|
+
if (isOctalDigit(inner[next])) {
|
|
28
|
+
oct += inner[next] ?? "";
|
|
29
|
+
next += 1;
|
|
30
|
+
if (isOctalDigit(inner[next])) {
|
|
31
|
+
oct += inner[next] ?? "";
|
|
32
|
+
next += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { value: Number.parseInt(oct, 8), next };
|
|
36
|
+
}
|
|
37
|
+
/** Decode a Git C-quoted path (`"foo\\tbar"` → `foo\tbar`). */
|
|
38
|
+
export function unescapeGitQuotedPath(raw) {
|
|
39
|
+
const s = raw.trim();
|
|
40
|
+
if (!(s.startsWith('"') && s.endsWith('"') && s.length >= 2)) {
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
const inner = s.slice(1, -1);
|
|
44
|
+
let out = "";
|
|
45
|
+
const utf8 = new TextDecoder("utf-8");
|
|
46
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
47
|
+
const ch = inner[i];
|
|
48
|
+
if (ch !== "\\") {
|
|
49
|
+
out += ch;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const next = inner[i + 1];
|
|
53
|
+
if (next === undefined) {
|
|
54
|
+
out += "\\";
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
i += 1;
|
|
58
|
+
const simple = GIT_C_ESCAPES[next];
|
|
59
|
+
if (simple !== undefined) {
|
|
60
|
+
out += simple;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const first = readOctalByte(inner, i);
|
|
64
|
+
if (first !== null) {
|
|
65
|
+
const bytes = [first.value];
|
|
66
|
+
i = first.next;
|
|
67
|
+
while (inner[i] === "\\" && isOctalDigit(inner[i + 1])) {
|
|
68
|
+
const more = readOctalByte(inner, i + 1);
|
|
69
|
+
if (more === null) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
bytes.push(more.value);
|
|
73
|
+
i = more.next;
|
|
74
|
+
}
|
|
75
|
+
i -= 1;
|
|
76
|
+
out += utf8.decode(Uint8Array.from(bytes));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out += next;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/** Strip git `a/` `b/` prefixes and quotes from a `+++` path. */
|
|
84
|
+
export function stripGitDiffPath(raw) {
|
|
85
|
+
let s = unescapeGitQuotedPath(raw.trim());
|
|
86
|
+
if (s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
|
|
87
|
+
s = s.slice(1, -1);
|
|
88
|
+
}
|
|
89
|
+
const prefixed = /^[abiwc]\/(.*)$/.exec(s);
|
|
90
|
+
if (prefixed?.[1] !== undefined) {
|
|
91
|
+
return prefixed[1].replace(/\\/g, "/");
|
|
92
|
+
}
|
|
93
|
+
return s.replace(/\\/g, "/");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Collect 1-indexed lines added on the new side of a unified diff (`-U0` ok).
|
|
97
|
+
* Deletions-only hunks contribute nothing. `/dev/null` destinations are skipped.
|
|
98
|
+
*/
|
|
99
|
+
export function parseUnifiedDiffAddedLines(diffText) {
|
|
100
|
+
const result = new Map();
|
|
101
|
+
let current = null;
|
|
102
|
+
let newLine = 0;
|
|
103
|
+
let inHunk = false;
|
|
104
|
+
for (const raw of diffText.split("\n")) {
|
|
105
|
+
const line = raw.replace(/\r$/, "");
|
|
106
|
+
if (line.startsWith("diff --git ")) {
|
|
107
|
+
current = null;
|
|
108
|
+
inHunk = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (line.startsWith("+++ ")) {
|
|
112
|
+
const rest = line.slice(4).trim();
|
|
113
|
+
if (rest === "/dev/null") {
|
|
114
|
+
current = null;
|
|
115
|
+
inHunk = false;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
current = stripGitDiffPath(rest);
|
|
119
|
+
if (!result.has(current)) {
|
|
120
|
+
result.set(current, new Set());
|
|
121
|
+
}
|
|
122
|
+
inHunk = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const hunk = HUNK_RE.exec(line);
|
|
126
|
+
if (hunk) {
|
|
127
|
+
newLine = Number(hunk[1]);
|
|
128
|
+
inHunk = current !== null;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!inHunk || current === null) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (line.startsWith("\\")) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const lines = result.get(current);
|
|
138
|
+
if (lines === undefined) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (line.startsWith("+")) {
|
|
142
|
+
lines.add(newLine);
|
|
143
|
+
newLine += 1;
|
|
144
|
+
}
|
|
145
|
+
else if (line.startsWith("-")) {
|
|
146
|
+
// old-file deletion; new-file cursor stays
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
newLine += 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/** Count physical lines in file content (no trailing empty split artifact). */
|
|
155
|
+
export function countFileLines(content) {
|
|
156
|
+
if (content.length === 0) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
const parts = content.split(/\r\n|\n/);
|
|
160
|
+
if (parts[parts.length - 1] === "") {
|
|
161
|
+
parts.pop();
|
|
162
|
+
}
|
|
163
|
+
return parts.length;
|
|
164
|
+
}
|
|
165
|
+
/** Treat every line of an untracked file as added. */
|
|
166
|
+
export function allLinesChanged(lineCount) {
|
|
167
|
+
const lines = new Set();
|
|
168
|
+
for (let i = 1; i <= lineCount; i += 1) {
|
|
169
|
+
lines.add(i);
|
|
170
|
+
}
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=diff-lines.js.map
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* forward-coverage/evaluate.ts -- deterministic forward-coverage gate
|
|
2
|
+
* forward-coverage/evaluate.ts -- deterministic forward-coverage gate
|
|
3
|
+
* (#1310 / #3514).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Two halves:
|
|
6
|
+
* 1. Fail-closed new-file existence (#1310): each NEW source file
|
|
7
|
+
* (`*.py` / `*.go` / `*.ts` / `*.tsx`, excluding tests and `*.d.ts`)
|
|
8
|
+
* must ship a corresponding test file in the same diff.
|
|
9
|
+
* 2. Warn-first diff coverage (#3514): intersect `coverage-final.json`
|
|
10
|
+
* with added/modified lines and report uncovered changed branches.
|
|
11
|
+
* Default threshold is 90% of those branches. That 90% is per-change
|
|
12
|
+
* coverage of new code; the project vitest floor (75) is a collapse
|
|
13
|
+
* detector for the aggregate -- they are not interchangeable.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Three-state exit: 0 clean / 1 missing forward coverage / 2 config error.
|
|
15
|
+
* Missing coverage reports skip the diff half (existence still runs).
|
|
16
|
+
* Pass `enforceDiffCoverage` / `--enforce` to fail closed on uncovered
|
|
17
|
+
* changed branches. Three-state exit: 0 clean or warn / 1 missing
|
|
18
|
+
* existence (or enforced diff findings) / 2 config error.
|
|
13
19
|
*/
|
|
20
|
+
import { type DiffCoverageReport } from "./diff-coverage.js";
|
|
21
|
+
export { DEFAULT_DIFF_COVERAGE_THRESHOLD, type DiffCoverageReport, type UncoveredChangedBranch, } from "./diff-coverage.js";
|
|
14
22
|
/** Diff scope: `staged` = index vs HEAD; `head` = working tree + index vs HEAD. */
|
|
15
23
|
export type ForwardCoverageMode = "head" | "staged";
|
|
16
24
|
/** A new source file that has no corresponding test file in the same diff. */
|
|
@@ -25,10 +33,17 @@ export interface ForwardCoverageResult {
|
|
|
25
33
|
readonly exitCode: 0 | 1 | 2;
|
|
26
34
|
readonly missing: MissingCoverage[];
|
|
27
35
|
readonly message: string;
|
|
36
|
+
readonly diffCoverage: DiffCoverageReport | null;
|
|
28
37
|
}
|
|
29
38
|
export interface ForwardCoverageOptions {
|
|
30
39
|
readonly mode?: ForwardCoverageMode;
|
|
31
40
|
readonly allowListPath?: string | null;
|
|
41
|
+
/** Fail closed on uncovered changed branches. Default warn-only. */
|
|
42
|
+
readonly enforceDiffCoverage?: boolean;
|
|
43
|
+
/** Override coverage-final.json path. Default `<root>/coverage/coverage-final.json`. */
|
|
44
|
+
readonly coverageReportPath?: string | null;
|
|
45
|
+
/** Per-diff branch threshold. Default 90 -- not the 75 global floor. */
|
|
46
|
+
readonly diffThreshold?: number;
|
|
32
47
|
}
|
|
33
48
|
/**
|
|
34
49
|
* True when `relPath` is a test file (excluded from the "needs coverage" set
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* forward-coverage/evaluate.ts -- deterministic forward-coverage gate
|
|
2
|
+
* forward-coverage/evaluate.ts -- deterministic forward-coverage gate
|
|
3
|
+
* (#1310 / #3514).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Two halves:
|
|
6
|
+
* 1. Fail-closed new-file existence (#1310): each NEW source file
|
|
7
|
+
* (`*.py` / `*.go` / `*.ts` / `*.tsx`, excluding tests and `*.d.ts`)
|
|
8
|
+
* must ship a corresponding test file in the same diff.
|
|
9
|
+
* 2. Warn-first diff coverage (#3514): intersect `coverage-final.json`
|
|
10
|
+
* with added/modified lines and report uncovered changed branches.
|
|
11
|
+
* Default threshold is 90% of those branches. That 90% is per-change
|
|
12
|
+
* coverage of new code; the project vitest floor (75) is a collapse
|
|
13
|
+
* detector for the aggregate -- they are not interchangeable.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Three-state exit: 0 clean / 1 missing forward coverage / 2 config error.
|
|
15
|
+
* Missing coverage reports skip the diff half (existence still runs).
|
|
16
|
+
* Pass `enforceDiffCoverage` / `--enforce` to fail closed on uncovered
|
|
17
|
+
* changed branches. Three-state exit: 0 clean or warn / 1 missing
|
|
18
|
+
* existence (or enforced diff findings) / 2 config error.
|
|
13
19
|
*/
|
|
14
20
|
import { spawnSync } from "node:child_process";
|
|
15
21
|
import { readFileSync } from "node:fs";
|
|
16
|
-
import { basename } from "node:path";
|
|
22
|
+
import { basename, join } from "node:path";
|
|
17
23
|
import { GitCommandError, GitNotFoundError } from "../encoding/git.js";
|
|
18
24
|
import { fnmatchCase } from "../encoding/text.js";
|
|
25
|
+
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
26
|
+
import { evaluateDiffCoverage, hasDiffCoverageFindings, } from "./diff-coverage.js";
|
|
27
|
+
import { allLinesChanged, countFileLines, parseUnifiedDiffAddedLines, } from "./diff-lines.js";
|
|
28
|
+
export { DEFAULT_DIFF_COVERAGE_THRESHOLD, } from "./diff-coverage.js";
|
|
19
29
|
/** Source-file extensions in scope for v1. */
|
|
20
30
|
const SOURCE_EXTENSIONS = new Set([".py", ".go", ".ts", ".tsx"]);
|
|
21
31
|
/** Raised by `loadAllowList` when the path does not exist. */
|
|
@@ -117,6 +127,7 @@ function git(args, projectRoot) {
|
|
|
117
127
|
cwd: projectRoot,
|
|
118
128
|
encoding: "utf8",
|
|
119
129
|
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
120
131
|
});
|
|
121
132
|
if (result.error !== undefined) {
|
|
122
133
|
const e = result.error;
|
|
@@ -176,7 +187,83 @@ function addedFiles(projectRoot, mode) {
|
|
|
176
187
|
return [...added];
|
|
177
188
|
}
|
|
178
189
|
function configError(message) {
|
|
179
|
-
return { exitCode: 2, missing: [], message };
|
|
190
|
+
return { exitCode: 2, missing: [], message, diffCoverage: null };
|
|
191
|
+
}
|
|
192
|
+
function mergeChangedLines(into, from) {
|
|
193
|
+
for (const [path, lines] of from) {
|
|
194
|
+
const posix = path.replace(/\\/g, "/");
|
|
195
|
+
const set = into.get(posix) ?? new Set();
|
|
196
|
+
for (const n of lines) {
|
|
197
|
+
set.add(n);
|
|
198
|
+
}
|
|
199
|
+
into.set(posix, set);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Added, modified, and renamed new-file line numbers in the diff scope.
|
|
204
|
+
* Untracked files in head mode count as fully added. Rename-classified
|
|
205
|
+
* edits (`--diff-filter=AMR`) still contribute their added lines.
|
|
206
|
+
*/
|
|
207
|
+
function changedLinesByFile(projectRoot, mode) {
|
|
208
|
+
const maps = new Map();
|
|
209
|
+
if (mode === "staged") {
|
|
210
|
+
const diff = git(["diff", "--cached", "-U0", "--diff-filter=AMR", "--no-color"], projectRoot);
|
|
211
|
+
if (diff.status === 0) {
|
|
212
|
+
mergeChangedLines(maps, parseUnifiedDiffAddedLines(diff.stdout));
|
|
213
|
+
}
|
|
214
|
+
return maps;
|
|
215
|
+
}
|
|
216
|
+
const hasHead = git(["rev-parse", "--verify", "-q", "HEAD"], projectRoot).status === 0;
|
|
217
|
+
if (hasHead) {
|
|
218
|
+
const diff = git(["diff", "-U0", "--diff-filter=AMR", "--no-color", "HEAD"], projectRoot);
|
|
219
|
+
if (diff.status === 0) {
|
|
220
|
+
mergeChangedLines(maps, parseUnifiedDiffAddedLines(diff.stdout));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const rel of toLines(gitOrThrow(["ls-files", "--others", "--exclude-standard"], projectRoot))) {
|
|
224
|
+
const posix = rel.replace(/\\/g, "/");
|
|
225
|
+
let content;
|
|
226
|
+
try {
|
|
227
|
+
content = readFileSync(join(projectRoot, posix), "utf8");
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const set = maps.get(posix) ?? new Set();
|
|
233
|
+
for (const n of allLinesChanged(countFileLines(content))) {
|
|
234
|
+
set.add(n);
|
|
235
|
+
}
|
|
236
|
+
maps.set(posix, set);
|
|
237
|
+
}
|
|
238
|
+
return maps;
|
|
239
|
+
}
|
|
240
|
+
function formatDiffCoverage(report, enforce) {
|
|
241
|
+
if (!report.reportPresent) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
const findings = hasDiffCoverageFindings(report);
|
|
245
|
+
if (!findings && report.changedBranchTotal === 0) {
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
const pct = report.percent.toFixed(2);
|
|
249
|
+
const roles = ` The ${report.threshold}% per-diff threshold covers added/modified branches; the 75 global ` +
|
|
250
|
+
"floor is a collapse detector for the aggregate -- they are not interchangeable (#3514 / #3512).";
|
|
251
|
+
if (!findings) {
|
|
252
|
+
return (`verify_forward_coverage: diff coverage ${pct}% of ${report.changedBranchTotal} ` +
|
|
253
|
+
`changed branch(es) (threshold ${report.threshold}%).\n${roles}`);
|
|
254
|
+
}
|
|
255
|
+
const header = `verify_forward_coverage: ${report.uncovered.length} uncovered changed branch(es) ` +
|
|
256
|
+
`(diff ${pct}% vs ${report.threshold}% threshold).\n${roles}`;
|
|
257
|
+
const body = report.uncovered
|
|
258
|
+
.slice(0, 20)
|
|
259
|
+
.map((u) => ` ${u.path}:${u.line} (${u.type} #${u.branchId}[${u.pathIndex}])`)
|
|
260
|
+
.join("\n");
|
|
261
|
+
const more = report.uncovered.length > 20 ? `\n ... ${report.uncovered.length - 20} more` : "";
|
|
262
|
+
const posture = enforce
|
|
263
|
+
? "FAIL: --enforce is set; cover the changed branches or raise the tests (#3514)."
|
|
264
|
+
: "ADVISORY (warn-only): exit 0 for the diff half. Pass --enforce to fail closed (#3514). " +
|
|
265
|
+
"New-file existence remains fail-closed.";
|
|
266
|
+
return `${header}\n${body}${more}\n${posture}`;
|
|
180
267
|
}
|
|
181
268
|
/**
|
|
182
269
|
* Pure evaluation returning `{ exitCode, missing, message }`. Three-state exit
|
|
@@ -201,8 +288,10 @@ export function evaluateForwardCoverage(projectRoot, options = {}) {
|
|
|
201
288
|
" Recovery: check file permissions.");
|
|
202
289
|
}
|
|
203
290
|
let added;
|
|
291
|
+
let changedLines;
|
|
204
292
|
try {
|
|
205
293
|
added = addedFiles(projectRoot, mode);
|
|
294
|
+
changedLines = changedLinesByFile(projectRoot, mode);
|
|
206
295
|
}
|
|
207
296
|
catch (err) {
|
|
208
297
|
if (err instanceof GitNotFoundError) {
|
|
@@ -233,6 +322,22 @@ export function evaluateForwardCoverage(projectRoot, options = {}) {
|
|
|
233
322
|
missing.push({ path: rel, expectedTests: expected });
|
|
234
323
|
}
|
|
235
324
|
}
|
|
325
|
+
const sourceChanged = new Map();
|
|
326
|
+
for (const [rel, lines] of changedLines) {
|
|
327
|
+
if (!isSourceFile(rel) || isAllowListed(rel, allowGlobs)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
sourceChanged.set(rel, lines);
|
|
331
|
+
}
|
|
332
|
+
const coverageReportPath = options.coverageReportPath ?? join(projectRoot, "coverage", "coverage-final.json");
|
|
333
|
+
const enforce = options.enforceDiffCoverage === true;
|
|
334
|
+
const diffCoverage = evaluateDiffCoverage({
|
|
335
|
+
projectRoot,
|
|
336
|
+
coverageReportPath,
|
|
337
|
+
changedLinesByFile: sourceChanged,
|
|
338
|
+
threshold: options.diffThreshold,
|
|
339
|
+
});
|
|
340
|
+
const diffSection = formatDiffCoverage(diffCoverage, enforce);
|
|
236
341
|
if (missing.length > 0) {
|
|
237
342
|
const header = `verify_forward_coverage: ${missing.length} new source file(s) added without a ` +
|
|
238
343
|
"corresponding test in the same diff (#1310).\n" +
|
|
@@ -242,13 +347,14 @@ export function evaluateForwardCoverage(projectRoot, options = {}) {
|
|
|
242
347
|
const body = missing
|
|
243
348
|
.map((m) => ` ${m.path}\n expected one of: ${m.expectedTests.join(", ")}`)
|
|
244
349
|
.join("\n");
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
350
|
+
const message = diffSection === "" ? `${header}\n${body}` : `${header}\n${body}\n${diffSection}`;
|
|
351
|
+
return { exitCode: 1, missing, message, diffCoverage };
|
|
352
|
+
}
|
|
353
|
+
const existence = `verify_forward_coverage: ${checked} new source file(s) checked -- ` +
|
|
354
|
+
"all have forward coverage in the diff (#1310).";
|
|
355
|
+
const findings = hasDiffCoverageFindings(diffCoverage);
|
|
356
|
+
const exitCode = enforce && findings ? 1 : 0;
|
|
357
|
+
const message = diffSection === "" ? existence : `${existence}\n${diffSection}`;
|
|
358
|
+
return { exitCode, missing, message, diffCoverage };
|
|
253
359
|
}
|
|
254
360
|
//# sourceMappingURL=evaluate.js.map
|
|
@@ -16,7 +16,7 @@ export declare const CURSOR_SESSION_HOOK_TIMEOUT_SECONDS = 5;
|
|
|
16
16
|
* Mutation tool.before runs `inspectMutationGates` → gated
|
|
17
17
|
* `verifySessionRitual`, which re-runs non-cacheable agent-hook readiness on
|
|
18
18
|
* every boundary. Live readiness alone has a multi-host fixture ceiling of
|
|
19
|
-
* ~
|
|
19
|
+
* ~24s (`content/contracts/agent-hook-readiness.md`); the historical deposit
|
|
20
20
|
* default of 5s was below that budget. Under Cursor `failClosed: true`, a
|
|
21
21
|
* host timeout kill surfaces as opaque exit-1 with no Directive decision
|
|
22
22
|
* code (#3246 / related #2864). Keep this above the readiness ceiling plus
|
|
@@ -24,7 +24,7 @@ export const CURSOR_SESSION_HOOK_TIMEOUT_SECONDS = 5;
|
|
|
24
24
|
* Mutation tool.before runs `inspectMutationGates` → gated
|
|
25
25
|
* `verifySessionRitual`, which re-runs non-cacheable agent-hook readiness on
|
|
26
26
|
* every boundary. Live readiness alone has a multi-host fixture ceiling of
|
|
27
|
-
* ~
|
|
27
|
+
* ~24s (`content/contracts/agent-hook-readiness.md`); the historical deposit
|
|
28
28
|
* default of 5s was below that budget. Under Cursor `failClosed: true`, a
|
|
29
29
|
* host timeout kill surfaces as opaque exit-1 with no Directive decision
|
|
30
30
|
* code (#3246 / related #2864). Keep this above the readiness ceiling plus
|
|
@@ -23,11 +23,15 @@ export interface ClauseStampPreparation {
|
|
|
23
23
|
readonly ambiguity_attestation?: "none_found";
|
|
24
24
|
readonly remediation?: string;
|
|
25
25
|
}
|
|
26
|
+
export declare const MISSING_AMBIGUITY_ATTESTATION_CAUSE: "missing_ambiguity_attestation";
|
|
27
|
+
export declare const AMBIGUITY_ATTESTATION_REMEDIATION = "record ambiguity readings or attest none_found";
|
|
26
28
|
export interface AmbiguityAttestationCheck {
|
|
27
29
|
readonly ok: boolean;
|
|
28
30
|
readonly attested: boolean;
|
|
29
31
|
readonly kind: "ambiguous-clause" | "none_found" | "missing";
|
|
30
32
|
readonly message: string;
|
|
33
|
+
readonly cause?: typeof MISSING_AMBIGUITY_ATTESTATION_CAUSE;
|
|
34
|
+
readonly remediation?: string;
|
|
31
35
|
}
|
|
32
36
|
/** True when activate/promote must run #3323 (absent, empty none_stated, or command-only). */
|
|
33
37
|
export declare function needsClauseDerivation(acceptance: unknown): boolean;
|
|
@@ -45,6 +49,11 @@ export declare function countClauseProvenance(clauses: readonly TracedAcceptance
|
|
|
45
49
|
export declare function prepareClauseStamp(clauses: readonly AcceptanceClause[], statement: string): ClauseStampPreparation;
|
|
46
50
|
/** Config-error surface for verify:ac: attestation or an ambiguous clause with readings. */
|
|
47
51
|
export declare function evaluateAmbiguityAttestation(acceptance: unknown): AmbiguityAttestationCheck;
|
|
52
|
+
/**
|
|
53
|
+
* Fail-closed check for a *stamped* plan.acceptance (#3559).
|
|
54
|
+
* Command-only / empty floor blocks are not stamps; they keep the empty-resolution path.
|
|
55
|
+
*/
|
|
56
|
+
export declare function stampedAmbiguityAttestationError(acceptance: unknown): AmbiguityAttestationCheck | null;
|
|
48
57
|
/**
|
|
49
58
|
* Annotate stamped clauses with provenance and an ambiguity attestation.
|
|
50
59
|
* An all-implementation stamp is stripped (never sufficient alone).
|