@deftai/directive-core 0.95.0 → 0.97.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 (116) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +636 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cache/main.js +298 -1
  6. package/dist/check/gate-lists.js +8 -0
  7. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  8. package/dist/consumer-check-contract/evaluate.js +699 -0
  9. package/dist/consumer-check-contract/index.d.ts +5 -0
  10. package/dist/consumer-check-contract/index.js +5 -0
  11. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  12. package/dist/content-contracts/skills/helpers.js +1 -0
  13. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  14. package/dist/delivery-attempt/disk-begin.js +68 -0
  15. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  16. package/dist/delivery-attempt/evaluate.js +443 -0
  17. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  18. package/dist/delivery-attempt/fingerprint.js +100 -0
  19. package/dist/delivery-attempt/handoff.d.ts +25 -0
  20. package/dist/delivery-attempt/handoff.js +102 -0
  21. package/dist/delivery-attempt/index.d.ts +17 -0
  22. package/dist/delivery-attempt/index.js +17 -0
  23. package/dist/delivery-attempt/ledger.d.ts +169 -0
  24. package/dist/delivery-attempt/ledger.js +758 -0
  25. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  26. package/dist/delivery-attempt/material-delta.js +126 -0
  27. package/dist/delivery-attempt/types.d.ts +210 -0
  28. package/dist/delivery-attempt/types.js +77 -0
  29. package/dist/doctor/index.d.ts +1 -0
  30. package/dist/doctor/index.js +1 -0
  31. package/dist/doctor/main.js +12 -0
  32. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  33. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  34. package/dist/hooks/dispatcher.d.ts +2 -1
  35. package/dist/hooks/dispatcher.js +63 -9
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +5 -0
  38. package/dist/init-deposit/gitignore.js +7 -0
  39. package/dist/init-deposit/hygiene.d.ts +1 -1
  40. package/dist/init-deposit/hygiene.js +16 -4
  41. package/dist/init-deposit/init-deposit.js +5 -0
  42. package/dist/init-deposit/refresh.js +3 -0
  43. package/dist/init-deposit/scaffold.js +6 -5
  44. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  45. package/dist/parent-turn-shape/evaluate.js +353 -0
  46. package/dist/parent-turn-shape/index.d.ts +8 -0
  47. package/dist/parent-turn-shape/index.js +8 -0
  48. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  49. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  50. package/dist/pr-merge-readiness/compute.js +10 -1
  51. package/dist/pr-merge-readiness/index.d.ts +2 -1
  52. package/dist/pr-merge-readiness/index.js +2 -1
  53. package/dist/pr-merge-readiness/output.js +14 -0
  54. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  55. package/dist/pr-merge-readiness/platform-status.js +49 -0
  56. package/dist/pr-watch/constants.d.ts +10 -0
  57. package/dist/pr-watch/constants.js +12 -1
  58. package/dist/pr-watch/main.js +16 -1
  59. package/dist/pr-watch/probe.js +13 -1
  60. package/dist/pr-watch/types.d.ts +3 -2
  61. package/dist/pr-watch/watch.js +14 -7
  62. package/dist/review-monitor/constants.js +3 -2
  63. package/dist/review-monitor/tier-detection.d.ts +7 -3
  64. package/dist/review-monitor/tier-detection.js +18 -1
  65. package/dist/review-monitor/verify.js +18 -0
  66. package/dist/scope/index.d.ts +2 -0
  67. package/dist/scope/index.js +2 -0
  68. package/dist/scope/main.d.ts +10 -0
  69. package/dist/scope/main.js +109 -24
  70. package/dist/scope/promote-from-issue.d.ts +49 -0
  71. package/dist/scope/promote-from-issue.js +367 -0
  72. package/dist/scope/promote-path.d.ts +39 -0
  73. package/dist/scope/promote-path.js +105 -0
  74. package/dist/scope-provenance/digest.d.ts +67 -0
  75. package/dist/scope-provenance/digest.js +188 -0
  76. package/dist/scope-provenance/evaluate.d.ts +82 -0
  77. package/dist/scope-provenance/evaluate.js +528 -0
  78. package/dist/scope-provenance/index.d.ts +6 -0
  79. package/dist/scope-provenance/index.js +6 -0
  80. package/dist/session/compact-ritual.d.ts +96 -0
  81. package/dist/session/compact-ritual.js +237 -0
  82. package/dist/session/compact-ritual.spec.d.ts +2 -0
  83. package/dist/session/compact-ritual.spec.js +21 -0
  84. package/dist/session/index.d.ts +2 -0
  85. package/dist/session/index.js +2 -0
  86. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  87. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  88. package/dist/swarm/routing.d.ts +4 -2
  89. package/dist/swarm/routing.js +26 -4
  90. package/dist/test-boundary/evaluate.d.ts +54 -0
  91. package/dist/test-boundary/evaluate.js +368 -0
  92. package/dist/test-boundary/index.d.ts +6 -0
  93. package/dist/test-boundary/index.js +6 -0
  94. package/dist/test-boundary/policy.d.ts +52 -0
  95. package/dist/test-boundary/policy.js +182 -0
  96. package/dist/triage/actions/index.js +62 -2
  97. package/dist/triage/actions/types.d.ts +8 -1
  98. package/dist/triage/author-filter.d.ts +51 -0
  99. package/dist/triage/author-filter.js +152 -0
  100. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  101. package/dist/triage/bootstrap/gitignore.js +15 -1
  102. package/dist/triage/classify/index.d.ts +2 -2
  103. package/dist/triage/classify/index.js +2 -2
  104. package/dist/triage/classify/label-mirror.d.ts +68 -5
  105. package/dist/triage/classify/label-mirror.js +261 -31
  106. package/dist/triage/help/registry-data.d.ts +49 -38
  107. package/dist/triage/help/registry-data.js +115 -40
  108. package/dist/triage/index.d.ts +1 -0
  109. package/dist/triage/index.js +1 -0
  110. package/dist/triage/queue/index.d.ts +1 -0
  111. package/dist/triage/queue/index.js +1 -0
  112. package/dist/triage/queue/render.d.ts +2 -0
  113. package/dist/triage/queue/render.js +6 -0
  114. package/dist/vbrief-activate/activate.js +22 -6
  115. package/dist/xbrief/styles.js +33 -17
  116. package/package.json +11 -3
