@deftai/directive-core 0.105.0 → 0.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/named-cause.js +1 -1
  3. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  4. package/dist/forward-coverage/diff-coverage.js +112 -0
  5. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  6. package/dist/forward-coverage/diff-lines.js +173 -0
  7. package/dist/forward-coverage/evaluate.d.ts +24 -9
  8. package/dist/forward-coverage/evaluate.js +125 -19
  9. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  10. package/dist/init-deposit/agent-hooks.js +1 -1
  11. package/dist/intake/clause-derivation.d.ts +9 -0
  12. package/dist/intake/clause-derivation.js +30 -3
  13. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  14. package/dist/lifecycle-visible/evaluate.js +710 -0
  15. package/dist/lifecycle-visible/index.d.ts +2 -0
  16. package/dist/lifecycle-visible/index.js +2 -0
  17. package/dist/literal-acceptance/capture.js +3 -3
  18. package/dist/orchestration/judgment-policy.d.ts +13 -0
  19. package/dist/orchestration/judgment-policy.js +15 -0
  20. package/dist/orchestration/probe-session.js +13 -8
  21. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  22. package/dist/orchestration/verify-judgment-gates.js +43 -11
  23. package/dist/policy/ceremony-dial.js +7 -7
  24. package/dist/policy/host-hooks.d.ts +20 -0
  25. package/dist/policy/host-hooks.js +121 -2
  26. package/dist/policy/org-force-on-migration.js +2 -1
  27. package/dist/policy/plan-extensions.d.ts +1 -1
  28. package/dist/policy/plan-extensions.js +2 -0
  29. package/dist/policy/product-signal.js +6 -7
  30. package/dist/policy/require-human-merge.js +8 -6
  31. package/dist/policy/resolve.d.ts +7 -2
  32. package/dist/policy/resolve.js +25 -8
  33. package/dist/policy/value-feedback.js +7 -8
  34. package/dist/preflight-cache/evaluate.d.ts +12 -0
  35. package/dist/preflight-cache/evaluate.js +15 -1
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  39. package/dist/product-first-done-gate/evaluate.js +46 -5
  40. package/dist/release/closed-verb-gate.d.ts +12 -0
  41. package/dist/release/closed-verb-gate.js +51 -0
  42. package/dist/release/index.d.ts +1 -0
  43. package/dist/release/index.js +1 -0
  44. package/dist/release/pipeline.js +7 -0
  45. package/dist/release/types.d.ts +12 -0
  46. package/dist/render/constants.d.ts +2 -1
  47. package/dist/render/constants.js +2 -1
  48. package/dist/render/framework-commands.js +4 -0
  49. package/dist/render/spec-validate.js +23 -12
  50. package/dist/run-summary/types.d.ts +4 -0
  51. package/dist/scope/main.js +15 -5
  52. package/dist/scope/promote-from-issue.js +5 -5
  53. package/dist/scope-provenance/evaluate.js +3 -3
  54. package/dist/session/ac-pass-banking.d.ts +5 -0
  55. package/dist/session/ac-pass-banking.js +3 -2
  56. package/dist/session/process-cost.d.ts +32 -10
  57. package/dist/session/process-cost.js +189 -1
  58. package/dist/session/product-state-hash.js +41 -24
  59. package/dist/session/ritual-entrypoint.js +16 -1
  60. package/dist/session/session-start.d.ts +6 -0
  61. package/dist/session/session-start.js +32 -2
  62. package/dist/session/verify-session-ritual.d.ts +24 -1
  63. package/dist/session/verify-session-ritual.js +89 -6
  64. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  65. package/dist/swarm/finalize-cohort-cli.js +165 -35
  66. package/dist/swarm/finalize-cohort.d.ts +1 -0
  67. package/dist/swarm/finalize-cohort.js +14 -2
  68. package/dist/swarm/routing-set-cli.js +2 -0
  69. package/dist/swarm/routing-verify.d.ts +1 -1
  70. package/dist/swarm/routing-verify.js +2 -2
  71. package/dist/triage/bootstrap/index.js +6 -6
  72. package/dist/triage/help/registry-data.d.ts +1 -1
  73. package/dist/triage/help/registry-data.js +1 -1
  74. package/dist/triage/welcome/writers.d.ts +1 -1
  75. package/dist/triage/welcome/writers.js +10 -5
  76. package/dist/value/readback.d.ts +4 -1
  77. package/dist/value/readback.js +51 -12
  78. package/dist/vbrief-validate/schema.d.ts +4 -0
  79. package/dist/vbrief-validate/schema.js +1 -1
  80. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  81. package/dist/verify-env/agent-hook-readiness.js +12 -8
  82. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  83. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  84. package/dist/verify-env/agent-hooks.js +3 -4
  85. package/package.json +7 -3
