@deftai/directive-core 0.104.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.
Files changed (287) hide show
  1. package/dist/authz/classify.js +1108 -67
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +34 -4
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  17. package/dist/forward-coverage/diff-coverage.js +112 -0
  18. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  19. package/dist/forward-coverage/diff-lines.js +173 -0
  20. package/dist/forward-coverage/evaluate.d.ts +24 -9
  21. package/dist/forward-coverage/evaluate.js +125 -19
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +241 -5
  24. package/dist/fs/mutation-ledger.d.ts +54 -0
  25. package/dist/fs/mutation-ledger.js +133 -0
  26. package/dist/fs/non-product-dirs.d.ts +79 -0
  27. package/dist/fs/non-product-dirs.js +96 -0
  28. package/dist/hooks/dispatcher.d.ts +3 -1
  29. package/dist/hooks/dispatcher.js +14 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +2 -0
  32. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  33. package/dist/init-deposit/agent-hooks.js +18 -16
  34. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  35. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  36. package/dist/init-deposit/gitignore.js +9 -0
  37. package/dist/init-deposit/hygiene.d.ts +47 -11
  38. package/dist/init-deposit/hygiene.js +172 -37
  39. package/dist/init-deposit/legacy-detect.js +6 -2
  40. package/dist/init-deposit/refresh.d.ts +21 -1
  41. package/dist/init-deposit/refresh.js +106 -9
  42. package/dist/init-deposit/scaffold.js +28 -279
  43. package/dist/init-deposit/slash-deposit.js +13 -7
  44. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  45. package/dist/init-deposit/xbrief-projections.js +24 -11
  46. package/dist/intake/clause-derivation.d.ts +47 -1
  47. package/dist/intake/clause-derivation.js +238 -9
  48. package/dist/intake/issue-ingest.d.ts +3 -1
  49. package/dist/intake/issue-ingest.js +35 -6
  50. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  51. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  52. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  53. package/dist/lifecycle-visible/evaluate.js +710 -0
  54. package/dist/lifecycle-visible/index.d.ts +2 -0
  55. package/dist/lifecycle-visible/index.js +2 -0
  56. package/dist/literal-acceptance/capture.d.ts +10 -0
  57. package/dist/literal-acceptance/capture.js +226 -77
  58. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  59. package/dist/literal-acceptance/evaluate.js +70 -2
  60. package/dist/literal-acceptance/index.d.ts +3 -3
  61. package/dist/literal-acceptance/index.js +3 -3
  62. package/dist/literal-acceptance/safety.d.ts +41 -0
  63. package/dist/literal-acceptance/safety.js +126 -2
  64. package/dist/literal-acceptance/types.d.ts +8 -0
  65. package/dist/orchestration/judgment-policy.d.ts +13 -0
  66. package/dist/orchestration/judgment-policy.js +15 -0
  67. package/dist/orchestration/probe-session.js +13 -8
  68. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  69. package/dist/orchestration/verify-judgment-gates.js +43 -11
  70. package/dist/orphan-active/evaluate.d.ts +7 -1
  71. package/dist/orphan-active/evaluate.js +102 -23
  72. package/dist/platform/platform-capabilities.js +2 -3
  73. package/dist/policy/base-branch.d.ts +37 -0
  74. package/dist/policy/base-branch.js +114 -0
  75. package/dist/policy/branch-sync.d.ts +80 -0
  76. package/dist/policy/branch-sync.js +211 -0
  77. package/dist/policy/ceremony-dial-escalation.js +1 -0
  78. package/dist/policy/ceremony-dial.js +11 -21
  79. package/dist/policy/delivery-branch.d.ts +5 -3
  80. package/dist/policy/delivery-branch.js +7 -3
  81. package/dist/policy/file-size-thresholds.d.ts +24 -0
  82. package/dist/policy/file-size-thresholds.js +23 -0
  83. package/dist/policy/forge-outage-retry.d.ts +47 -0
  84. package/dist/policy/forge-outage-retry.js +157 -0
  85. package/dist/policy/host-hooks.d.ts +20 -0
  86. package/dist/policy/host-hooks.js +121 -2
  87. package/dist/policy/index.d.ts +7 -0
  88. package/dist/policy/index.js +66 -7
  89. package/dist/policy/org-force-on-migration.js +2 -1
  90. package/dist/policy/plan-extensions.d.ts +1 -1
  91. package/dist/policy/plan-extensions.js +2 -0
  92. package/dist/policy/product-signal.js +6 -7
  93. package/dist/policy/project-invariants.d.ts +50 -0
  94. package/dist/policy/project-invariants.js +205 -0
  95. package/dist/policy/require-human-merge.js +8 -6
  96. package/dist/policy/resolve.d.ts +7 -2
  97. package/dist/policy/resolve.js +25 -8
  98. package/dist/policy/sync-default.d.ts +129 -0
  99. package/dist/policy/sync-default.js +452 -0
  100. package/dist/policy/sync-max-files.d.ts +93 -0
  101. package/dist/policy/sync-max-files.js +192 -0
  102. package/dist/policy/value-feedback.js +7 -8
  103. package/dist/pr-monitor/absent-required.d.ts +11 -0
  104. package/dist/pr-monitor/absent-required.js +61 -0
  105. package/dist/pr-monitor/constants.d.ts +7 -0
  106. package/dist/pr-monitor/constants.js +7 -0
  107. package/dist/pr-monitor/index.d.ts +2 -1
  108. package/dist/pr-monitor/index.js +2 -1
  109. package/dist/pr-monitor/main.js +4 -0
  110. package/dist/pr-monitor/monitor.js +24 -1
  111. package/dist/pr-wait-mergeable/classify.js +6 -0
  112. package/dist/preflight/evaluate.d.ts +4 -0
  113. package/dist/preflight/evaluate.js +35 -1
  114. package/dist/preflight/index.d.ts +4 -0
  115. package/dist/preflight/index.js +2 -0
  116. package/dist/preflight/intended-placement.d.ts +62 -0
  117. package/dist/preflight/intended-placement.js +275 -0
  118. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  119. package/dist/preflight/project-invariants-gate.js +82 -0
  120. package/dist/preflight-cache/evaluate.d.ts +12 -0
  121. package/dist/preflight-cache/evaluate.js +15 -1
  122. package/dist/preflight-cache/index.d.ts +1 -1
  123. package/dist/preflight-cache/index.js +1 -1
  124. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  125. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  126. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  127. package/dist/product-first-done-gate/acceptance.js +79 -8
  128. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  129. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  130. package/dist/product-first-done-gate/evaluate.d.ts +16 -0
  131. package/dist/product-first-done-gate/evaluate.js +298 -34
  132. package/dist/product-first-done-gate/index.d.ts +2 -0
  133. package/dist/product-first-done-gate/index.js +1 -0
  134. package/dist/release/build-dist.d.ts +101 -0
  135. package/dist/release/build-dist.js +428 -46
  136. package/dist/release/closed-verb-gate.d.ts +12 -0
  137. package/dist/release/closed-verb-gate.js +51 -0
  138. package/dist/release/index.d.ts +1 -0
  139. package/dist/release/index.js +1 -0
  140. package/dist/release/pipeline.js +7 -0
  141. package/dist/release/types.d.ts +12 -0
  142. package/dist/render/constants.d.ts +2 -1
  143. package/dist/render/constants.js +2 -1
  144. package/dist/render/framework-commands.js +4 -0
  145. package/dist/render/spec-validate.js +23 -12
  146. package/dist/run-summary/emit.d.ts +16 -10
  147. package/dist/run-summary/emit.js +26 -15
  148. package/dist/run-summary/index.d.ts +3 -2
  149. package/dist/run-summary/index.js +2 -1
  150. package/dist/run-summary/session-id.d.ts +21 -0
  151. package/dist/run-summary/session-id.js +92 -0
  152. package/dist/run-summary/types.d.ts +33 -4
  153. package/dist/run-summary/types.js +1 -0
  154. package/dist/scope/acceptance-evidence.d.ts +19 -7
  155. package/dist/scope/acceptance-evidence.js +38 -11
  156. package/dist/scope/delivery-evidence.d.ts +3 -3
  157. package/dist/scope/delivery-evidence.js +22 -27
  158. package/dist/scope/index.d.ts +1 -0
  159. package/dist/scope/index.js +1 -0
  160. package/dist/scope/main.js +15 -5
  161. package/dist/scope/parent-lineage.js +7 -6
  162. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  163. package/dist/scope/project-invariant-coverage.js +322 -0
  164. package/dist/scope/promote-from-issue.js +5 -5
  165. package/dist/scope/transition.js +1 -1
  166. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  167. package/dist/scope-provenance/compare-intent.js +233 -0
  168. package/dist/scope-provenance/digest.d.ts +18 -1
  169. package/dist/scope-provenance/digest.js +16 -11
  170. package/dist/scope-provenance/evaluate.d.ts +5 -1
  171. package/dist/scope-provenance/evaluate.js +67 -7
  172. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  173. package/dist/scope-provenance/extract-intent.js +262 -0
  174. package/dist/scope-provenance/index.d.ts +7 -1
  175. package/dist/scope-provenance/index.js +7 -1
  176. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  177. package/dist/scope-provenance/intent-digest.js +33 -0
  178. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  179. package/dist/scope-provenance/intent-evaluate.js +149 -0
  180. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  181. package/dist/scope-provenance/json-tokenizer.js +225 -0
  182. package/dist/scope-provenance/known-machine.d.ts +38 -0
  183. package/dist/scope-provenance/known-machine.js +157 -0
  184. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  185. package/dist/scope-provenance/mint-artifacts.js +425 -0
  186. package/dist/session/ac-pass-banking.d.ts +9 -36
  187. package/dist/session/ac-pass-banking.js +33 -79
  188. package/dist/session/ac-pass-reuse.d.ts +53 -0
  189. package/dist/session/ac-pass-reuse.js +95 -0
  190. package/dist/session/git.js +19 -4
  191. package/dist/session/index.d.ts +4 -0
  192. package/dist/session/index.js +4 -0
  193. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  194. package/dist/session/occupancy-stress-worker.js +36 -0
  195. package/dist/session/occupancy.d.ts +97 -0
  196. package/dist/session/occupancy.js +422 -0
  197. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  198. package/dist/session/process-cost.d.ts +32 -10
  199. package/dist/session/process-cost.js +189 -1
  200. package/dist/session/product-state-hash.d.ts +26 -0
  201. package/dist/session/product-state-hash.js +700 -0
  202. package/dist/session/ritual-entrypoint.js +16 -1
  203. package/dist/session/session-ready.d.ts +2 -0
  204. package/dist/session/session-ready.js +52 -30
  205. package/dist/session/session-start.d.ts +13 -0
  206. package/dist/session/session-start.js +103 -9
  207. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  208. package/dist/session/verify-ac-session-cache.js +126 -0
  209. package/dist/session/verify-session-ritual.d.ts +28 -0
  210. package/dist/session/verify-session-ritual.js +97 -5
  211. package/dist/slash/openclaw-deposit.js +13 -9
  212. package/dist/slice/constants.d.ts +2 -2
  213. package/dist/slice/constants.js +2 -2
  214. package/dist/slice/existing.js +9 -3
  215. package/dist/slice/lock.d.ts +16 -1
  216. package/dist/slice/lock.js +137 -11
  217. package/dist/story-ready/evaluate.d.ts +2 -0
  218. package/dist/story-ready/evaluate.js +13 -0
  219. package/dist/swarm/complete-cohort.d.ts +3 -0
  220. package/dist/swarm/complete-cohort.js +59 -11
  221. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  222. package/dist/swarm/finalize-cohort-cli.js +165 -35
  223. package/dist/swarm/finalize-cohort.d.ts +1 -0
  224. package/dist/swarm/finalize-cohort.js +47 -4
  225. package/dist/swarm/launch.d.ts +22 -0
  226. package/dist/swarm/launch.js +155 -0
  227. package/dist/swarm/routing-set-cli.d.ts +11 -1
  228. package/dist/swarm/routing-set-cli.js +15 -3
  229. package/dist/swarm/routing-verify.d.ts +13 -1
  230. package/dist/swarm/routing-verify.js +60 -21
  231. package/dist/swarm/routing.d.ts +14 -0
  232. package/dist/swarm/routing.js +53 -3
  233. package/dist/telemetry-coverage/fake-trial.js +10 -0
  234. package/dist/telemetry-coverage/kinds.js +2 -0
  235. package/dist/triage/actions/index.js +4 -1
  236. package/dist/triage/bootstrap/index.js +6 -6
  237. package/dist/triage/help/registry-data.d.ts +1 -1
  238. package/dist/triage/help/registry-data.js +1 -1
  239. package/dist/triage/welcome/writers.d.ts +1 -1
  240. package/dist/triage/welcome/writers.js +10 -5
  241. package/dist/ts-check-lane/index.d.ts +3 -0
  242. package/dist/ts-check-lane/index.js +2 -0
  243. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  244. package/dist/ts-check-lane/progress-reporter.js +38 -0
  245. package/dist/ts-check-lane/progress.d.ts +32 -0
  246. package/dist/ts-check-lane/progress.js +57 -0
  247. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  248. package/dist/ts-check-lane/run-lane.js +8 -6
  249. package/dist/validate-content/validate-links.d.ts +8 -0
  250. package/dist/validate-content/validate-links.js +9 -15
  251. package/dist/value/feedback-file.js +12 -2
  252. package/dist/value/readback.d.ts +4 -1
  253. package/dist/value/readback.js +51 -12
  254. package/dist/vbrief-build/index.d.ts +1 -0
  255. package/dist/vbrief-build/index.js +1 -0
  256. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  257. package/dist/vbrief-build/project-invariants-io.js +41 -0
  258. package/dist/vbrief-reconcile/graph.js +18 -1
  259. package/dist/vbrief-reconcile/index.d.ts +1 -1
  260. package/dist/vbrief-reconcile/index.js +1 -1
  261. package/dist/vbrief-reconcile/types.d.ts +13 -0
  262. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  263. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  264. package/dist/vbrief-validate/index.d.ts +1 -1
  265. package/dist/vbrief-validate/index.js +1 -1
  266. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  267. package/dist/vbrief-validate/plan-hooks.js +68 -0
  268. package/dist/vbrief-validate/schema.d.ts +4 -0
  269. package/dist/vbrief-validate/schema.js +1 -1
  270. package/dist/verify-ac/evaluate.d.ts +7 -1
  271. package/dist/verify-ac/evaluate.js +34 -26
  272. package/dist/verify-ac/flag.d.ts +4 -0
  273. package/dist/verify-ac/flag.js +16 -0
  274. package/dist/verify-ac/index.d.ts +2 -2
  275. package/dist/verify-ac/index.js +2 -2
  276. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  277. package/dist/verify-env/agent-hook-readiness.js +12 -8
  278. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  279. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  280. package/dist/verify-env/agent-hooks.js +3 -4
  281. package/dist/verify-source/contained-writes.js +2 -0
  282. package/dist/verify-source/verify-stubs.d.ts +15 -1
  283. package/dist/verify-source/verify-stubs.js +20 -16
  284. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  285. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  286. package/dist/xbrief-migrate/migrate-project.js +2 -5
  287. package/package.json +7 -3
