@deftai/directive-runtime 0.129.6 → 0.129.8

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 (149) hide show
  1. package/Taskfile.yml +103 -16
  2. package/commands.md +8 -3
  3. package/dist/session-start.d.ts +8 -2
  4. package/dist/session-start.js +33 -12
  5. package/dist/vbrief-preflight.d.ts +4 -0
  6. package/dist/vbrief-preflight.js +34 -0
  7. package/dist/verify-scope-provenance.d.ts +13 -1
  8. package/dist/verify-scope-provenance.js +44 -1
  9. package/docs/getting-started.md +2 -0
  10. package/docs/scope-provenance.md +15 -1
  11. package/package.json +1 -1
  12. package/packs/skills/skills-pack-0.1.json +1 -1
  13. package/scm/github.md +5 -0
  14. package/skills/deft-directive-review-cycle/SKILL.md +15 -6
  15. package/skills/deft-directive-swarm/references/host-claude-code.md +11 -1
  16. package/skills/deft-directive-swarm/references/host-grok-build.md +3 -2
  17. package/vendor/directive-content/Taskfile.yml +103 -16
  18. package/vendor/directive-content/commands.md +8 -3
  19. package/vendor/directive-content/docs/getting-started.md +2 -0
  20. package/vendor/directive-content/docs/scope-provenance.md +15 -1
  21. package/vendor/directive-content/packs/skills/skills-pack-0.1.json +1 -1
  22. package/vendor/directive-content/scm/github.md +5 -0
  23. package/vendor/directive-content/skills/deft-directive-review-cycle/SKILL.md +15 -6
  24. package/vendor/directive-content/skills/deft-directive-swarm/references/host-claude-code.md +11 -1
  25. package/vendor/directive-content/skills/deft-directive-swarm/references/host-grok-build.md +3 -2
  26. package/vendor/directive-core/dist/.tsbuildinfo +1 -1
  27. package/vendor/directive-core/dist/check/context.d.ts.map +1 -1
  28. package/vendor/directive-core/dist/check/context.js +3 -31
  29. package/vendor/directive-core/dist/check/context.js.map +1 -1
  30. package/vendor/directive-core/dist/check/gate-lists.d.ts.map +1 -1
  31. package/vendor/directive-core/dist/check/gate-lists.js +4 -32
  32. package/vendor/directive-core/dist/check/gate-lists.js.map +1 -1
  33. package/vendor/directive-core/dist/check/walk-test-files.d.ts +3 -0
  34. package/vendor/directive-core/dist/check/walk-test-files.d.ts.map +1 -0
  35. package/vendor/directive-core/dist/check/walk-test-files.js +67 -0
  36. package/vendor/directive-core/dist/check/walk-test-files.js.map +1 -0
  37. package/vendor/directive-core/dist/fs/contained-write.d.ts +8 -2
  38. package/vendor/directive-core/dist/fs/contained-write.d.ts.map +1 -1
  39. package/vendor/directive-core/dist/fs/contained-write.js +22 -6
  40. package/vendor/directive-core/dist/fs/contained-write.js.map +1 -1
  41. package/vendor/directive-core/dist/hooks/dispatcher.d.ts +7 -1
  42. package/vendor/directive-core/dist/hooks/dispatcher.d.ts.map +1 -1
  43. package/vendor/directive-core/dist/hooks/dispatcher.js +180 -10
  44. package/vendor/directive-core/dist/hooks/dispatcher.js.map +1 -1
  45. package/vendor/directive-core/dist/hooks/scope.d.ts +54 -0
  46. package/vendor/directive-core/dist/hooks/scope.d.ts.map +1 -1
  47. package/vendor/directive-core/dist/hooks/scope.js +83 -2
  48. package/vendor/directive-core/dist/hooks/scope.js.map +1 -1
  49. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts +26 -1
  50. package/vendor/directive-core/dist/init-deposit/agent-hooks.d.ts.map +1 -1
  51. package/vendor/directive-core/dist/init-deposit/agent-hooks.js +52 -3
  52. package/vendor/directive-core/dist/init-deposit/agent-hooks.js.map +1 -1
  53. package/vendor/directive-core/dist/init-deposit/host-hook-outer.d.ts +43 -0
  54. package/vendor/directive-core/dist/init-deposit/host-hook-outer.d.ts.map +1 -1
  55. package/vendor/directive-core/dist/init-deposit/host-hook-outer.js +148 -0
  56. package/vendor/directive-core/dist/init-deposit/host-hook-outer.js.map +1 -1
  57. package/vendor/directive-core/dist/intake/reconcile-issues.d.ts +33 -1
  58. package/vendor/directive-core/dist/intake/reconcile-issues.d.ts.map +1 -1
  59. package/vendor/directive-core/dist/intake/reconcile-issues.js +288 -12
  60. package/vendor/directive-core/dist/intake/reconcile-issues.js.map +1 -1
  61. package/vendor/directive-core/dist/intake/thin-brief.d.ts +78 -0
  62. package/vendor/directive-core/dist/intake/thin-brief.d.ts.map +1 -0
  63. package/vendor/directive-core/dist/intake/thin-brief.js +224 -0
  64. package/vendor/directive-core/dist/intake/thin-brief.js.map +1 -0
  65. package/vendor/directive-core/dist/policy/ceremony-dial.d.ts.map +1 -1
  66. package/vendor/directive-core/dist/policy/ceremony-dial.js +2 -1
  67. package/vendor/directive-core/dist/policy/ceremony-dial.js.map +1 -1
  68. package/vendor/directive-core/dist/policy/small-change-lane.d.ts +193 -0
  69. package/vendor/directive-core/dist/policy/small-change-lane.d.ts.map +1 -0
  70. package/vendor/directive-core/dist/policy/small-change-lane.js +690 -0
  71. package/vendor/directive-core/dist/policy/small-change-lane.js.map +1 -0
  72. package/vendor/directive-core/dist/pr-merge-readiness/slizard-gate.d.ts +10 -5
  73. package/vendor/directive-core/dist/pr-merge-readiness/slizard-gate.d.ts.map +1 -1
  74. package/vendor/directive-core/dist/pr-merge-readiness/slizard-gate.js +41 -18
  75. package/vendor/directive-core/dist/pr-merge-readiness/slizard-gate.js.map +1 -1
  76. package/vendor/directive-core/dist/preflight/evaluate.d.ts +3 -0
  77. package/vendor/directive-core/dist/preflight/evaluate.d.ts.map +1 -1
  78. package/vendor/directive-core/dist/preflight/evaluate.js +15 -0
  79. package/vendor/directive-core/dist/preflight/evaluate.js.map +1 -1
  80. package/vendor/directive-core/dist/preflight-gh/pre-push.d.ts +3 -2
  81. package/vendor/directive-core/dist/preflight-gh/pre-push.d.ts.map +1 -1
  82. package/vendor/directive-core/dist/preflight-gh/pre-push.js +19 -18
  83. package/vendor/directive-core/dist/preflight-gh/pre-push.js.map +1 -1
  84. package/vendor/directive-core/dist/product-first-done-gate/evaluate.d.ts +6 -0
  85. package/vendor/directive-core/dist/product-first-done-gate/evaluate.d.ts.map +1 -1
  86. package/vendor/directive-core/dist/product-first-done-gate/evaluate.js +26 -0
  87. package/vendor/directive-core/dist/product-first-done-gate/evaluate.js.map +1 -1
  88. package/vendor/directive-core/dist/scope/decomposed-refs.d.ts +7 -0
  89. package/vendor/directive-core/dist/scope/decomposed-refs.d.ts.map +1 -1
  90. package/vendor/directive-core/dist/scope/decomposed-refs.js +27 -10
  91. package/vendor/directive-core/dist/scope/decomposed-refs.js.map +1 -1
  92. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts +218 -19
  93. package/vendor/directive-core/dist/scope/delivery-evidence.d.ts.map +1 -1
  94. package/vendor/directive-core/dist/scope/delivery-evidence.js +973 -107
  95. package/vendor/directive-core/dist/scope/delivery-evidence.js.map +1 -1
  96. package/vendor/directive-core/dist/scope/transition.d.ts +7 -1
  97. package/vendor/directive-core/dist/scope/transition.d.ts.map +1 -1
  98. package/vendor/directive-core/dist/scope/transition.js +45 -5
  99. package/vendor/directive-core/dist/scope/transition.js.map +1 -1
  100. package/vendor/directive-core/dist/scope-provenance/base-fence.d.ts +13 -3
  101. package/vendor/directive-core/dist/scope-provenance/base-fence.d.ts.map +1 -1
  102. package/vendor/directive-core/dist/scope-provenance/base-fence.js +25 -2
  103. package/vendor/directive-core/dist/scope-provenance/base-fence.js.map +1 -1
  104. package/vendor/directive-core/dist/scope-provenance/evaluate.d.ts +12 -0
  105. package/vendor/directive-core/dist/scope-provenance/evaluate.d.ts.map +1 -1
  106. package/vendor/directive-core/dist/scope-provenance/evaluate.js +41 -5
  107. package/vendor/directive-core/dist/scope-provenance/evaluate.js.map +1 -1
  108. package/vendor/directive-core/dist/scope-provenance/forge-approval.d.ts +106 -0
  109. package/vendor/directive-core/dist/scope-provenance/forge-approval.d.ts.map +1 -0
  110. package/vendor/directive-core/dist/scope-provenance/forge-approval.js +382 -0
  111. package/vendor/directive-core/dist/scope-provenance/forge-approval.js.map +1 -0
  112. package/vendor/directive-core/dist/session/orientation-compression.d.ts +18 -2
  113. package/vendor/directive-core/dist/session/orientation-compression.d.ts.map +1 -1
  114. package/vendor/directive-core/dist/session/orientation-compression.js +38 -5
  115. package/vendor/directive-core/dist/session/orientation-compression.js.map +1 -1
  116. package/vendor/directive-core/dist/session/session-start.d.ts +47 -2
  117. package/vendor/directive-core/dist/session/session-start.d.ts.map +1 -1
  118. package/vendor/directive-core/dist/session/session-start.js +149 -1
  119. package/vendor/directive-core/dist/session/session-start.js.map +1 -1
  120. package/vendor/directive-core/dist/swarm/approach1-babysitter.d.ts +18 -4
  121. package/vendor/directive-core/dist/swarm/approach1-babysitter.d.ts.map +1 -1
  122. package/vendor/directive-core/dist/swarm/approach1-babysitter.js +28 -4
  123. package/vendor/directive-core/dist/swarm/approach1-babysitter.js.map +1 -1
  124. package/vendor/directive-core/dist/swarm/finalize-cohort.d.ts +48 -0
  125. package/vendor/directive-core/dist/swarm/finalize-cohort.d.ts.map +1 -1
  126. package/vendor/directive-core/dist/swarm/finalize-cohort.js +175 -19
  127. package/vendor/directive-core/dist/swarm/finalize-cohort.js.map +1 -1
  128. package/vendor/directive-core/dist/swarm/finalize-owed.d.ts +18 -3
  129. package/vendor/directive-core/dist/swarm/finalize-owed.d.ts.map +1 -1
  130. package/vendor/directive-core/dist/swarm/finalize-owed.js +139 -32
  131. package/vendor/directive-core/dist/swarm/finalize-owed.js.map +1 -1
  132. package/vendor/directive-core/dist/swarm/launch.d.ts.map +1 -1
  133. package/vendor/directive-core/dist/swarm/launch.js +4 -8
  134. package/vendor/directive-core/dist/swarm/launch.js.map +1 -1
  135. package/vendor/directive-core/dist/swarm/worker-auth-assignment.d.ts +7 -0
  136. package/vendor/directive-core/dist/swarm/worker-auth-assignment.d.ts.map +1 -1
  137. package/vendor/directive-core/dist/swarm/worker-auth-assignment.js +16 -1
  138. package/vendor/directive-core/dist/swarm/worker-auth-assignment.js.map +1 -1
  139. package/vendor/directive-core/dist/task-surface/index.d.ts.map +1 -1
  140. package/vendor/directive-core/dist/task-surface/index.js +41 -0
  141. package/vendor/directive-core/dist/task-surface/index.js.map +1 -1
  142. package/vendor/directive-core/dist/triage/evaluate/worktrees.d.ts.map +1 -1
  143. package/vendor/directive-core/dist/triage/evaluate/worktrees.js +219 -60
  144. package/vendor/directive-core/dist/triage/evaluate/worktrees.js.map +1 -1
  145. package/vendor/directive-core/dist/vbrief-reconcile/types.d.ts +5 -1
  146. package/vendor/directive-core/dist/vbrief-reconcile/types.d.ts.map +1 -1
  147. package/vendor/directive-core/dist/vbrief-reconcile/umbrellas.d.ts.map +1 -1
  148. package/vendor/directive-core/dist/vbrief-reconcile/umbrellas.js +19 -7
  149. package/vendor/directive-core/dist/vbrief-reconcile/umbrellas.js.map +1 -1
