@deftai/directive-core 0.105.0 → 0.107.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/gate-lists.js +2 -0
- package/dist/check/named-cause.js +1 -1
- package/dist/design-critique/exclusive-chip.d.ts +33 -0
- package/dist/design-critique/exclusive-chip.js +68 -0
- package/dist/design-critique/parent-audit.d.ts +59 -0
- package/dist/design-critique/parent-audit.js +121 -0
- package/dist/doctor/checks.d.ts +6 -0
- package/dist/doctor/checks.js +35 -0
- 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/hooks/dest-form.d.ts +46 -0
- package/dist/hooks/dest-form.js +575 -0
- package/dist/hooks/dispatcher.d.ts +17 -1
- package/dist/hooks/dispatcher.js +82 -11
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- 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/intake/github-auth-modes-cli.js +14 -2
- package/dist/intake/github-auth-modes.d.ts +54 -9
- package/dist/intake/github-auth-modes.js +326 -123
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
- package/dist/lifecycle/completed-write-guard.d.ts +48 -0
- package/dist/lifecycle/completed-write-guard.js +373 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- 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.d.ts +16 -0
- package/dist/literal-acceptance/capture.js +78 -10
- package/dist/literal-acceptance/evaluate.js +25 -3
- package/dist/literal-acceptance/index.d.ts +2 -2
- package/dist/literal-acceptance/index.js +2 -2
- package/dist/literal-acceptance/run.js +9 -3
- package/dist/literal-acceptance/types.d.ts +6 -0
- package/dist/literal-acceptance/types.js +6 -0
- 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/runtime-authority.d.ts +18 -0
- package/dist/policy/runtime-authority.js +11 -0
- package/dist/policy/value-feedback.js +7 -8
- package/dist/policy/write-fence.js +3 -0
- package/dist/preflight-cache/evaluate.d.ts +12 -0
- package/dist/preflight-cache/evaluate.js +24 -4
- 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 +82 -10
- 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/scm/design-critique-chip.d.ts +37 -0
- package/dist/scm/design-critique-chip.js +171 -0
- package/dist/scm/index.d.ts +1 -0
- package/dist/scm/index.js +1 -0
- package/dist/scm/main.d.ts +3 -0
- package/dist/scm/main.js +15 -1
- package/dist/scm/readiness-cli.d.ts +2 -0
- package/dist/scm/readiness-cli.js +42 -1
- package/dist/scm/readiness.d.ts +2 -1
- package/dist/scm/readiness.js +7 -1
- package/dist/scope/decompose.js +2 -1
- package/dist/scope/delivery-evidence.d.ts +27 -1
- package/dist/scope/index.d.ts +1 -0
- package/dist/scope/index.js +1 -0
- package/dist/scope/lifecycle-write.d.ts +24 -0
- package/dist/scope/lifecycle-write.js +64 -0
- package/dist/scope/main.js +18 -6
- package/dist/scope/promote-from-issue.js +5 -5
- package/dist/scope/transition.js +90 -11
- package/dist/scope-provenance/evaluate.js +8 -4
- package/dist/session/ac-pass-banking.d.ts +5 -0
- package/dist/session/ac-pass-banking.js +3 -2
- package/dist/session/git.d.ts +18 -0
- package/dist/session/git.js +81 -1
- package/dist/session/occupancy.d.ts +2 -1
- package/dist/session/occupancy.js +11 -4
- 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 +18 -0
- package/dist/session/session-start.js +49 -2
- package/dist/session/verify-session-ritual.d.ts +46 -4
- package/dist/session/verify-session-ritual.js +118 -10
- 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/launch.d.ts +69 -0
- package/dist/swarm/launch.js +191 -1
- 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/actions/index.d.ts +4 -0
- package/dist/triage/actions/index.js +6 -0
- package/dist/triage/bootstrap/index.js +6 -6
- package/dist/triage/evaluate/evaluate.d.ts +11 -0
- package/dist/triage/evaluate/evaluate.js +169 -0
- package/dist/triage/evaluate/github.d.ts +16 -0
- package/dist/triage/evaluate/github.js +153 -0
- package/dist/triage/evaluate/index.d.ts +9 -0
- package/dist/triage/evaluate/index.js +8 -0
- package/dist/triage/evaluate/paths.d.ts +7 -0
- package/dist/triage/evaluate/paths.js +25 -0
- package/dist/triage/evaluate/sink.d.ts +5 -0
- package/dist/triage/evaluate/sink.js +52 -0
- package/dist/triage/evaluate/types.d.ts +93 -0
- package/dist/triage/evaluate/types.js +13 -0
- package/dist/triage/evaluate/validity.d.ts +8 -0
- package/dist/triage/evaluate/validity.js +87 -0
- package/dist/triage/evaluate/value.d.ts +12 -0
- package/dist/triage/evaluate/value.js +33 -0
- package/dist/triage/evaluate/wip-census.d.ts +5 -0
- package/dist/triage/evaluate/wip-census.js +39 -0
- package/dist/triage/evaluate/worktrees.d.ts +8 -0
- package/dist/triage/evaluate/worktrees.js +68 -0
- package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
- package/dist/triage/evaluate/xbrief-refs.js +60 -0
- package/dist/triage/help/registry-data.d.ts +22 -8
- package/dist/triage/help/registry-data.js +54 -6
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/welcome/writers.d.ts +1 -1
- package/dist/triage/welcome/writers.js +10 -5
- package/dist/value/feedback-file.d.ts +17 -2
- package/dist/value/feedback-file.js +80 -6
- package/dist/value/readback.d.ts +4 -1
- package/dist/value/readback.js +51 -12
- package/dist/vbrief-reconcile/labels.d.ts +1 -0
- package/dist/vbrief-reconcile/labels.js +30 -0
- package/dist/vbrief-validate/schema.d.ts +4 -0
- package/dist/vbrief-validate/schema.js +1 -1
- package/dist/vbrief-validation/story-quality.d.ts +7 -0
- package/dist/vbrief-validation/story-quality.js +8 -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/dist/xbrief-migrate/agents-header.js +69 -7
- package/package.json +7 -3
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product dest-form classifier for in-repo Shell mutations (#3438).
|
|
3
|
+
*
|
|
4
|
+
* NEW harvest — do not reuse `classifyShellAuthzOps` / `listShellOps` as-is.
|
|
5
|
+
* Those return [] for `git checkout --` / `rm` of ordinary product paths.
|
|
6
|
+
*
|
|
7
|
+
* ! Resolves a target for ONE shape only: a single simple command. Everything
|
|
8
|
+
* else is recognized and fail-closed. Reconstructing a target from shell state
|
|
9
|
+
* was implemented and withdrawn — cwd depends on operator precedence, on exit
|
|
10
|
+
* status, on subshell boundaries, and on git config, and every resolution rule
|
|
11
|
+
* added produced its own fence bypass. Do not add it back; widen the recognized
|
|
12
|
+
* *forms* instead, and see `content/contracts/path-write-fence.md`.
|
|
13
|
+
*
|
|
14
|
+
* Residual known-open (recognition, not resolution): `python -c`,
|
|
15
|
+
* `cmd /c copy`, obfuscated `bash -c`.
|
|
16
|
+
*
|
|
17
|
+
* Commit-time active-scope is out of this slice (#3438), and ⊗ do not weaken
|
|
18
|
+
* Edit to match Shell. Keep this sentence on ONE line: acceptance clause 4 is
|
|
19
|
+
* verified by a literal token search, so wrapping it silently fails the gate.
|
|
20
|
+
*/
|
|
21
|
+
export type ProductDestFormKind = "git-checkout" | "git-restore" | "rm" | "rmdir";
|
|
22
|
+
export interface ProductDestForm {
|
|
23
|
+
readonly kind: ProductDestFormKind;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
/**
|
|
26
|
+
* True when the target is not provable — a glob/variable or leading `~` dest,
|
|
27
|
+
* a git context option, or any command that is not a single simple one. The
|
|
28
|
+
* dispatcher denies these outright; `path` is then only for the message, and
|
|
29
|
+
* is `SHELL_DEST_EXPANSION_SENTINEL` when no token can be named.
|
|
30
|
+
*/
|
|
31
|
+
readonly expansion?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Injected write target for expansion dests so the fence cannot match a literal glob. */
|
|
34
|
+
export declare const SHELL_DEST_EXPANSION_SENTINEL = "__shell_dest_expansion__";
|
|
35
|
+
/**
|
|
36
|
+
* Inject a dest path so `inspectMutationGates` sees the same write target as Edit/Write.
|
|
37
|
+
* Preserves original payload fields (command, posture markers).
|
|
38
|
+
*/
|
|
39
|
+
export declare function payloadWithInjectedWriteTarget(payload: unknown, destPath: string): Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Classify recognized product dest-forms in a shell command (#3438 v1 list).
|
|
42
|
+
* Empty when the command is not `git checkout -- <paths>`, `git restore`, or `rm`/`rmdir`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function destFormHasExpansion(path: string): boolean;
|
|
45
|
+
export declare function classifyProductDestForms(command: string): ProductDestForm[];
|
|
46
|
+
//# sourceMappingURL=dest-form.d.ts.map
|