@@ -0,0 +1,710 @@
1
+ /**
2
+ * verify:lifecycle-visible — detect ignore/index flags that hide lifecycle roots (#3505).
3
+ *
4
+ * Warn-only by default (exit 0). Pass --enforce to fail closed.
5
+ * Not a task-check gate: this is a per-clone environment property.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
8
+ import { isAbsolute, relative, resolve } from "node:path";
9
+ import { defaultGitRunner } from "../session/git.js";
10
+ const EXIT_OK = 0;
11
+ const EXIT_ENFORCE_FINDINGS = 1;
12
+ const EXIT_CONFIG = 2;
13
+ export const LIFECYCLE_ROOT_PREFIXES = ["xbrief", "vbrief"];
14
+ export const LIFECYCLE_STAGE_DIRS = [
15
+ "proposed",
16
+ "pending",
17
+ "active",
18
+ "completed",
19
+ "cancelled",
20
+ ];
21
+ /** Canonical lifecycle root pathspecs (`xbrief/active/`, `vbrief/pending/`, …). */
22
+ export function lifecycleRootRelPaths() {
23
+ const paths = [];
24
+ for (const prefix of LIFECYCLE_ROOT_PREFIXES) {
25
+ for (const stage of LIFECYCLE_STAGE_DIRS) {
26
+ paths.push(`${prefix}/${stage}/`);
27
+ }
28
+ }
29
+ return paths;
30
+ }
31
+ /** Fills `*` in ignore globs so a derived probe still matches the rule. */
32
+ export const LIFECYCLE_PROBE_STEM = "lifecycle-visible";
33
+ /**
34
+ * Date-prefixed brief-shaped fallback when no ignore-rule date glob exists.
35
+ * Date globs emit their own matching names (`2026-07-*` → a July file, `2025-*` → 2025).
36
+ */
37
+ export const LIFECYCLE_PROBE_SENTINEL = "2026-01-01-lifecycle-visible.xbrief.json";
38
+ /** Legacy vbrief filename so `*.vbrief.json` / `2026-*.vbrief.json` cannot report clean. */
39
+ export const LIFECYCLE_PROBE_SENTINEL_VBRIEF = "2026-01-01-lifecycle-visible.vbrief.json";
40
+ /** Windows CreateProcess argv budget; chunks check-ignore, never drops probes. */
41
+ const CHECK_IGNORE_ARGV_CHAR_BUDGET = 24_000;
42
+ function sentinelForRoot(root) {
43
+ return root.startsWith("vbrief/") ? LIFECYCLE_PROBE_SENTINEL_VBRIEF : LIFECYCLE_PROBE_SENTINEL;
44
+ }
45
+ /** Stage dirs plus one matching-extension sentinel under each. */
46
+ export function lifecycleIgnoreProbeRelPaths() {
47
+ const probes = [];
48
+ for (const root of lifecycleRootRelPaths()) {
49
+ probes.push(root);
50
+ probes.push(`${root}${sentinelForRoot(root)}`);
51
+ }
52
+ return probes;
53
+ }
54
+ /** True when an ignore glob could hide a convention-valid brief. */
55
+ export function ignorePatternLooksLifecycleRelevant(pattern) {
56
+ const p = pattern.replace(/\\/g, "/").replace(/\/+$/, "");
57
+ if (p.includes(".triage-cache"))
58
+ return false;
59
+ if (/(^|\/)(xbrief|vbrief)(\/|$)/.test(p))
60
+ return true;
61
+ if (p.includes(".xbrief.json") || p.includes(".vbrief.json"))
62
+ return true;
63
+ if (/(^|\/)\*\.json$/.test(p) || p.endsWith("/*.json"))
64
+ return true;
65
+ if (/\d{4}-/.test(p) || /\[[0-9]/.test(p))
66
+ return true;
67
+ return false;
68
+ }
69
+ /** True when `ch` is listed in a gitignore class body (`0-5`, `abc`). Linear scan. */
70
+ function gitignoreClassContains(inner, ch) {
71
+ const code = ch.charCodeAt(0);
72
+ let i = 0;
73
+ while (i < inner.length) {
74
+ const a = inner[i] ?? "";
75
+ if (i + 2 < inner.length && inner[i + 1] === "-") {
76
+ const b = inner[i + 2] ?? "";
77
+ if (a.charCodeAt(0) <= code && code <= b.charCodeAt(0))
78
+ return true;
79
+ i += 3;
80
+ continue;
81
+ }
82
+ if (a === ch)
83
+ return true;
84
+ i += 1;
85
+ }
86
+ return false;
87
+ }
88
+ /** One concrete char that still matches a gitignore class. Negated classes skip `0`. */
89
+ function pickGitignoreClassChar(inner, negated) {
90
+ const unescaped = inner.replace(/\\/g, "");
91
+ if (!negated)
92
+ return unescaped[0] ?? "0";
93
+ if (unescaped.length === 0)
94
+ return "0";
95
+ const candidates = "6789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01";
96
+ for (const c of candidates) {
97
+ if (!gitignoreClassContains(unescaped, c))
98
+ return c;
99
+ }
100
+ return "z";
101
+ }
102
+ /** Linear `[abc]` expansion — avoid nested regex quantifiers (CodeQL js/polynomial-redos). */
103
+ export function expandGitignoreCharClasses(body) {
104
+ let out = "";
105
+ let i = 0;
106
+ while (i < body.length) {
107
+ const ch = body[i];
108
+ if (ch !== "[") {
109
+ out += ch;
110
+ i += 1;
111
+ continue;
112
+ }
113
+ const close = body.indexOf("]", i + 1);
114
+ if (close < 0) {
115
+ out += ch;
116
+ i += 1;
117
+ continue;
118
+ }
119
+ let inner = body.slice(i + 1, close);
120
+ const negated = inner.startsWith("!") || inner.startsWith("^");
121
+ if (negated)
122
+ inner = inner.slice(1);
123
+ out += pickGitignoreClassChar(inner, negated);
124
+ i = close + 1;
125
+ }
126
+ return out;
127
+ }
128
+ /**
129
+ * Expand one gitignore glob into a single concrete pathname that still matches it.
130
+ * `*` becomes {@link LIFECYCLE_PROBE_STEM}; `**` collapses; `?` becomes `0`.
131
+ */
132
+ export function expandGitignoreGlobToConcrete(pattern) {
133
+ let body = pattern.replace(/\\/g, "/").trim();
134
+ if (body.startsWith("!"))
135
+ return null;
136
+ if (body.endsWith("/"))
137
+ body = body.slice(0, -1);
138
+ if (body.startsWith("/"))
139
+ body = body.slice(1);
140
+ if (body.length === 0)
141
+ return null;
142
+ body = expandGitignoreCharClasses(body);
143
+ body = body.replace(/\*\*/g, "");
144
+ body = body.replace(/\*/g, LIFECYCLE_PROBE_STEM);
145
+ body = body.replace(/\?/g, "0");
146
+ body = body.replace(/\/{2,}/g, "/").replace(/^\/+/, "");
147
+ return body.length > 0 ? body : null;
148
+ }
149
+ function lifecycleRootsForBasenameProbe(name) {
150
+ const roots = lifecycleRootRelPaths();
151
+ if (name.endsWith(".vbrief.json"))
152
+ return roots.filter((r) => r.startsWith("vbrief/"));
153
+ if (name.endsWith(".xbrief.json"))
154
+ return roots.filter((r) => r.startsWith("xbrief/"));
155
+ return roots;
156
+ }
157
+ function posixTrimDir(dir) {
158
+ return dir.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
159
+ }
160
+ /** Join a glob-expanded probe to the ignore file's directory (Git's match root). */
161
+ export function joinIgnoreFileBase(baseDir, concrete) {
162
+ const base = posixTrimDir(baseDir);
163
+ if (base.length === 0)
164
+ return concrete;
165
+ if (concrete === base || concrete.startsWith(`${base}/`))
166
+ return concrete;
167
+ return `${base}/${concrete}`;
168
+ }
169
+ function lifecycleRootsUnderIgnoreBase(name, baseDir) {
170
+ const roots = lifecycleRootsForBasenameProbe(name);
171
+ const base = posixTrimDir(baseDir);
172
+ if (base.length === 0)
173
+ return roots;
174
+ return roots.filter((root) => {
175
+ const r = root.replace(/\/+$/, "");
176
+ return r === base || r.startsWith(`${base}/`);
177
+ });
178
+ }
179
+ function isLifecycleStageName(name) {
180
+ return LIFECYCLE_STAGE_DIRS.includes(name);
181
+ }
182
+ /**
183
+ * Map a glob-expanded pathname onto real stage roots.
184
+ * A stage-wildcard glob expands `*` to the stem, which is not a stage —
185
+ * rewrite that slot to each canonical stage so nested globs cannot evade.
186
+ */
187
+ function candidateLifecycleProbePaths(concrete, baseDir) {
188
+ const rebased = joinIgnoreFileBase(baseDir, concrete);
189
+ const out = [];
190
+ const seen = new Set();
191
+ const push = (relPosix) => {
192
+ const posix = relPosix.replace(/\\/g, "/").replace(/^\/+/, "");
193
+ if (posix.length === 0 || seen.has(posix))
194
+ return;
195
+ if (lifecycleRootForRelPath(posix) === null)
196
+ return;
197
+ seen.add(posix);
198
+ out.push(posix);
199
+ };
200
+ push(rebased);
201
+ const parts = rebased.split("/").filter((p) => p.length > 0);
202
+ const prefix = parts[0] ?? "";
203
+ if (prefix === "xbrief" || prefix === "vbrief") {
204
+ if (parts.length === 2) {
205
+ const name = parts[1] ?? "";
206
+ for (const stage of LIFECYCLE_STAGE_DIRS)
207
+ push(`${prefix}/${stage}/${name}`);
208
+ }
209
+ else if (parts.length >= 3 && !isLifecycleStageName(parts[1] ?? "")) {
210
+ const rest = parts.slice(2).join("/");
211
+ for (const stage of LIFECYCLE_STAGE_DIRS)
212
+ push(`${prefix}/${stage}/${rest}`);
213
+ }
214
+ }
215
+ else if (isLifecycleStageName(prefix)) {
216
+ const name = parts[parts.length - 1] ?? "";
217
+ const remainder = parts.slice(1).join("/");
218
+ for (const root of lifecycleRootsForBasenameProbe(name)) {
219
+ const stage = root.replace(/\/+$/, "").split("/")[1] ?? "";
220
+ if (stage === prefix)
221
+ push(`${root.replace(/\/+$/, "")}/${remainder}`);
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+ function ignoreFileBaseDir(projectRoot, file) {
227
+ try {
228
+ const rel = relative(resolve(projectRoot), resolve(file)).replace(/\\/g, "/");
229
+ if (rel.length === 0 || rel.startsWith("..") || isAbsolute(rel))
230
+ return "";
231
+ if (rel === ".gitignore" || rel === ".git/info/exclude")
232
+ return "";
233
+ if (rel.endsWith("/.gitignore"))
234
+ return rel.slice(0, -"/.gitignore".length);
235
+ }
236
+ catch {
237
+ return "";
238
+ }
239
+ return "";
240
+ }
241
+ /**
242
+ * One derived probe per relevant ignore-rule line (bounded by ignore-file size, not a numeric cap).
243
+ * Date globs keep their year/month so `2026-07-*` probes a July name, not the January fallback.
244
+ * Slash patterns keep the ignore-file directory; stage wildcards expand onto real stages.
245
+ */
246
+ export function derivedLifecycleIgnoreProbesFromSources(sources) {
247
+ const out = [];
248
+ const seen = new Set();
249
+ const add = (relPosix) => {
250
+ const posix = relPosix.replace(/\\/g, "/").replace(/^\/+/, "");
251
+ if (posix.length === 0 || seen.has(posix))
252
+ return;
253
+ if (lifecycleRootForRelPath(posix) === null)
254
+ return;
255
+ seen.add(posix);
256
+ out.push(posix);
257
+ };
258
+ for (const source of sources) {
259
+ const baseDir = posixTrimDir(source.baseDir);
260
+ for (const pattern of source.patterns) {
261
+ if (!ignorePatternLooksLifecycleRelevant(pattern))
262
+ continue;
263
+ const concrete = expandGitignoreGlobToConcrete(pattern);
264
+ if (concrete === null)
265
+ continue;
266
+ for (const candidate of candidateLifecycleProbePaths(concrete, baseDir)) {
267
+ add(candidate);
268
+ }
269
+ if (!concrete.includes("/")) {
270
+ for (const root of lifecycleRootsUnderIgnoreBase(concrete, baseDir)) {
271
+ add(`${root}${concrete}`);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ return out;
277
+ }
278
+ function collectNestedLifecycleGitignoreFiles(projectRoot) {
279
+ const files = [];
280
+ const walk = (dir) => {
281
+ let entries;
282
+ try {
283
+ entries = readdirSync(dir, { withFileTypes: true });
284
+ }
285
+ catch {
286
+ return;
287
+ }
288
+ for (const ent of entries) {
289
+ if (ent.name === ".gitignore" && (ent.isFile() || ent.isSymbolicLink())) {
290
+ files.push(resolve(dir, ent.name));
291
+ continue;
292
+ }
293
+ if (!ent.isDirectory() || ent.name.startsWith("."))
294
+ continue;
295
+ walk(resolve(dir, ent.name));
296
+ }
297
+ };
298
+ for (const prefix of LIFECYCLE_ROOT_PREFIXES) {
299
+ walk(joinPath(projectRoot, prefix));
300
+ }
301
+ return files;
302
+ }
303
+ function readIgnorePatternSources(projectRoot, runGit) {
304
+ const files = new Set([
305
+ joinPath(projectRoot, ".gitignore"),
306
+ joinPath(projectRoot, ".git/info/exclude"),
307
+ ]);
308
+ for (const prefix of LIFECYCLE_ROOT_PREFIXES) {
309
+ files.add(joinPath(projectRoot, `${prefix}/.gitignore`));
310
+ for (const stage of LIFECYCLE_STAGE_DIRS) {
311
+ files.add(joinPath(projectRoot, `${prefix}/${stage}/.gitignore`));
312
+ }
313
+ }
314
+ for (const nested of collectNestedLifecycleGitignoreFiles(projectRoot)) {
315
+ files.add(nested);
316
+ }
317
+ const cfg = runGit(projectRoot, ["config", "--get", "core.excludesFile"]);
318
+ if (cfg.code === 0 && cfg.stdout.trim().length > 0) {
319
+ const raw = cfg.stdout.trim();
320
+ files.add(isAbsolute(raw) ? raw : resolve(projectRoot, raw));
321
+ }
322
+ const sources = [];
323
+ for (const file of files) {
324
+ if (!existsSync(file))
325
+ continue;
326
+ let text;
327
+ try {
328
+ text = readFileSync(file, "utf8");
329
+ }
330
+ catch {
331
+ continue;
332
+ }
333
+ const patterns = [];
334
+ for (const raw of text.split(/\r?\n/)) {
335
+ const line = raw.trim();
336
+ if (line.length === 0 || line.startsWith("#") || line.startsWith("!"))
337
+ continue;
338
+ patterns.push(line.replace(/\\ /g, " "));
339
+ }
340
+ if (patterns.length === 0)
341
+ continue;
342
+ sources.push({ baseDir: ignoreFileBaseDir(projectRoot, file), patterns });
343
+ }
344
+ return sources;
345
+ }
346
+ /** Ignore-file globs → check-ignore pathspecs (one per relevant rule line). */
347
+ export function derivedLifecycleIgnoreProbes(projectRoot, runGit) {
348
+ return derivedLifecycleIgnoreProbesFromSources(readIgnorePatternSources(projectRoot, runGit));
349
+ }
350
+ /** Map a check-ignore pathname back to its canonical stage root, or null if outside. */
351
+ export function lifecycleRootForRelPath(relPosix) {
352
+ const posix = relPosix.replace(/\\/g, "/").replace(/\/+$/, "");
353
+ for (const root of lifecycleRootRelPaths()) {
354
+ const prefix = root.replace(/\/+$/, "");
355
+ if (posix === prefix || posix.startsWith(`${prefix}/`))
356
+ return root;
357
+ }
358
+ return null;
359
+ }
360
+ /** Deliberate hybrid ignores under a visible root must not trip the check (#1144). */
361
+ export function isSelectiveLifecyclePath(relPosix) {
362
+ const posix = relPosix.replace(/\\/g, "/");
363
+ return posix.includes("/.triage-cache/") || posix.includes(".triage-cache/");
364
+ }
365
+ /** Map git check-ignore source paths to the actionable display form. */
366
+ export function displayIgnoreSource(source, projectRoot) {
367
+ const posix = source.replace(/\\/g, "/");
368
+ if (posix.endsWith("/info/exclude") || posix.endsWith(".git/info/exclude")) {
369
+ return ".git/info/exclude";
370
+ }
371
+ try {
372
+ const rel = relative(resolve(projectRoot), resolve(source)).replace(/\\/g, "/");
373
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
374
+ return rel;
375
+ }
376
+ }
377
+ catch {
378
+ // keep original
379
+ }
380
+ return source;
381
+ }
382
+ /**
383
+ * Parse one `git check-ignore -v` line: `<source>:<linenum>:<pattern>\\t<pathname>`.
384
+ * Source may be a Windows absolute path with a drive colon.
385
+ */
386
+ export function parseCheckIgnoreVerboseLine(line) {
387
+ const tab = line.lastIndexOf("\t");
388
+ if (tab < 0)
389
+ return null;
390
+ const meta = line.slice(0, tab);
391
+ const pathname = line.slice(tab + 1).replace(/\\/g, "/");
392
+ const match = /^(.*):(\d+):(.*)$/.exec(meta);
393
+ if (match === null)
394
+ return null;
395
+ const source = match[1] ?? "";
396
+ const lineNo = Number.parseInt(match[2] ?? "", 10);
397
+ const pattern = match[3] ?? "";
398
+ if (source.length === 0 || Number.isNaN(lineNo))
399
+ return null;
400
+ return { source, line: lineNo, pattern, path: pathname };
401
+ }
402
+ /** Parse one `git ls-files -v` record (`S path` / `h path`). */
403
+ export function parseLsFilesVerboseRecord(record) {
404
+ if (record.length < 3 || record[1] !== " ")
405
+ return null;
406
+ const tag = record[0];
407
+ if (tag === undefined)
408
+ return null;
409
+ return { tag, path: record.slice(2).replace(/\\/g, "/") };
410
+ }
411
+ /** Index flag encoded by the `ls-files -v` status tag. */
412
+ export function indexFlagKind(tag) {
413
+ if (tag === "S" || tag === "s")
414
+ return "skip-worktree";
415
+ if (tag.length === 1 && tag >= "a" && tag <= "z")
416
+ return "assume-unchanged";
417
+ return null;
418
+ }
419
+ /** Git top-level so a nested cwd cannot miss repo-root lifecycle dirs. */
420
+ function resolveScanRoot(projectRoot, runGit) {
421
+ const { code, stdout } = runGit(projectRoot, ["rev-parse", "--show-toplevel"]);
422
+ if (code === 0 && stdout.trim().length > 0) {
423
+ return resolve(stdout.trim());
424
+ }
425
+ return resolve(projectRoot);
426
+ }
427
+ function formatFinding(finding) {
428
+ if (finding.kind === "ignored") {
429
+ const loc = finding.line === null
430
+ ? `${finding.source}:${finding.rule}`
431
+ : `${finding.source}:${finding.line}:${finding.rule}`;
432
+ return ` ${finding.path} ignored by ${loc}`;
433
+ }
434
+ return ` ${finding.path} ${finding.kind} (${finding.source})`;
435
+ }
436
+ function resultFor(findings, enforce) {
437
+ if (findings.length === 0) {
438
+ return {
439
+ code: EXIT_OK,
440
+ message: `OK: verify:lifecycle-visible — lifecycle roots are not hidden by ignore rules ` +
441
+ `or index flags (enforce=${enforce}).`,
442
+ stream: "stdout",
443
+ findings: [],
444
+ enforce,
445
+ failOpen: !enforce,
446
+ };
447
+ }
448
+ const lines = [
449
+ `verify:lifecycle-visible: ${findings.length} hidden lifecycle path(s):`,
450
+ ...findings.map(formatFinding),
451
+ ];
452
+ if (enforce) {
453
+ lines.push("FAIL: --enforce is set; remove the matching ignore rule or index flag so lifecycle roots stay visible to git (#3505).");
454
+ return {
455
+ code: EXIT_ENFORCE_FINDINGS,
456
+ message: lines.join("\n"),
457
+ stream: "stderr",
458
+ findings,
459
+ enforce: true,
460
+ failOpen: false,
461
+ };
462
+ }
463
+ lines.push("ADVISORY (warn-only): exit 0. Pass --enforce to fail closed (#3505).");
464
+ return {
465
+ code: EXIT_OK,
466
+ message: lines.join("\n"),
467
+ stream: "stdout",
468
+ findings,
469
+ enforce: false,
470
+ failOpen: true,
471
+ };
472
+ }
473
+ function joinPath(root, relPosix) {
474
+ return resolve(root, ...relPosix
475
+ .replace(/\/+$/, "")
476
+ .split("/")
477
+ .filter((p) => p.length > 0));
478
+ }
479
+ /** Existing non-dot files under present stage dirs (skip .triage-cache / .gitkeep). */
480
+ function presentLifecycleFileRelPaths(projectRoot) {
481
+ const out = [];
482
+ for (const root of lifecycleRootRelPaths()) {
483
+ let entries;
484
+ try {
485
+ entries = readdirSync(joinPath(projectRoot, root), { withFileTypes: true });
486
+ }
487
+ catch {
488
+ continue;
489
+ }
490
+ for (const ent of entries) {
491
+ if (!ent.isFile() || ent.name.startsWith("."))
492
+ continue;
493
+ const rel = `${root.replace(/\/+$/, "")}/${ent.name}`;
494
+ if (isSelectiveLifecyclePath(rel))
495
+ continue;
496
+ out.push(rel);
497
+ }
498
+ }
499
+ return out;
500
+ }
501
+ /** January file sentinel only on roots that have no date-shaped derived probe. */
502
+ function emptyStageFallbackProbes(derived) {
503
+ const rootsWithDateProbe = new Set();
504
+ for (const probe of derived) {
505
+ const root = lifecycleRootForRelPath(probe);
506
+ const base = probe.split("/").pop() ?? "";
507
+ if (root !== null && /\d{4}-/.test(base))
508
+ rootsWithDateProbe.add(root);
509
+ }
510
+ if (rootsWithDateProbe.size === 0)
511
+ return lifecycleIgnoreProbeRelPaths();
512
+ const probes = [];
513
+ for (const root of lifecycleRootRelPaths()) {
514
+ probes.push(root);
515
+ if (!rootsWithDateProbe.has(root)) {
516
+ probes.push(`${root}${sentinelForRoot(root)}`);
517
+ }
518
+ }
519
+ return probes;
520
+ }
521
+ function chunkCheckIgnorePathspecs(probes) {
522
+ const overhead = "check-ignore -v --no-index --".length;
523
+ const budget = CHECK_IGNORE_ARGV_CHAR_BUDGET - overhead;
524
+ const chunks = [];
525
+ let current = [];
526
+ let used = 0;
527
+ for (const probe of probes) {
528
+ const cost = probe.length + 1;
529
+ if (current.length > 0 && used + cost > budget) {
530
+ chunks.push(current);
531
+ current = [];
532
+ used = 0;
533
+ }
534
+ current.push(probe);
535
+ used += cost;
536
+ }
537
+ if (current.length > 0)
538
+ chunks.push(current);
539
+ return chunks;
540
+ }
541
+ function checkIgnoreVerboseStdout(projectRoot, runGit, probes) {
542
+ const parts = [];
543
+ for (const chunk of chunkCheckIgnorePathspecs(probes)) {
544
+ const { code, stdout, stderr } = runGit(projectRoot, [
545
+ "check-ignore",
546
+ "-v",
547
+ "--no-index",
548
+ "--",
549
+ ...chunk,
550
+ ]);
551
+ if (code === 1)
552
+ continue;
553
+ if (code !== 0) {
554
+ throw new GitProbeError(code, stderr || "git check-ignore failed");
555
+ }
556
+ if (stdout.length > 0)
557
+ parts.push(stdout);
558
+ }
559
+ return parts.join("\n");
560
+ }
561
+ function collectIgnoredRoots(projectRoot, runGit) {
562
+ // Probe every canonical stage even when the directory is absent so an ignore
563
+ // rule cannot hide a missing xbrief/active/ before it is created (#3505).
564
+ // Directory pathspecs miss file-only globs (`xbrief/active/*.json`); a
565
+ // brief-shaped sentinel plus present files close that hole. Date-range globs
566
+ // such as `2026-07-*.xbrief.json` miss the January fallback, so emit one
567
+ // matching derived probe per ignore-rule line (no numeric cap).
568
+ const derived = derivedLifecycleIgnoreProbes(projectRoot, runGit);
569
+ const probes = [
570
+ ...new Set([
571
+ ...emptyStageFallbackProbes(derived),
572
+ ...presentLifecycleFileRelPaths(projectRoot),
573
+ ...derived,
574
+ ]),
575
+ ];
576
+ // --no-index: report the matching rule even when some files under the root
577
+ // are already tracked (the #3504 hide is untracked new briefs).
578
+ const stdout = checkIgnoreVerboseStdout(projectRoot, runGit, probes);
579
+ if (stdout.length === 0)
580
+ return [];
581
+ const findings = [];
582
+ const seen = new Set();
583
+ for (const rawLine of stdout.split(/\r?\n/)) {
584
+ if (rawLine.length === 0)
585
+ continue;
586
+ const negatedLine = rawLine.startsWith("!");
587
+ const line = negatedLine ? rawLine.slice(1) : rawLine;
588
+ const parsed = parseCheckIgnoreVerboseLine(line);
589
+ if (parsed === null)
590
+ continue;
591
+ if (negatedLine || parsed.pattern.startsWith("!"))
592
+ continue;
593
+ if (isSelectiveLifecyclePath(parsed.path))
594
+ continue;
595
+ const displayPath = lifecycleRootForRelPath(parsed.path);
596
+ if (displayPath === null)
597
+ continue;
598
+ const source = displayIgnoreSource(parsed.source, projectRoot);
599
+ const key = `${displayPath}|ignored|${source}|${parsed.line}|${parsed.pattern}`;
600
+ if (seen.has(key))
601
+ continue;
602
+ seen.add(key);
603
+ findings.push({
604
+ path: displayPath,
605
+ kind: "ignored",
606
+ source,
607
+ line: parsed.line,
608
+ rule: parsed.pattern,
609
+ raw: rawLine,
610
+ });
611
+ }
612
+ return findings;
613
+ }
614
+ function collectIndexFlags(projectRoot, runGit) {
615
+ const pathspecs = lifecycleRootRelPaths().map((rel) => rel.replace(/\/+$/, ""));
616
+ const { code, stdout, stderr } = runGit(projectRoot, ["ls-files", "-v", "--", ...pathspecs]);
617
+ if (code !== 0) {
618
+ if (code === 1 && stdout.length === 0)
619
+ return [];
620
+ throw new GitProbeError(code, stderr || "git ls-files -v failed");
621
+ }
622
+ const findings = [];
623
+ for (const record of stdout.split(/\r?\n/)) {
624
+ if (record.length === 0)
625
+ continue;
626
+ const parsed = parseLsFilesVerboseRecord(record);
627
+ if (parsed === null)
628
+ continue;
629
+ if (isSelectiveLifecyclePath(parsed.path))
630
+ continue;
631
+ const kind = indexFlagKind(parsed.tag);
632
+ if (kind === null)
633
+ continue;
634
+ findings.push({
635
+ path: parsed.path,
636
+ kind,
637
+ source: "index",
638
+ line: null,
639
+ rule: kind,
640
+ raw: record,
641
+ });
642
+ }
643
+ return findings;
644
+ }
645
+ class GitProbeError extends Error {
646
+ exitCode;
647
+ constructor(exitCode, message) {
648
+ super(message);
649
+ this.exitCode = exitCode;
650
+ }
651
+ }
652
+ /** Session:start advisory lines — silent when clean. */
653
+ export function formatLifecycleVisibleSessionLines(result) {
654
+ if (result.findings.length === 0)
655
+ return [];
656
+ const lines = result.findings.map((finding) => {
657
+ if (finding.kind === "ignored") {
658
+ const loc = finding.line === null
659
+ ? `${finding.source}:${finding.rule}`
660
+ : `${finding.source}:${finding.line}:${finding.rule}`;
661
+ return `[deft lifecycle-visible] hidden ${finding.path} (${loc})`;
662
+ }
663
+ return `[deft lifecycle-visible] ${finding.kind} on ${finding.path}`;
664
+ });
665
+ lines.push("[deft lifecycle-visible] ADVISORY: lifecycle roots must stay visible to git " +
666
+ "(warn-only; task verify:lifecycle-visible -- --enforce to fail closed).");
667
+ return lines;
668
+ }
669
+ /** Evaluate whether lifecycle roots are hidden by ignore rules or index flags (#3505). */
670
+ export function evaluateLifecycleVisible(options) {
671
+ const root = resolve(options.projectRoot);
672
+ const enforce = options.enforce === true;
673
+ if (!existsSync(root)) {
674
+ return {
675
+ code: EXIT_CONFIG,
676
+ message: `verify:lifecycle-visible: project root not found: ${root}`,
677
+ stream: "stderr",
678
+ findings: [],
679
+ enforce,
680
+ failOpen: !enforce,
681
+ };
682
+ }
683
+ const runGit = options.runGit ?? defaultGitRunner;
684
+ try {
685
+ const scanRoot = resolveScanRoot(root, runGit);
686
+ const findings = [
687
+ ...collectIgnoredRoots(scanRoot, runGit),
688
+ ...collectIndexFlags(scanRoot, runGit),
689
+ ];
690
+ return resultFor(findings, enforce);
691
+ }
692
+ catch (err) {
693
+ const detail = err instanceof Error ? err.message : String(err);
694
+ const gitCode = err instanceof GitProbeError ? err.exitCode : undefined;
695
+ const hint = gitCode === 127
696
+ ? "git executable not found on PATH"
697
+ : gitCode === 128
698
+ ? "not a git repository"
699
+ : detail;
700
+ return {
701
+ code: EXIT_CONFIG,
702
+ message: `verify:lifecycle-visible: ${hint}`,
703
+ stream: "stderr",
704
+ findings: [],
705
+ enforce,
706
+ failOpen: !enforce,
707
+ };
708
+ }
709
+ }
710
+ //# sourceMappingURL=evaluate.js.map