@@ -0,0 +1,690 @@
1
+ /**
2
+ * Small-change lane under the rapid ceremony dial (#6204).
3
+ *
4
+ * One fail-closed evaluator next to the ceremony dial policy. A thin brief
5
+ * (see `intake/thin-brief.ts`) stays in the lane only while:
6
+ * - the ceremony dial depth is `rapid`;
7
+ * - the bound holds: `maxProductionFiles` / `maxChangedLines` from
8
+ * `plan.policy.ceremonyDial.smallChangeLane`, each field falling back to
9
+ * the shipped default (SMALL_CHANGE_LANE_DEFAULT_*) when unset. An invalid
10
+ * policy value or an unreadable PROJECT-DEFINITION refuses;
11
+ * - the measured cumulative product diff against the lane-admit base holds
12
+ * the bound;
13
+ * - the public-surface determination is `none`. `unknown` refuses.
14
+ *
15
+ * The measurement is git, never the provisional dial estimators
16
+ * (`estimateProvisionalCeremonyInputs`, task-size hints). Those are not proof.
17
+ *
18
+ * The gates that can refuse (xbrief:preflight, verify:ac, scope:complete) call
19
+ * `evaluateSmallChangeLaneGate`, so the bound is rechecked at each one.
20
+ *
21
+ * Lane exit: once the bound is crossed, every retry on that scope refuses until
22
+ * the brief is converted to the full shape (`escalateThinBriefToFullShape`).
23
+ * That conversion is scope-local. It is not the #3319 dial-depth raise
24
+ * (`escalateCeremonyDial`), which does not clear this refusal.
25
+ */
26
+ import { spawnSync } from "node:child_process";
27
+ import { existsSync, readFileSync } from "node:fs";
28
+ import { createRequire } from "node:module";
29
+ import { basename, dirname, extname, join } from "node:path";
30
+ import { atomicWriteBrief } from "../scope/brief-io.js";
31
+ import { isTestOrFixturePath } from "../scope-provenance/base-fence.js";
32
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
33
+ import { isRecognizedTestBasename } from "../test-boundary/evaluate.js";
34
+ import { resolveCeremonyDial } from "./ceremony-dial.js";
35
+ import { readPlanPolicy } from "./plan-extensions.js";
36
+ import { loadProjectDefinition } from "./resolve.js";
37
+ /** Policy field for the lane bound, nested under the ceremony dial policy. */
38
+ export const FIELD_SMALL_CHANGE_LANE = "plan.policy.ceremonyDial.smallChangeLane";
39
+ /** Plan metadata key that records lane membership and the lane-admit base. */
40
+ export const SMALL_CHANGE_LANE_MARKER_KEY = "x-directive/smallChangeLane";
41
+ /** Schema id of the lane marker. */
42
+ export const SMALL_CHANGE_LANE_SCHEMA = "deft.small-change-lane.v1";
43
+ /** The one remediation for every lane refusal: lane exit to the full brief shape. */
44
+ export const SMALL_CHANGE_LANE_REMEDIATION = "Remediation (lane exit, #6204): escalate-to-full-shape. Convert this scope in place " +
45
+ "with escalateThinBriefToFullShape (packages/core/src/intake/thin-brief.ts), or by hand: " +
46
+ "keep plan.id, the issue reference, metadata.swarm.file_scope and plan.acceptance; add " +
47
+ "plan.narratives and plan.items; set " +
48
+ `plan.metadata["${SMALL_CHANGE_LANE_MARKER_KEY}"].state to "escalated". ` +
49
+ "Raising the ceremony dial (escalateCeremonyDial, #3319) does not clear this refusal.";
50
+ /** Default production-file cap when the policy field is unset (operator choice, #6204). */
51
+ export const SMALL_CHANGE_LANE_DEFAULT_MAX_PRODUCTION_FILES = 5;
52
+ /** Default changed-line cap (added + deleted, tests included) when the policy field is unset. */
53
+ export const SMALL_CHANGE_LANE_DEFAULT_MAX_CHANGED_LINES = 500;
54
+ function verdict(kind, reason, measurement) {
55
+ return { ok: kind === "admitted", kind, reason, measurement };
56
+ }
57
+ /**
58
+ * Pure fail-closed evaluator. Admits only at depth `rapid`, with a valid
59
+ * bound (null means invalid policy), a successful measurement, public surface `none`, and counts at or
60
+ * under the bound. Every other input refuses.
61
+ */
62
+ export function evaluateSmallChangeLane(input) {
63
+ if (input.depth !== "rapid") {
64
+ return verdict("not-rapid", `ceremony dial depth is ${input.depth}; the small-change lane admits only at rapid`, null);
65
+ }
66
+ if (input.bound === null) {
67
+ const why = input.boundError ?? `${FIELD_SMALL_CHANGE_LANE} is invalid`;
68
+ return verdict("unconfigured", why, null);
69
+ }
70
+ if (!input.measure.ok) {
71
+ return verdict("unmeasured", `product diff could not be measured: ${input.measure.reason}`, null);
72
+ }
73
+ const m = input.measure.measurement;
74
+ if (m.publicSurface === "unknown") {
75
+ return verdict("public-surface-unknown", `public surface could not be determined: ${m.publicSurfaceReason}`, m);
76
+ }
77
+ if (m.publicSurface === "changed") {
78
+ return verdict("public-surface-changed", `public surface changed: ${m.publicSurfaceReason}`, m);
79
+ }
80
+ const files = m.productionFiles.length;
81
+ if (files > input.bound.maxProductionFiles || m.changedLines > input.bound.maxChangedLines) {
82
+ return verdict("bound-crossed", `bound crossed against ${m.base} (${m.source}): ${files} production file(s) ` +
83
+ `(max ${input.bound.maxProductionFiles}), ${m.changedLines} changed line(s) ` +
84
+ `(max ${input.bound.maxChangedLines})`, m);
85
+ }
86
+ return verdict("admitted", `within bound against ${m.base} (${m.source}): ${files} production file(s), ` +
87
+ `${m.changedLines} changed line(s), public surface none`, m);
88
+ }
89
+ function isNonNegativeInteger(value) {
90
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
91
+ }
92
+ /** The shipped default bound, used for any field the policy leaves unset. */
93
+ export const DEFAULT_SMALL_CHANGE_LANE_BOUND = {
94
+ maxProductionFiles: SMALL_CHANGE_LANE_DEFAULT_MAX_PRODUCTION_FILES,
95
+ maxChangedLines: SMALL_CHANGE_LANE_DEFAULT_MAX_CHANGED_LINES,
96
+ };
97
+ /**
98
+ * Validate a `plan.policy.ceremonyDial.smallChangeLane` payload. Each field is
99
+ * optional; a present field must be a non-negative integer.
100
+ */
101
+ export function validateSmallChangeLaneBound(value) {
102
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
103
+ return [`${FIELD_SMALL_CHANGE_LANE} must be an object`];
104
+ }
105
+ const rec = value;
106
+ const errors = [];
107
+ for (const key of ["maxProductionFiles", "maxChangedLines"]) {
108
+ if (rec[key] !== undefined && !isNonNegativeInteger(rec[key])) {
109
+ errors.push(`${FIELD_SMALL_CHANGE_LANE}.${key} must be a non-negative integer`);
110
+ }
111
+ }
112
+ return errors;
113
+ }
114
+ /**
115
+ * Read the bound from a `plan.policy.ceremonyDial` block. Unset fields use the
116
+ * shipped defaults; an invalid value refuses (bound null).
117
+ */
118
+ export function readSmallChangeLaneBound(ceremonyDialBlock) {
119
+ // Absent → defaults. Present but malformed → refuse (never default past bad policy).
120
+ if (ceremonyDialBlock === undefined || ceremonyDialBlock === null) {
121
+ return { bound: { ...DEFAULT_SMALL_CHANGE_LANE_BOUND }, error: null };
122
+ }
123
+ if (typeof ceremonyDialBlock !== "object" || Array.isArray(ceremonyDialBlock)) {
124
+ return { bound: null, error: "plan.policy.ceremonyDial must be an object" };
125
+ }
126
+ const raw = ceremonyDialBlock.smallChangeLane;
127
+ if (raw === undefined || raw === null) {
128
+ return { bound: { ...DEFAULT_SMALL_CHANGE_LANE_BOUND }, error: null };
129
+ }
130
+ const errors = validateSmallChangeLaneBound(raw);
131
+ if (errors.length > 0) {
132
+ return { bound: null, error: errors.join("; ") };
133
+ }
134
+ const rec = raw;
135
+ return {
136
+ bound: {
137
+ maxProductionFiles: rec.maxProductionFiles ?? DEFAULT_SMALL_CHANGE_LANE_BOUND.maxProductionFiles,
138
+ maxChangedLines: rec.maxChangedLines ?? DEFAULT_SMALL_CHANGE_LANE_BOUND.maxChangedLines,
139
+ },
140
+ error: null,
141
+ };
142
+ }
143
+ /** Read the bound from the project's PROJECT-DEFINITION policy. */
144
+ export function readSmallChangeLaneBoundFromProject(projectRoot) {
145
+ const [data, err] = loadProjectDefinition(projectRoot);
146
+ if (data === null) {
147
+ return { bound: null, error: err ?? "PROJECT-DEFINITION could not be loaded" };
148
+ }
149
+ const policy = readPlanPolicy(data.plan);
150
+ const block = policy !== null && typeof policy === "object" && !Array.isArray(policy)
151
+ ? policy.ceremonyDial
152
+ : undefined;
153
+ return readSmallChangeLaneBound(block);
154
+ }
155
+ export const defaultSmallChangeLaneGitRunner = (args, cwd) => {
156
+ const r = spawnSync("git", [...args], {
157
+ cwd,
158
+ encoding: "utf8",
159
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
160
+ windowsHide: true,
161
+ });
162
+ return { status: r.status ?? 1, stdout: typeof r.stdout === "string" ? r.stdout : "" };
163
+ };
164
+ /** Lifecycle paths are not product diff (briefs, changelog, framework state). */
165
+ export function isLaneLifecyclePath(relPath) {
166
+ const p = relPath.replace(/\\/g, "/");
167
+ return (p === "CHANGELOG.md" ||
168
+ p.startsWith("xbrief/") ||
169
+ p.startsWith("vbrief/") ||
170
+ p.startsWith(".deft/") ||
171
+ p.startsWith(".planning/"));
172
+ }
173
+ /** Test and fixture paths are product diff but not production files or public surface. */
174
+ export function isLaneTestPath(relPath) {
175
+ const p = relPath.replace(/\\/g, "/");
176
+ return isTestOrFixturePath(p) || isRecognizedTestBasename(p);
177
+ }
178
+ /** Basenames that are public surface by themselves (manifests, entry points, task runners). */
179
+ const PUBLIC_SURFACE_BASENAMES = new Set([
180
+ "package.json",
181
+ "pyproject.toml",
182
+ "setup.py",
183
+ "setup.cfg",
184
+ "cargo.toml",
185
+ "go.mod",
186
+ "taskfile.yml",
187
+ "taskfile.yaml",
188
+ "makefile",
189
+ "justfile",
190
+ "__init__.py",
191
+ "mod.rs",
192
+ "lib.rs",
193
+ ]);
194
+ const BARREL_BASENAME = /^index\.[cm]?[jt]sx?$/i;
195
+ const CODE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]);
196
+ const DOC_EXT = new Set([".md", ".mdx", ".txt", ".rst"]);
197
+ const EXPORT_LINE = /^[+-]\s*(export\b|module\.exports\b|exports\.)/;
198
+ /** Public-surface class of one production path, before reading its diff. */
199
+ export function classifyPublicSurfacePath(relPath) {
200
+ const p = relPath.replace(/\\/g, "/");
201
+ const base = basename(p).toLowerCase();
202
+ if (PUBLIC_SURFACE_BASENAMES.has(base) || BARREL_BASENAME.test(base))
203
+ return "surface";
204
+ if (p.startsWith("tasks/") && (base.endsWith(".yml") || base.endsWith(".yaml"))) {
205
+ return "surface";
206
+ }
207
+ const ext = extname(base);
208
+ if (CODE_EXT.has(ext))
209
+ return "code";
210
+ if (DOC_EXT.has(ext))
211
+ return "doc";
212
+ return "unknown";
213
+ }
214
+ function parseNumstatZ(stdout) {
215
+ const out = [];
216
+ for (const record of stdout.split("\0")) {
217
+ if (record.trim().length === 0)
218
+ continue;
219
+ const parts = record.replace(/^\n+/, "").split("\t");
220
+ if (parts.length < 3)
221
+ continue;
222
+ const [added, deleted, ...rest] = parts;
223
+ const path = rest.join("\t");
224
+ if (added === "-" || deleted === "-") {
225
+ out.push({ path, lines: null });
226
+ continue;
227
+ }
228
+ out.push({ path, lines: Number(added) + Number(deleted) });
229
+ }
230
+ return out;
231
+ }
232
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
233
+ function lineRange(start, count) {
234
+ const out = [];
235
+ for (let n = start; n < start + count; n += 1)
236
+ out.push(n);
237
+ return out;
238
+ }
239
+ /** Map each file in a `-U0` diff to its changed lines and hunk line numbers. */
240
+ function parseUnifiedDiff(stdout) {
241
+ const byFile = new Map();
242
+ let current = null;
243
+ for (const line of stdout.split("\n")) {
244
+ if (line.startsWith("diff --git ")) {
245
+ const match = / b\/(.+)$/.exec(line);
246
+ current = { lines: [], removed: [], added: [], baseText: null, headText: null };
247
+ if (match?.[1] !== undefined)
248
+ byFile.set(match[1], current);
249
+ continue;
250
+ }
251
+ if (current === null || line.startsWith("+++") || line.startsWith("---"))
252
+ continue;
253
+ const hunk = HUNK_HEADER.exec(line);
254
+ if (hunk !== null) {
255
+ current.removed.push(...lineRange(Number(hunk[1]), Number(hunk[2] ?? "1")));
256
+ current.added.push(...lineRange(Number(hunk[3]), Number(hunk[4] ?? "1")));
257
+ continue;
258
+ }
259
+ if (line.startsWith("+") || line.startsWith("-"))
260
+ current.lines.push(line);
261
+ }
262
+ return byFile;
263
+ }
264
+ const requireParser = createRequire(import.meta.url);
265
+ let cachedTs;
266
+ /** Engine-relative TypeScript parser, or null when it cannot load. */
267
+ function loadTypeScript() {
268
+ if (cachedTs !== undefined)
269
+ return cachedTs;
270
+ try {
271
+ const mod = requireParser("typescript");
272
+ const ts = "createSourceFile" in mod ? mod : mod.default;
273
+ cachedTs = ts !== undefined && typeof ts.createSourceFile === "function" ? ts : null;
274
+ }
275
+ catch {
276
+ cachedTs = null;
277
+ }
278
+ return cachedTs;
279
+ }
280
+ function scriptKindFor(ts, path) {
281
+ const ext = extname(path).toLowerCase();
282
+ if (ext === ".tsx")
283
+ return ts.ScriptKind.TSX;
284
+ if (ext === ".jsx")
285
+ return ts.ScriptKind.JSX;
286
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs")
287
+ return ts.ScriptKind.JS;
288
+ return ts.ScriptKind.TS;
289
+ }
290
+ function isExportSurfaceStatement(ts, stmt) {
291
+ if (ts.isExportDeclaration(stmt) || ts.isExportAssignment(stmt))
292
+ return true;
293
+ if (ts.canHaveModifiers(stmt) &&
294
+ (ts.getModifiers(stmt) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
295
+ return true;
296
+ }
297
+ if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression)) {
298
+ const left = stmt.expression.left.getText();
299
+ return left.startsWith("module.exports") || left.startsWith("exports.");
300
+ }
301
+ return false;
302
+ }
303
+ function functionBlockBody(ts, node) {
304
+ if (ts.isFunctionDeclaration(node) ||
305
+ ts.isMethodDeclaration(node) ||
306
+ ts.isConstructorDeclaration(node) ||
307
+ ts.isGetAccessorDeclaration(node) ||
308
+ ts.isSetAccessorDeclaration(node) ||
309
+ ts.isFunctionExpression(node) ||
310
+ ts.isArrowFunction(node)) {
311
+ return node.body !== undefined && ts.isBlock(node.body) ? node.body : null;
312
+ }
313
+ return null;
314
+ }
315
+ /**
316
+ * 1-based lines inside top-level export statements (export lists, exported
317
+ * declarations, CommonJS export assignments), minus the inner lines of
318
+ * function bodies. A changed line in this set changes public surface.
319
+ */
320
+ function exportSurfaceLines(ts, text, path) {
321
+ const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKindFor(ts, path));
322
+ const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
323
+ const out = new Set();
324
+ for (const stmt of sf.statements) {
325
+ if (!isExportSurfaceStatement(ts, stmt))
326
+ continue;
327
+ const excluded = new Set();
328
+ const visit = (node) => {
329
+ const body = functionBlockBody(ts, node);
330
+ if (body !== null) {
331
+ const open = lineOf(body.getStart(sf));
332
+ const close = lineOf(body.getEnd());
333
+ for (let n = open + 1; n < close; n += 1)
334
+ excluded.add(n);
335
+ return;
336
+ }
337
+ ts.forEachChild(node, visit);
338
+ };
339
+ visit(stmt);
340
+ const last = lineOf(stmt.getEnd());
341
+ for (let n = lineOf(stmt.getStart(sf)); n <= last; n += 1) {
342
+ if (!excluded.has(n))
343
+ out.add(n);
344
+ }
345
+ }
346
+ return out;
347
+ }
348
+ function countFileLines(text) {
349
+ if (text.length === 0)
350
+ return 0;
351
+ const newlines = text.split("\n").length - 1;
352
+ return text.endsWith("\n") ? newlines : newlines + 1;
353
+ }
354
+ function determinePublicSurface(productionFiles, changesByFile) {
355
+ for (const path of productionFiles) {
356
+ if (classifyPublicSurfacePath(path) === "surface") {
357
+ return { determination: "changed", reason: `${path} is a manifest, entry or task file` };
358
+ }
359
+ }
360
+ for (const path of productionFiles) {
361
+ const kind = classifyPublicSurfacePath(path);
362
+ if (kind === "unknown") {
363
+ return {
364
+ determination: "unknown",
365
+ reason: `${path} has no supported public-surface rule`,
366
+ };
367
+ }
368
+ if (kind !== "code")
369
+ continue;
370
+ const change = changesByFile.get(path);
371
+ if (change === undefined) {
372
+ return { determination: "unknown", reason: `${path} diff lines could not be read` };
373
+ }
374
+ if (change.lines.some((line) => EXPORT_LINE.test(line))) {
375
+ return { determination: "changed", reason: `${path} adds or removes an export` };
376
+ }
377
+ const sides = [
378
+ [change.removed, change.baseText, "base"],
379
+ [change.added, change.headText, "head"],
380
+ ];
381
+ for (const [lineNos, text, side] of sides) {
382
+ if (lineNos.length === 0)
383
+ continue;
384
+ const ts = loadTypeScript();
385
+ if (ts === null) {
386
+ return { determination: "unknown", reason: "the TypeScript parser could not be loaded" };
387
+ }
388
+ if (text === null) {
389
+ return { determination: "unknown", reason: `${path} ${side} contents could not be read` };
390
+ }
391
+ const surface = exportSurfaceLines(ts, text, path);
392
+ const hit = lineNos.find((n) => surface.has(n));
393
+ if (hit !== undefined) {
394
+ return {
395
+ determination: "changed",
396
+ reason: `${path} changes an export statement (${side} line ${hit})`,
397
+ };
398
+ }
399
+ }
400
+ }
401
+ return { determination: "none", reason: "no manifest, entry, task or export change" };
402
+ }
403
+ /**
404
+ * Measure the cumulative product diff. Worktree mode diffs the working tree
405
+ * (committed, staged, unstaged and untracked) against the lane-admit base.
406
+ * Delivered mode diffs the delivered commit against its first parent.
407
+ * Any git failure or binary file fails the measurement.
408
+ */
409
+ export function measureSmallChangeLaneDiff(options) {
410
+ const run = options.runGit ?? defaultSmallChangeLaneGitRunner;
411
+ // Run from the repository top level so every path is repo-root relative.
412
+ const top = run(["rev-parse", "--show-toplevel"], options.projectRoot);
413
+ if (top.status !== 0 || top.stdout.trim().length === 0) {
414
+ return { ok: false, reason: `${options.projectRoot} is not inside a git work tree` };
415
+ }
416
+ const cwd = top.stdout.trim();
417
+ const delivered = options.deliveredCommit?.trim() ?? "";
418
+ const source = delivered.length > 0 ? "delivered" : "worktree";
419
+ const range = source === "delivered" ? [`${delivered}^1`, delivered] : [options.admitBase.trim()];
420
+ if (range.some((ref) => ref.length === 0)) {
421
+ return { ok: false, reason: "no comparison base" };
422
+ }
423
+ const resolved = run(["rev-parse", "--verify", "--quiet", `${range[0]}^{commit}`], cwd);
424
+ if (resolved.status !== 0 || resolved.stdout.trim().length === 0) {
425
+ return { ok: false, reason: `comparison base ${range[0]} does not resolve to a commit` };
426
+ }
427
+ const base = resolved.stdout.trim();
428
+ const numstat = run(["diff", "--numstat", "-z", "--no-renames", ...range], cwd);
429
+ if (numstat.status !== 0) {
430
+ return { ok: false, reason: `git diff --numstat failed against ${range[0]}` };
431
+ }
432
+ const stats = parseNumstatZ(numstat.stdout);
433
+ const untrackedText = new Map();
434
+ if (source === "worktree") {
435
+ const untracked = run(["ls-files", "--others", "--exclude-standard", "-z"], cwd);
436
+ if (untracked.status !== 0) {
437
+ return { ok: false, reason: "git ls-files --others failed" };
438
+ }
439
+ for (const path of untracked.stdout.split("\0")) {
440
+ if (path.length === 0)
441
+ continue;
442
+ let text;
443
+ try {
444
+ text = readFileSync(join(cwd, path), "utf8");
445
+ }
446
+ catch {
447
+ return { ok: false, reason: `untracked file ${path} could not be read` };
448
+ }
449
+ if (text.includes("\0")) {
450
+ stats.push({ path, lines: null });
451
+ continue;
452
+ }
453
+ untrackedText.set(path, text);
454
+ stats.push({ path, lines: countFileLines(text) });
455
+ }
456
+ }
457
+ const product = stats.filter((s) => !isLaneLifecyclePath(s.path));
458
+ const binary = product.find((s) => s.lines === null);
459
+ if (binary !== undefined) {
460
+ return { ok: false, reason: `${binary.path} is binary; line count is unknown` };
461
+ }
462
+ const changedLines = product.reduce((sum, s) => sum + (s.lines ?? 0), 0);
463
+ const productionFiles = [
464
+ ...new Set(product.filter((s) => !isLaneTestPath(s.path)).map((s) => s.path)),
465
+ ].sort();
466
+ const trackedCode = productionFiles.filter((p) => classifyPublicSurfacePath(p) === "code" && !untrackedText.has(p));
467
+ const changesByFile = new Map();
468
+ if (trackedCode.length > 0) {
469
+ const diff = run([
470
+ "-c",
471
+ "core.quotePath=false",
472
+ "diff",
473
+ "-U0",
474
+ "--no-color",
475
+ "--no-renames",
476
+ ...range,
477
+ "--",
478
+ ...trackedCode,
479
+ ], cwd);
480
+ if (diff.status !== 0) {
481
+ return { ok: false, reason: "git diff -U0 failed for production code files" };
482
+ }
483
+ for (const [path, change] of parseUnifiedDiff(diff.stdout)) {
484
+ if (change.removed.length > 0) {
485
+ const shown = run(["show", `${range[0]}:${path}`], cwd);
486
+ change.baseText = shown.status === 0 ? shown.stdout : null;
487
+ }
488
+ if (change.added.length > 0) {
489
+ if (source === "delivered") {
490
+ const shown = run(["show", `${delivered}:${path}`], cwd);
491
+ change.headText = shown.status === 0 ? shown.stdout : null;
492
+ }
493
+ else {
494
+ const abs = join(cwd, path);
495
+ change.headText = existsSync(abs) ? readFileSync(abs, "utf8") : null;
496
+ }
497
+ }
498
+ changesByFile.set(path, change);
499
+ }
500
+ }
501
+ for (const [path, text] of untrackedText) {
502
+ const lines = text.split("\n");
503
+ changesByFile.set(path, {
504
+ lines: lines.map((line) => `+${line}`),
505
+ removed: [],
506
+ added: lineRange(1, lines.length),
507
+ baseText: null,
508
+ headText: text,
509
+ });
510
+ }
511
+ const surface = determinePublicSurface(productionFiles, changesByFile);
512
+ return {
513
+ ok: true,
514
+ measurement: {
515
+ base,
516
+ source,
517
+ productionFiles,
518
+ changedLines,
519
+ publicSurface: surface.determination,
520
+ publicSurfaceReason: surface.reason,
521
+ },
522
+ };
523
+ }
524
+ const SMALL_CHANGE_LANE_STATES = ["admitted", "crossed", "escalated"];
525
+ function asRecord(value) {
526
+ return value !== null && typeof value === "object" && !Array.isArray(value)
527
+ ? value
528
+ : null;
529
+ }
530
+ /** Build the marker the thin-brief emitter writes. */
531
+ export function buildSmallChangeLaneMarker(admitBase, state = "admitted") {
532
+ return { schema: SMALL_CHANGE_LANE_SCHEMA, state, admitBase };
533
+ }
534
+ /** Read lane membership from `plan.metadata`. */
535
+ export function readSmallChangeLaneMarker(plan) {
536
+ const meta = asRecord(asRecord(plan)?.metadata);
537
+ if (meta === null || !Object.hasOwn(meta, SMALL_CHANGE_LANE_MARKER_KEY)) {
538
+ return { present: false };
539
+ }
540
+ const rec = asRecord(meta[SMALL_CHANGE_LANE_MARKER_KEY]);
541
+ if (rec === null || rec.schema !== SMALL_CHANGE_LANE_SCHEMA) {
542
+ return {
543
+ present: true,
544
+ ok: false,
545
+ reason: `lane marker schema is not ${SMALL_CHANGE_LANE_SCHEMA}`,
546
+ };
547
+ }
548
+ if (typeof rec.state !== "string" || !SMALL_CHANGE_LANE_STATES.includes(rec.state)) {
549
+ return {
550
+ present: true,
551
+ ok: false,
552
+ reason: 'lane marker state must be "admitted", "crossed" or "escalated"',
553
+ };
554
+ }
555
+ const admitBase = typeof rec.admitBase === "string" ? rec.admitBase.trim() : "";
556
+ if (admitBase.length === 0) {
557
+ return { present: true, ok: false, reason: "lane marker has no admitBase" };
558
+ }
559
+ const state = rec.state;
560
+ const crossedReason = typeof rec.crossedReason === "string" ? rec.crossedReason : undefined;
561
+ return {
562
+ present: true,
563
+ ok: true,
564
+ marker: crossedReason !== undefined ? { state, admitBase, crossedReason } : { state, admitBase },
565
+ };
566
+ }
567
+ /**
568
+ * Persist a crossing on the brief file so later retries refuse even if the
569
+ * diff shrinks back under the bound. Returns false when the write fails.
570
+ */
571
+ export function recordSmallChangeLaneCrossing(briefPath, reason, projectRoot) {
572
+ try {
573
+ const data = JSON.parse(readFileSync(briefPath, "utf8"));
574
+ const root = asRecord(data);
575
+ const meta = asRecord(asRecord(root?.plan)?.metadata);
576
+ const marker = asRecord(meta?.[SMALL_CHANGE_LANE_MARKER_KEY]);
577
+ if (root === null || meta === null || marker === null)
578
+ return false;
579
+ meta[SMALL_CHANGE_LANE_MARKER_KEY] = { ...marker, state: "crossed", crossedReason: reason };
580
+ // Contained temp-then-rename write: a failed save never truncates the brief.
581
+ const written = atomicWriteBrief(briefPath, root, dirname(dirname(briefPath)), {
582
+ ...(projectRoot !== undefined ? { projectRoot } : {}),
583
+ });
584
+ return written.ok;
585
+ }
586
+ catch {
587
+ return false;
588
+ }
589
+ }
590
+ /** Full brief representation: at least one narrative with text and at least one plan item. */
591
+ export function isFullBriefShape(plan) {
592
+ const rec = asRecord(plan);
593
+ if (rec === null)
594
+ return false;
595
+ const narratives = asRecord(rec.narratives);
596
+ const hasNarrative = narratives !== null &&
597
+ Object.values(narratives).some((v) => typeof v === "string" && v.trim().length > 0);
598
+ const hasItems = Array.isArray(rec.items) && rec.items.length > 0;
599
+ return hasNarrative && hasItems;
600
+ }
601
+ /** Verdicts that mean the change outgrew the lane after admit. */
602
+ const CROSSING_KINDS = new Set([
603
+ "bound-crossed",
604
+ "public-surface-changed",
605
+ ]);
606
+ function refuse(gate, reason, laneVerdict) {
607
+ return {
608
+ applies: true,
609
+ ok: false,
610
+ message: `[deft small-change-lane] ${gate} refused (#6204): ${reason}\n ${SMALL_CHANGE_LANE_REMEDIATION}`,
611
+ verdict: laneVerdict,
612
+ };
613
+ }
614
+ /**
615
+ * Recheck the lane at a gate that can refuse. No marker → not in the lane, no-op.
616
+ * Admitted marker → measure and evaluate; any refusal names the lane exit.
617
+ * Escalated marker → the scope left the lane; it passes here only once the
618
+ * brief has the full shape, and the existing gates then decide.
619
+ */
620
+ export function evaluateSmallChangeLaneGate(plan, options) {
621
+ const read = readSmallChangeLaneMarker(plan);
622
+ if (!read.present) {
623
+ return { applies: false, ok: true, message: "", verdict: null };
624
+ }
625
+ if (!read.ok) {
626
+ return refuse(options.gate, read.reason, null);
627
+ }
628
+ if (read.marker.state === "escalated") {
629
+ if (isFullBriefShape(plan)) {
630
+ return {
631
+ applies: true,
632
+ ok: true,
633
+ message: "[deft small-change-lane] scope left the lane; full brief shape applies (#6204)",
634
+ verdict: null,
635
+ };
636
+ }
637
+ return refuse(options.gate, "lane exit is incomplete: the brief is marked escalated but lacks narratives or items", null);
638
+ }
639
+ if (read.marker.state === "crossed") {
640
+ const why = read.marker.crossedReason ?? "bound crossed";
641
+ return refuse(options.gate, `the bound was crossed after admit (${why}); the scope stays refused until lane exit`, null);
642
+ }
643
+ let depth;
644
+ if (options.depth !== undefined) {
645
+ depth = options.depth;
646
+ }
647
+ else {
648
+ // An invalid dial policy falls back to a default depth; the lane does not.
649
+ const selection = resolveCeremonyDial(options.projectRoot);
650
+ if (selection.error !== null) {
651
+ return refuse(options.gate, `ceremony dial policy is invalid: ${selection.error}`, null);
652
+ }
653
+ depth = selection.depth;
654
+ }
655
+ const boundRead = options.bound !== undefined
656
+ ? {
657
+ bound: options.bound,
658
+ error: options.bound === null ? `${FIELD_SMALL_CHANGE_LANE} is invalid` : null,
659
+ }
660
+ : readSmallChangeLaneBoundFromProject(options.projectRoot);
661
+ const measure = measureSmallChangeLaneDiff({
662
+ projectRoot: options.projectRoot,
663
+ admitBase: read.marker.admitBase,
664
+ deliveredCommit: options.deliveredCommit,
665
+ runGit: options.runGit,
666
+ });
667
+ const laneVerdict = evaluateSmallChangeLane({
668
+ depth,
669
+ bound: boundRead.bound,
670
+ boundError: boundRead.error,
671
+ measure,
672
+ });
673
+ if (!laneVerdict.ok) {
674
+ if (CROSSING_KINDS.has(laneVerdict.kind) && options.briefPath) {
675
+ const recorded = recordSmallChangeLaneCrossing(options.briefPath, laneVerdict.reason, options.projectRoot);
676
+ const note = recorded
677
+ ? "recorded on the brief as crossed"
678
+ : "could not record the crossing on the brief";
679
+ return refuse(options.gate, `${laneVerdict.reason}; ${note}`, laneVerdict);
680
+ }
681
+ return refuse(options.gate, laneVerdict.reason, laneVerdict);
682
+ }
683
+ return {
684
+ applies: true,
685
+ ok: true,
686
+ message: `[deft small-change-lane] ${options.gate}: ${laneVerdict.reason} (#6204)`,
687
+ verdict: laneVerdict,
688
+ };
689
+ }
690
+ //# sourceMappingURL=small-change-lane.js.map