@@ -0,0 +1,699 @@
1
+ /**
2
+ * verify:consumer-check-contract evaluation (#3145).
3
+ *
4
+ * Fail closed when the consumer check task or required CI workflows omit
5
+ * configured Directive enforcement gates (test-boundary, scope-provenance,
6
+ * and other required surfaces). Emits a concrete repair path.
7
+ *
8
+ * Three-state exit: 0 clean / 1 missing gates / 2 config.
9
+ */
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+ import { taskDefinedInTaskfileYaml } from "../check/consumer-gate-integrity.js";
13
+ import { CONSUMER_CHECK_GATES, checkGateId } from "../check/gate-lists.js";
14
+ /** Gates that #3145 requires on consumer check composition (beyond baseline). */
15
+ export const REQUIRED_CONSUMER_ENFORCEMENT_GATES = [
16
+ "verify:test-boundary",
17
+ "verify:scope-provenance",
18
+ "verify:consumer-check-contract",
19
+ ];
20
+ function readText(path) {
21
+ try {
22
+ if (!existsSync(path))
23
+ return null;
24
+ return readFileSync(path, "utf8");
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /** Escape a string for safe inclusion in a RegExp source (CodeQL js/incomplete-sanitization). */
31
+ function escapeRegExp(s) {
32
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33
+ }
34
+ /** True if `text` mentions the gate as a task dep, script, or CLI invocation. */
35
+ export function textReferencesGate(text, gateId) {
36
+ const forms = [
37
+ gateId,
38
+ gateId.replace(":", "-"),
39
+ `task ${gateId}`,
40
+ `deft ${gateId}`,
41
+ `directive ${gateId}`,
42
+ ];
43
+ const lower = text.toLowerCase();
44
+ return forms.some((f) => lower.includes(f.toLowerCase()));
45
+ }
46
+ /**
47
+ * True when a shell line is only assignment(s) (no executed command).
48
+ * `CHECK_CMD="task check"` is pure assignment; `FOO=1 task check` is not.
49
+ * Linear scan (no nested regex quantifiers — CodeQL js/redos).
50
+ */
51
+ export function isPureAssignmentLine(cmd) {
52
+ let t = cmd.trim();
53
+ if (t.length === 0)
54
+ return false;
55
+ if (/(?:&&|\|\||;)/.test(t))
56
+ return false;
57
+ // export NAME=value
58
+ if (/^export\s+/i.test(t)) {
59
+ t = t.replace(/^export\s+/i, "").trim();
60
+ }
61
+ // Consume one or more NAME=value tokens; refuse leftover command words.
62
+ let i = 0;
63
+ let sawAssign = false;
64
+ while (i < t.length) {
65
+ while (i < t.length && /\s/.test(t[i] ?? ""))
66
+ i += 1;
67
+ if (i >= t.length)
68
+ break;
69
+ // NAME=
70
+ const nameMatch = /^[A-Za-z_][\w]*=/.exec(t.slice(i));
71
+ if (nameMatch === null) {
72
+ // leftover command → env-prefix form (not pure)
73
+ return false;
74
+ }
75
+ i += nameMatch[0].length;
76
+ sawAssign = true;
77
+ const ch = t[i];
78
+ if (ch === '"' || ch === "'") {
79
+ const q = ch;
80
+ i += 1;
81
+ while (i < t.length && t[i] !== q)
82
+ i += 1;
83
+ if (i >= t.length)
84
+ return false; // unclosed quote
85
+ i += 1; // closing quote
86
+ }
87
+ else {
88
+ // bare value until whitespace
89
+ while (i < t.length && !/\s/.test(t[i] ?? ""))
90
+ i += 1;
91
+ }
92
+ }
93
+ return sawAssign;
94
+ }
95
+ /**
96
+ * True when a shell/task line does not execute gates (echo, comment, pure assign).
97
+ * Greptile conf gate: non-executing text must not satisfy composition (#3145).
98
+ */
99
+ export function isNonExecutingCommandLine(line) {
100
+ const stripped = line.trim();
101
+ if (stripped.length === 0)
102
+ return true;
103
+ if (stripped.startsWith("#"))
104
+ return true;
105
+ // go-task list item: - echo "..."
106
+ const cmd = stripped.replace(/^-\s+/, "");
107
+ if (/^echo\b/i.test(cmd))
108
+ return true;
109
+ // Pure assignment even when RHS contains runner phrases (Greptile conf=1).
110
+ if (isPureAssignmentLine(cmd))
111
+ return true;
112
+ return false;
113
+ }
114
+ /** Executable run/script command lines from a workflow (skips comments). */
115
+ export function extractWorkflowRunCommands(text) {
116
+ const out = [];
117
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
118
+ for (let i = 0; i < lines.length; i += 1) {
119
+ const raw = lines[i] ?? "";
120
+ const stripped = raw.trim();
121
+ if (!stripped || stripped.startsWith("#"))
122
+ continue;
123
+ const m = stripped.match(/^(?:-\s*)?(?:run|script)\s*:\s*(.*)$/i);
124
+ if (m === null)
125
+ continue;
126
+ const rest = (m[1] ?? "").trim();
127
+ // Block scalar: run: | or run: >
128
+ if (rest === "|" || rest === ">" || rest.startsWith("|") || rest.startsWith(">")) {
129
+ const indent = raw.length - raw.trimStart().length;
130
+ const body = [];
131
+ let bodyBaseIndent = null;
132
+ for (let j = i + 1; j < lines.length; j += 1) {
133
+ const lr = lines[j] ?? "";
134
+ if (lr.trim().length === 0) {
135
+ body.push("");
136
+ continue;
137
+ }
138
+ const li = lr.length - lr.trimStart().length;
139
+ if (li <= indent)
140
+ break;
141
+ if (bodyBaseIndent === null)
142
+ bodyBaseIndent = li;
143
+ body.push(lr.slice(bodyBaseIndent));
144
+ i = j;
145
+ }
146
+ const joined = body.join("\n").trim();
147
+ if (joined.length > 0)
148
+ out.push(joined);
149
+ continue;
150
+ }
151
+ if (rest.length > 0) {
152
+ // Only strip outer wrapping quotes on the whole scalar — not a trailing
153
+ // quote that closes an assignment value (CHECK_CMD="task check").
154
+ let scalar = rest;
155
+ if ((scalar.startsWith('"') && scalar.endsWith('"') && scalar.length >= 2) ||
156
+ (scalar.startsWith("'") && scalar.endsWith("'") && scalar.length >= 2)) {
157
+ scalar = scalar.slice(1, -1);
158
+ }
159
+ out.push(scalar.trim());
160
+ }
161
+ }
162
+ return out;
163
+ }
164
+ /**
165
+ * Strip simple single/double-quoted segments so runner phrases inside printf/args
166
+ * do not impersonate command-position invocations (Greptile conf=1).
167
+ */
168
+ export function stripQuotedSegments(line) {
169
+ return line.replace(/'[^']*'|"[^"]*"/g, " ");
170
+ }
171
+ /**
172
+ * True when a shell line discards or defers the primary command's exit status
173
+ * via `||`, pipelines, `;` chains, or job-control backgrounding. Used for
174
+ * dispatcher tokens that do not match the check-specific mask predicate.
175
+ */
176
+ export function lineMasksShellStatus(line) {
177
+ const lower = stripQuotedSegments(line).toLowerCase();
178
+ if (/\|\|/.test(lower))
179
+ return true;
180
+ // Pipeline `|` but not `||` (already handled) and not fd redirs `2>&1` / `>&2`.
181
+ const noFd = lower
182
+ .replace(/\d*>&?\d+/g, " ")
183
+ .replace(/>&\d+/g, " ")
184
+ .replace(/&>\S*/g, " ");
185
+ if (/\|/.test(noFd))
186
+ return true;
187
+ if (/;/.test(lower))
188
+ return true;
189
+ if (lineHasBackgroundJob(line))
190
+ return true;
191
+ return false;
192
+ }
193
+ /**
194
+ * True when a shell line uses job-control backgrounding (`cmd &`, `cmd & next`)
195
+ * as opposed to fd redirection (`2>&1`, `&>file`). Backgrounded jobs return
196
+ * before completion and do not propagate exit status — must not satisfy
197
+ * composition (Greptile conf=3/4 residual, #3145).
198
+ */
199
+ export function lineHasBackgroundJob(line) {
200
+ const s = stripQuotedSegments(line);
201
+ // Strip common fd redirections that contain `&` without job control.
202
+ const withoutRedir = s
203
+ .replace(/\d*>&?\d+/g, " ")
204
+ .replace(/&>\S*/g, " ")
205
+ .replace(/>&\d+/g, " ");
206
+ // Bare `&` that is not part of `&&`: trailing, or followed by another token.
207
+ if (/(?:^|[^&])&\s*(?:#.*)?$/.test(withoutRedir))
208
+ return true;
209
+ if (/(?:^|[^&])&\s+\S/.test(withoutRedir))
210
+ return true;
211
+ return false;
212
+ }
213
+ /**
214
+ * True when a shell line masks check failures (`|| true`, `| true`, `|| :`).
215
+ * Such lines must not satisfy composition (Greptile conf=3).
216
+ */
217
+ export function lineMasksCheckFailure(line) {
218
+ const lower = stripQuotedSegments(line).toLowerCase();
219
+ const checkish = /(?:deft|directive)\s+check\b|task\s+(?:deft:)?check\b|(?:task\s+)?check:(?:consumer|framework-source)\b/;
220
+ if (!checkish.test(lower))
221
+ return false;
222
+ // Any pipeline after a check can hide nonzero status without pipefail (Greptile).
223
+ if (/check\b[\s\S]*\|/.test(lower))
224
+ return true;
225
+ // Any `|| fallback` after a check discards the check's exit status (Greptile conf=4).
226
+ // Includes `|| true`, `|| :`, `|| echo ignored`, `|| exit 0`, etc.
227
+ if (/check\b[\s\S]*\|\|/.test(lower))
228
+ return true;
229
+ // Semicolon chain after check: `deft check; echo ignored` masks failure (Greptile conf=2).
230
+ if (/check\b[\s\S]*;/.test(lower))
231
+ return true;
232
+ // Background job after check (including mid-line `& echo done`).
233
+ if (lineHasBackgroundJob(line))
234
+ return true;
235
+ return false;
236
+ }
237
+ /**
238
+ * True when `line` invokes a runner at command position (start / after chain),
239
+ * not merely mentions it inside an argument, and does not mask failures.
240
+ */
241
+ export function lineHasCommandPositionRunner(line, runnerPattern) {
242
+ if (lineMasksCheckFailure(line))
243
+ return false;
244
+ // Backgrounded gates/runners are not awaited (covers verify:* & forms too).
245
+ if (lineHasBackgroundJob(line))
246
+ return false;
247
+ const lower = stripQuotedSegments(line).toLowerCase().trim().replace(/^-\s+/, "");
248
+ if (lower.length === 0)
249
+ return false;
250
+ // Known argument-only tools never execute our gates.
251
+ if (/^(?:printf|print|echo|cat|true|false|:|test|\[)\b/.test(lower))
252
+ return false;
253
+ // Command position: line start or after shell chain / env-prefix assignments.
254
+ const withoutEnv = lower.replace(/^(?:[a-z_][\w]*=(?:\S+)\s+)*/i, "");
255
+ if (runnerPattern.test(withoutEnv))
256
+ return true;
257
+ // Chained: ... && task check (but not || true — already rejected above)
258
+ const chainParts = withoutEnv.split(/(?:&&|\|\||;)/);
259
+ return chainParts.some((part) => runnerPattern.test(part.trim()));
260
+ }
261
+ /** True when a run command is a full check aggregate (not a single verify gate). */
262
+ export function runCommandIsFullCheck(cmd) {
263
+ // Scan executable lines only — echo/docs/printf-args do not execute gates.
264
+ for (const raw of cmd.split("\n")) {
265
+ if (isNonExecutingCommandLine(raw))
266
+ continue;
267
+ const line = raw.trim().replace(/^-\s+/, "");
268
+ // Word-boundary / command-position matches only.
269
+ if (lineHasCommandPositionRunner(line, /^(?:sudo\s+)?(?:deft|directive)\s+check\b/)) {
270
+ return true;
271
+ }
272
+ if (lineHasCommandPositionRunner(line, /^(?:sudo\s+)?task\s+(?:deft:)?check\b/)) {
273
+ return true;
274
+ }
275
+ if (lineHasCommandPositionRunner(line, /^(?:sudo\s+)?(?:task\s+)?check:consumer(?:\s|$)/)) {
276
+ return true;
277
+ }
278
+ if (lineHasCommandPositionRunner(line, /^(?:sudo\s+)?(?:task\s+)?check:framework-source(?:\s|$)/)) {
279
+ return true;
280
+ }
281
+ }
282
+ return false;
283
+ }
284
+ /**
285
+ * True when a run command invokes a specific verify gate id as an executable
286
+ * task/CLI form — not a bare substring in echo/docs/printf args.
287
+ */
288
+ export function runCommandInvokesGate(cmd, gateId) {
289
+ const gid = gateId.toLowerCase();
290
+ const dashed = gateId.replace(":", "-").toLowerCase();
291
+ const invokers = [
292
+ new RegExp(`^(?:sudo\\s+)?(?:task|deft|directive)\\s+(?:deft:)?${escapeRegExp(gid)}\\b`, "i"),
293
+ new RegExp(`^(?:sudo\\s+)?(?:task|deft|directive)\\s+(?:deft:)?${escapeRegExp(dashed)}\\b`, "i"),
294
+ new RegExp(`^(?:sudo\\s+)?npm\\s+run\\s+${escapeRegExp(gid)}\\b`, "i"),
295
+ ];
296
+ for (const raw of cmd.split("\n")) {
297
+ if (isNonExecutingCommandLine(raw))
298
+ continue;
299
+ const line = raw.trim().replace(/^-\s+/, "");
300
+ if (invokers.some((re) => lineHasCommandPositionRunner(line, re)))
301
+ return true;
302
+ }
303
+ return false;
304
+ }
305
+ /**
306
+ * True when CI/workflow text has an executable `run:` line that invokes the full
307
+ * check aggregate (not a single gate, not prose) — Greptile P1.
308
+ */
309
+ export function workflowExecutesCheck(text) {
310
+ return extractWorkflowRunCommands(text).some(runCommandIsFullCheck);
311
+ }
312
+ /** Extract the body of a top-level go-task task (indent=2 key). */
313
+ export function extractTaskBody(taskfileText, taskName) {
314
+ const lines = taskfileText.replace(/\r\n/g, "\n").split("\n");
315
+ const body = [];
316
+ let inTask = false;
317
+ let taskIndent = 0;
318
+ const taskRe = new RegExp(`^${escapeRegExp(taskName)}\\s*:`);
319
+ for (const raw of lines) {
320
+ const stripped = raw.trim();
321
+ if (!stripped || stripped.startsWith("#")) {
322
+ if (inTask)
323
+ body.push(raw);
324
+ continue;
325
+ }
326
+ const indent = raw.length - raw.trimStart().length;
327
+ if (!inTask) {
328
+ if (taskRe.test(stripped) && indent <= 2) {
329
+ inTask = true;
330
+ taskIndent = indent;
331
+ }
332
+ continue;
333
+ }
334
+ if (indent <= taskIndent && /^[\w:-]+:/.test(stripped)) {
335
+ break;
336
+ }
337
+ body.push(raw);
338
+ }
339
+ return body.join("\n");
340
+ }
341
+ /**
342
+ * Drop YAML/shell comment lines (and trailing `# ...` on otherwise empty marker
343
+ * lines) before orchestrator matching so commented markers cannot grant trust.
344
+ */
345
+ export function stripTaskBodyComments(body) {
346
+ const out = [];
347
+ for (const raw of body.split("\n")) {
348
+ const stripped = raw.trim();
349
+ if (!stripped || stripped.startsWith("#"))
350
+ continue;
351
+ // Full-line list item that is only a comment: - # ...
352
+ if (/^-\s+#/.test(stripped))
353
+ continue;
354
+ out.push(raw);
355
+ }
356
+ return out.join("\n");
357
+ }
358
+ /**
359
+ * True when a Taskfile task body actually dispatches the check orchestrator.
360
+ * ENGINE_CMD alone is inert metadata — require engine:invoke / dispatchTaskCheck
361
+ * (or a command-position runner) so inert YAML cannot grant trust (Greptile).
362
+ * Comments are stripped first so commented markers cannot bypass (Greptile conf=3).
363
+ */
364
+ export function taskBodyInvokesCheckOrchestrator(body) {
365
+ if (body.trim().length === 0)
366
+ return false;
367
+ const live = stripTaskBodyComments(body);
368
+ if (live.trim().length === 0)
369
+ return false;
370
+ let hasEngineInvoke = false;
371
+ let hasDispatch = false;
372
+ let hasEngineCheckCmd = false;
373
+ let hasCommandRunner = false;
374
+ for (const raw of live.split("\n")) {
375
+ const stripped = raw.trim();
376
+ if (!stripped)
377
+ continue;
378
+ if (isNonExecutingCommandLine(stripped))
379
+ continue;
380
+ if (lineMasksCheckFailure(stripped))
381
+ continue;
382
+ // YAML go-task form only: "- task: engine:invoke" (not inert ENGINE_INVOKE: keys).
383
+ if (/^(?:-\s*)?task:\s*engine:invoke\b/.test(stripped)) {
384
+ hasEngineInvoke = true;
385
+ }
386
+ // Exact YAML key form (not ENGINE_CMD_DOCS: or assignment noise).
387
+ if (/^ENGINE_CMD:\s*['"]?check\b/.test(stripped)) {
388
+ hasEngineCheckCmd = true;
389
+ }
390
+ // dispatchTaskCheck only as the command being invoked (not an arbitrary arg).
391
+ const cmd = stripped.replace(/^-\s+/, "");
392
+ const unquoted = stripQuotedSegments(cmd).trim();
393
+ // Forms: dispatchTaskCheck ... | node path/dispatchTaskCheck.js | npx ...dispatchTaskCheck
394
+ // Failure-masked dispatchers (`|| true`, pipelines, `;`, background) must not grant
395
+ // orchestrator trust — same class as masked `task check` (Greptile conf=2, #3145).
396
+ if (!lineMasksShellStatus(cmd) &&
397
+ (/^(?:sudo\s+)?dispatchTaskCheck\b/.test(unquoted) ||
398
+ /^(?:sudo\s+)?(?:node|npx)\s+\S*dispatchTaskCheck\S*/.test(unquoted))) {
399
+ hasDispatch = true;
400
+ }
401
+ if (lineHasCommandPositionRunner(cmd, /^(?:sudo\s+)?(?:deft|directive)\s+check\b/) ||
402
+ lineHasCommandPositionRunner(cmd, /^(?:sudo\s+)?task\s+(?:deft:)?check\b/)) {
403
+ hasCommandRunner = true;
404
+ }
405
+ }
406
+ // Framework Taskfile pattern: task: engine:invoke + ENGINE_CMD: 'check ...'
407
+ if (hasEngineInvoke && hasEngineCheckCmd)
408
+ return true;
409
+ if (hasDispatch && hasEngineCheckCmd)
410
+ return true;
411
+ if (hasDispatch)
412
+ return true;
413
+ if (hasCommandRunner)
414
+ return true;
415
+ return false;
416
+ }
417
+ /** True when the check / check:consumer / check:framework-source task invokes orchestrator. */
418
+ export function taskfileInvokesCheckOrchestrator(text) {
419
+ for (const taskName of ["check", "check:consumer", "check:framework-source"]) {
420
+ const body = extractTaskBody(text, taskName);
421
+ if (taskBodyInvokesCheckOrchestrator(body))
422
+ return true;
423
+ }
424
+ return false;
425
+ }
426
+ /**
427
+ * Extract dependency task names from a go-task task definition body snippet.
428
+ * Best-effort line parser for `deps:` list entries.
429
+ */
430
+ export function extractCheckDeps(taskfileText, taskName) {
431
+ const lines = taskfileText.replace(/\r\n/g, "\n").split("\n");
432
+ const deps = [];
433
+ let inTask = false;
434
+ let taskIndent = 0;
435
+ let inDeps = false;
436
+ let depsIndent = 0;
437
+ const taskRe = new RegExp(`^${escapeRegExp(taskName)}\\s*:`);
438
+ for (const raw of lines) {
439
+ const stripped = raw.trim();
440
+ if (!stripped || stripped.startsWith("#"))
441
+ continue;
442
+ const indent = raw.length - raw.trimStart().length;
443
+ if (!inTask) {
444
+ if (taskRe.test(stripped) && indent === 2) {
445
+ inTask = true;
446
+ taskIndent = indent;
447
+ }
448
+ continue;
449
+ }
450
+ if (indent <= taskIndent && !stripped.startsWith("#") && /^[\w-]+:/.test(stripped)) {
451
+ break;
452
+ }
453
+ if (/^deps\s*:/.test(stripped)) {
454
+ inDeps = true;
455
+ depsIndent = indent;
456
+ continue;
457
+ }
458
+ if (inDeps) {
459
+ if (indent <= depsIndent && !stripped.startsWith("-")) {
460
+ inDeps = false;
461
+ }
462
+ else if (stripped.startsWith("-")) {
463
+ // - verify:branch OR - task: verify:branch
464
+ const m = stripped.match(/^-\s+task:\s*["']?([^"'#]+?)["']?\s*(?:#.*)?$/) ??
465
+ stripped.match(/^-\s+["']?([^"'#]+?)["']?\s*(?:#.*)?$/);
466
+ if (m?.[1]) {
467
+ deps.push(m[1].trim());
468
+ }
469
+ }
470
+ }
471
+ }
472
+ return deps;
473
+ }
474
+ function remediationForMissing(gateId, surface) {
475
+ return (`Add \`${gateId}\` to the consumer ${surface} composition (Taskfile check deps ` +
476
+ `and/or CI workflow invoking \`deft check\` / \`task ${gateId}\`). ` +
477
+ "See content/docs/consumer-check-contract.md (#3145).");
478
+ }
479
+ function configError(message) {
480
+ return { exitCode: 2, findings: [], message };
481
+ }
482
+ /**
483
+ * Evaluate whether required enforcement gates are composed into consumer check/CI.
484
+ */
485
+ export function evaluateConsumerCheckContract(projectRoot, options = {}) {
486
+ const root = resolve(projectRoot);
487
+ const required = options.requiredGates ?? REQUIRED_CONSUMER_ENFORCEMENT_GATES;
488
+ const ciWarnOnly = options.ciWarnOnly ?? true;
489
+ const enforce = options.enforce ?? true;
490
+ const rootTfPath = join(root, "Taskfile.yml");
491
+ const verifyTfPath = join(root, "tasks", "verify.yml");
492
+ const rootText = options.rootTaskfileText !== undefined ? options.rootTaskfileText : readText(rootTfPath);
493
+ const verifyText = options.verifyTaskfileText !== undefined ? options.verifyTaskfileText : readText(verifyTfPath);
494
+ // If no Taskfile at all, config error only when enforce (else skip clean for non-task projects)
495
+ if (rootText === null && options.rootTaskfileText === undefined) {
496
+ if (!existsSync(rootTfPath) && options.frameworkSource !== true) {
497
+ return {
498
+ exitCode: 0,
499
+ findings: [],
500
+ message: "verify_consumer_check_contract: no Taskfile.yml found -- skipped (not a task-based consumer).",
501
+ };
502
+ }
503
+ }
504
+ if (rootText === null) {
505
+ return configError("verify_consumer_check_contract: Taskfile.yml unreadable.\n" +
506
+ " Recovery: restore Taskfile.yml or run from the project root.");
507
+ }
508
+ // Narrow for nested helpers (TS does not carry null-check into closures).
509
+ const rootTaskfile = rootText;
510
+ const findings = [];
511
+ const soft = [];
512
+ // 1) verify.yml must define the local task keys for required gates
513
+ if (verifyText !== null) {
514
+ for (const gateId of required) {
515
+ if (!gateId.startsWith("verify:"))
516
+ continue;
517
+ const local = gateId.slice("verify:".length);
518
+ if (!taskDefinedInTaskfileYaml(verifyText, local)) {
519
+ findings.push({
520
+ gateId,
521
+ surface: "verify-taskfile",
522
+ detail: `tasks/verify.yml does not define task '${local}' for ${gateId}`,
523
+ remediation: remediationForMissing(gateId, "tasks/verify.yml"),
524
+ });
525
+ }
526
+ }
527
+ }
528
+ else if (options.frameworkSource === true || existsSync(join(root, "tasks"))) {
529
+ findings.push({
530
+ gateId: "verify:*",
531
+ surface: "verify-taskfile",
532
+ detail: "tasks/verify.yml missing while tasks/ directory or framework-source expected",
533
+ remediation: "Restore tasks/verify.yml via `deft update` deposit repair. See UPGRADING.md (#3145).",
534
+ });
535
+ }
536
+ // 2) Each defined check aggregate must compose required gates OR invoke full deft check.
537
+ // Do NOT union deps across check / check:consumer / check:framework-source — a sibling
538
+ // with full deps must not conceal omissions on the aggregate that actually runs (Greptile).
539
+ const checkTargets = ["check", "check:consumer", "check:framework-source"];
540
+ const verifyDefinesRequired = verifyText !== null &&
541
+ required.every((gateId) => {
542
+ if (!gateId.startsWith("verify:"))
543
+ return true;
544
+ return taskDefinedInTaskfileYaml(verifyText, gateId.slice("verify:".length));
545
+ });
546
+ /** Exact dep match only — never substring (noop-verify:test-boundary must not count). */
547
+ function depListsGate(deps, gateId) {
548
+ for (const d of deps) {
549
+ const name = d.trim();
550
+ if (name === gateId)
551
+ return true;
552
+ // go-task object form may normalize to bare task name already via extractCheckDeps
553
+ if (name === `task ${gateId}` || name === `task:${gateId}`)
554
+ return true;
555
+ }
556
+ return false;
557
+ }
558
+ /** True when this task body alone invokes the check orchestrator (not whole file). */
559
+ function taskInvokesOrchestrator(taskName) {
560
+ return taskBodyInvokesCheckOrchestrator(extractTaskBody(rootTaskfile, taskName));
561
+ }
562
+ let anyAggregateDefined = false;
563
+ for (const target of checkTargets) {
564
+ const deps = extractCheckDeps(rootTaskfile, target);
565
+ const body = extractTaskBody(rootTaskfile, target);
566
+ const defined = deps.length > 0 || body.trim().length > 0;
567
+ if (!defined)
568
+ continue;
569
+ anyAggregateDefined = true;
570
+ const trustThis = taskInvokesOrchestrator(target) && verifyDefinesRequired;
571
+ if (trustThis)
572
+ continue;
573
+ for (const gateId of required) {
574
+ if (!depListsGate(deps, gateId)) {
575
+ findings.push({
576
+ gateId,
577
+ surface: "check-task",
578
+ detail: deps.length === 0
579
+ ? `aggregate '${target}' has no deps and does not invoke a full check orchestrator covering ${gateId}`
580
+ : `aggregate '${target}' deps do not include ${gateId}`,
581
+ remediation: remediationForMissing(gateId, `check task (${target})`),
582
+ });
583
+ }
584
+ }
585
+ }
586
+ // No check aggregates defined at all — still a composition gap when enforce
587
+ // (unless trust via whole-file orchestrator is impossible without a body).
588
+ if (!anyAggregateDefined) {
589
+ // Preserve prior behavior for empty/missing check tasks: fail each required gate.
590
+ const invokesAny = taskfileInvokesCheckOrchestrator(rootTaskfile);
591
+ if (!(invokesAny && verifyDefinesRequired)) {
592
+ for (const gateId of required) {
593
+ findings.push({
594
+ gateId,
595
+ surface: "check-task",
596
+ detail: `check aggregate has no deps and does not invoke a full check orchestrator covering ${gateId}`,
597
+ remediation: remediationForMissing(gateId, "check task"),
598
+ });
599
+ }
600
+ }
601
+ }
602
+ // Framework source: FRAMEWORK_CHECK_GATES should include the trio when enforce
603
+ if (options.frameworkSource === true) {
604
+ const baselineIds = CONSUMER_CHECK_GATES.map(checkGateId);
605
+ void baselineIds;
606
+ // Framework wiring is validated via verify.yml definitions (step 1) + gate-lists unit tests.
607
+ }
608
+ // 3) CI workflows (soft by default)
609
+ let workflows;
610
+ if (options.ciWorkflows !== undefined) {
611
+ workflows = options.ciWorkflows;
612
+ }
613
+ else {
614
+ const map = new Map();
615
+ const wfDir = join(root, ".github", "workflows");
616
+ if (existsSync(wfDir)) {
617
+ for (const name of readdirSync(wfDir)) {
618
+ if (!/\.ya?ml$/i.test(name))
619
+ continue;
620
+ const text = readText(join(wfDir, name));
621
+ if (text !== null)
622
+ map.set(`.github/workflows/${name}`, text);
623
+ }
624
+ }
625
+ workflows = map;
626
+ }
627
+ if (workflows.size > 0) {
628
+ const allCi = [...workflows.values()].join("\n");
629
+ const runCmds = extractWorkflowRunCommands(allCi);
630
+ const fullCheck = runCmds.some(runCommandIsFullCheck) &&
631
+ verifyText !== null &&
632
+ required.every((gateId) => {
633
+ if (!gateId.startsWith("verify:"))
634
+ return true;
635
+ return taskDefinedInTaskfileYaml(verifyText, gateId.slice("verify:".length));
636
+ });
637
+ for (const gateId of required) {
638
+ const direct = runCmds.some((c) => runCommandInvokesGate(c, gateId));
639
+ // Single gate does NOT satisfy the whole trio (Greptile P1).
640
+ const mentioned = direct || fullCheck;
641
+ if (!mentioned) {
642
+ const finding = {
643
+ gateId,
644
+ surface: "ci-workflow",
645
+ detail: `CI workflows do not invoke ${gateId} or a full check entrypoint that composes it`,
646
+ remediation: remediationForMissing(gateId, "CI workflow"),
647
+ };
648
+ if (ciWarnOnly)
649
+ soft.push(finding);
650
+ else
651
+ findings.push(finding);
652
+ }
653
+ }
654
+ }
655
+ if (findings.length === 0 && soft.length === 0) {
656
+ return {
657
+ exitCode: 0,
658
+ findings: [],
659
+ message: `verify_consumer_check_contract: clean (${required.length} required gate(s) composed) (#3145).`,
660
+ };
661
+ }
662
+ if (findings.length === 0) {
663
+ const body = soft
664
+ .map((f) => ` ${f.gateId} [${f.surface}]\n ${f.detail}\n remediation: ${f.remediation}`)
665
+ .join("\n");
666
+ return {
667
+ exitCode: 0,
668
+ findings: soft,
669
+ message: `verify_consumer_check_contract: WARN ${soft.length} CI composition note(s) ` +
670
+ `(not failing) (#3145).\n${body}`,
671
+ };
672
+ }
673
+ if (!enforce) {
674
+ const all = [...findings, ...soft];
675
+ const body = all
676
+ .map((f) => ` ${f.gateId} [${f.surface}]\n ${f.detail}\n remediation: ${f.remediation}`)
677
+ .join("\n");
678
+ return {
679
+ exitCode: 0,
680
+ findings: all,
681
+ message: `verify_consumer_check_contract: WARN ${all.length} finding(s) (enforce off) (#3145).\n${body}`,
682
+ };
683
+ }
684
+ const all = [...findings, ...soft];
685
+ const body = all
686
+ .map((f) => ` ${f.gateId} [${f.surface}]\n ${f.detail}\n remediation: ${f.remediation}`)
687
+ .join("\n");
688
+ return {
689
+ exitCode: 1,
690
+ findings: all,
691
+ message: `verify_consumer_check_contract: ${findings.length} missing required gate(s) (#3145).\n${body}`,
692
+ };
693
+ }
694
+ /** Exported for tests — gate list helper. */
695
+ export function requiredGatesFromConsumerList(gates = CONSUMER_CHECK_GATES) {
696
+ const ids = gates.map(checkGateId);
697
+ return REQUIRED_CONSUMER_ENFORCEMENT_GATES.filter((g) => ids.includes(g));
698
+ }
699
+ //# sourceMappingURL=evaluate.js.map