@@ -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 (#1310).
2
+ * forward-coverage/evaluate.ts -- deterministic forward-coverage gate
3
+ * (#1310 / #3514).
3
4
  *
4
- * Migrates the prose-tier "new source files MUST include corresponding test
5
- * files in the same PR" rule (AGENTS.md) to deterministic enforcement,
6
- * mirroring `verify:encoding` (#798) and `verify:branch` (#747).
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
- * Behaviour (v1 = NEW-file granularity only, to keep false positives near
9
- * zero): read the staged (`--staged`) or HEAD-relative diff; for each NEW
10
- * source file (`*.py` / `*.go` / `*.ts` / `*.tsx`, excluding test files and
11
- * `*.d.ts`), assert a corresponding test file was added in the SAME diff.
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 (#1310).
2
+ * forward-coverage/evaluate.ts -- deterministic forward-coverage gate
3
+ * (#1310 / #3514).
3
4
  *
4
- * Migrates the prose-tier "new source files MUST include corresponding test
5
- * files in the same PR" rule (AGENTS.md) to deterministic enforcement,
6
- * mirroring `verify:encoding` (#798) and `verify:branch` (#747).
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
- * Behaviour (v1 = NEW-file granularity only, to keep false positives near
9
- * zero): read the staged (`--staged`) or HEAD-relative diff; for each NEW
10
- * source file (`*.py` / `*.go` / `*.ts` / `*.tsx`, excluding test files and
11
- * `*.d.ts`), assert a corresponding test file was added in the SAME diff.
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
- return { exitCode: 1, missing, message: `${header}\n${body}` };
246
- }
247
- return {
248
- exitCode: 0,
249
- missing,
250
- message: `verify_forward_coverage: ${checked} new source file(s) checked -- ` +
251
- "all have forward coverage in the diff (#1310).",
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