arkgate 4.8.23 → 4.8.25

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 (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -6,6 +6,9 @@ import path from 'node:path';
6
6
  /** Kill hung gh instead of stalling CI. */
7
7
  export const SPAWN_TIMEOUT_MS = 8000;
8
8
 
9
+ /** GitHub Actions app id: a required check bound to it can only be reported by a workflow job. */
10
+ export const GITHUB_ACTIONS_APP_ID = 15368;
11
+
9
12
  function runGh(args, opts = {}) {
10
13
  const { run, ...rest } = opts;
11
14
  const spawn = typeof run === 'function' ? run : spawnSync;
@@ -18,11 +21,11 @@ function runGh(args, opts = {}) {
18
21
 
19
22
  const IF_LINE = /^[ \t]*(?:-\s+)?(?:"if"|'if'|if):\s*(.*?)\s*(?:#.*)?$/i;
20
23
  const CONTINUE_LINE = /^[ \t]*(?:-\s+)?(?:"continue-on-error"|'continue-on-error'|continue-on-error):\s*(.*?)\s*(?:#.*)?$/i;
21
- const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]?)$/i;
22
- const SAFE_NEEDS_IF = /^['"]?\$\{\{\s*always\(\)\s*\}\}['"]?$/i;
24
+ const SAFE_IF = /^(?:['"]?true['"]?|['"]?(?:\$\{\{\s*(?:true|always\(\))\s*\}\}|always\(\))['"]?)$/i;
25
+ const SAFE_NEEDS_IF = /^['"]?(?:\$\{\{\s*always\(\)\s*\}\}|always\(\))['"]?$/i;
23
26
  const SAFE_CONTINUE = /^['"]?false['"]?$/i;
24
27
  const ENV_PREFIX = /^(?:(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)/;
25
- const DIRECT_ARK = /^(?:(?:npx|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
28
+ const DIRECT_ARK = /^(?:(?:npx(?:\s+(?:-y|--yes))?(?:\s+(?:-p\s+|--package[\s=])arkgate(?:@\S+)?)?|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
26
29
  const CHECK_SCRIPT = /^(?:(?:npm|pnpm)\s+run\s+check:architecture(?=\s|$)|yarn(?:\s+run)?\s+check:architecture(?=\s|$))/;
27
30
  const FAIL_CLOSED = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
28
31
 
@@ -164,7 +167,7 @@ function propertyValue(line, pattern) {
164
167
  return line.match(pattern)?.[1]?.trim() ?? null;
165
168
  }
166
169
 
167
- function unsafeControl(lines, propertyIndent, needs = false) {
170
+ function controlValues(lines, propertyIndent) {
168
171
  const atIndent = lines.filter((line) => {
169
172
  const indent = line.match(/^\s*/)?.[0].length ?? 0;
170
173
  return indent + (/^\s*-\s+/.test(line) ? 2 : 0) === propertyIndent;
@@ -173,9 +176,13 @@ function unsafeControl(lines, propertyIndent, needs = false) {
173
176
  const continuation = atIndent
174
177
  .map((line) => propertyValue(line, CONTINUE_LINE))
175
178
  .find((value) => value !== null);
179
+ return { condition, continuation };
180
+ }
181
+
182
+ function unsafeControl(lines, propertyIndent) {
183
+ const { condition, continuation } = controlValues(lines, propertyIndent);
176
184
  if (condition !== undefined && !SAFE_IF.test(condition)) return true;
177
- if (continuation !== undefined && !SAFE_CONTINUE.test(continuation)) return true;
178
- return needs && (condition === undefined || !SAFE_NEEDS_IF.test(condition));
185
+ return continuation !== undefined && !SAFE_CONTINUE.test(continuation);
179
186
  }
180
187
 
181
188
  function stepBounds(lines, runIndex, runIndent, inline) {
@@ -217,7 +224,7 @@ function unquoteRun(value) {
217
224
  return trimmed;
218
225
  }
219
226
 
220
- export function workflowRunText(text) {
227
+ function workflowRunText(text) {
221
228
  const lines = String(text || '').split('\n');
222
229
  const commands = [];
223
230
  for (let index = 0; index < lines.length; index++) {
@@ -243,14 +250,90 @@ export function workflowRunText(text) {
243
250
  return commands.join('\n');
244
251
  }
245
252
 
246
- function jobCannotEnforce(job) {
253
+ const NEEDS_LINE = /^\s*(?:"needs"|'needs'|needs):\s*(.*?)\s*(?:#.*)?$/;
254
+ const JOB_ID = /^[A-Za-z0-9_-]+$/;
255
+
256
+ function unquoteScalar(value) {
257
+ return String(value).trim().replace(/^(['"])(.*)\1$/, '$2').trim();
258
+ }
259
+
260
+ /** `needs:` as scalar, flow list, or block list. `valid: false` for expressions/malformed. */
261
+ function jobNeeds(job) {
247
262
  const propertyIndent = jobPropertyIndent(job);
248
263
  const lines = job.body.split('\n');
249
- const needs = lines.some((line) =>
250
- (line.match(/^\s*/)?.[0].length ?? 0) === propertyIndent &&
251
- /^\s*(?:"needs"|'needs'|needs):/.test(line)
264
+ const start = lines.findIndex((line) =>
265
+ (line.match(/^\s*/)?.[0].length ?? 0) === propertyIndent && NEEDS_LINE.test(line)
252
266
  );
253
- return unsafeControl(lines, propertyIndent, needs);
267
+ if (start < 0) return { present: false, ids: [], valid: true };
268
+ const value = unquoteScalar(lines[start].match(NEEDS_LINE)[1]);
269
+ if (value) {
270
+ const raw = value.startsWith('[') && value.endsWith(']') ? value.slice(1, -1).split(',') : [value];
271
+ const ids = raw.map(unquoteScalar).filter((id) => JOB_ID.test(id));
272
+ return { present: true, ids, valid: ids.length > 0 && ids.length === raw.length };
273
+ }
274
+ const ids = [];
275
+ for (let index = start + 1; index < lines.length; index++) {
276
+ if (!lines[index].trim() || /^\s*#/.test(lines[index])) continue;
277
+ if ((lines[index].match(/^\s*/)?.[0].length ?? 0) <= propertyIndent) break;
278
+ const item = lines[index].match(/^\s*-\s*(['"]?)([A-Za-z0-9_-]+)\1\s*(?:#.*)?$/);
279
+ if (!item) return { present: true, ids: [], valid: false };
280
+ ids.push(item[2]);
281
+ }
282
+ return { present: true, ids, valid: ids.length > 0 };
283
+ }
284
+
285
+ /** 'always' | 'default' (absent, `true`, `${{ true }}` = implicit success()) | 'conditional'. */
286
+ function jobConditionKind(job) {
287
+ const { condition } = controlValues(job.body.split('\n'), jobPropertyIndent(job));
288
+ if (condition === undefined) return 'default';
289
+ if (SAFE_NEEDS_IF.test(condition)) return 'always';
290
+ return SAFE_IF.test(condition) ? 'default' : 'conditional';
291
+ }
292
+
293
+ function jobIndex(jobs) {
294
+ return new Map(jobs.map((job) => [job.id, job]));
295
+ }
296
+
297
+ /**
298
+ * True when nothing in the `needs:` chain can intentionally skip this job.
299
+ *
300
+ * GitHub skips a job whose needed job failed, and a skipped job satisfies a required
301
+ * status check. An unconditional chain is still fail-closed for the *workflow*: its
302
+ * only skip path is an upstream failure that fails the same run. Whether that upstream
303
+ * failure also blocks the merge is a provider question, answered by required-status
304
+ * correlation over `upstreamJobs`. A conditional `if:` anywhere in the chain (for
305
+ * example a CI-profile output) is an intentional skip reported as success, so the job
306
+ * cannot enforce.
307
+ */
308
+ function needsChainUnconditional(job, byId, seen = new Set()) {
309
+ const needs = jobNeeds(job);
310
+ if (!needs.present) return true;
311
+ if (!needs.valid || seen.has(job.id)) return false;
312
+ const nextSeen = new Set(seen).add(job.id);
313
+ return needs.ids.every((id) => {
314
+ const upstream = byId.get(id);
315
+ if (!upstream || nextSeen.has(id)) return false;
316
+ const kind = jobConditionKind(upstream);
317
+ if (kind === 'conditional') return false;
318
+ return kind === 'always' || needsChainUnconditional(upstream, byId, nextSeen);
319
+ });
320
+ }
321
+
322
+ function jobCannotEnforce(job, byId) {
323
+ if (unsafeControl(job.body.split('\n'), jobPropertyIndent(job))) return true;
324
+ if (jobConditionKind(job) === 'always') return false;
325
+ return !needsChainUnconditional(job, byId);
326
+ }
327
+
328
+ /** Jobs whose failure skips `job`; stops at `if: always()` upstreams, which still run. */
329
+ function upstreamJobs(job, byId, out = new Map()) {
330
+ for (const id of jobNeeds(job).ids) {
331
+ const upstream = byId.get(id);
332
+ if (!upstream || out.has(id)) continue;
333
+ out.set(id, upstream);
334
+ if (jobConditionKind(upstream) !== 'always') upstreamJobs(upstream, byId, out);
335
+ }
336
+ return out;
254
337
  }
255
338
 
256
339
  function commandsRunArk(commands, script) {
@@ -261,9 +344,10 @@ function commandsEnforceArk(commands, script) {
261
344
  return analyzeCommands(commands, script).some((entry) => entry.enforcing && FAIL_CLOSED.test(entry.text));
262
345
  }
263
346
 
264
- export function activeWorkflowRunText(text) {
347
+ function activeWorkflowRunText(text) {
265
348
  const jobs = workflowJobs(text);
266
- return jobs.filter((job) => !jobCannotEnforce(job)).map((job) => workflowRunText(job.body)).join('\n');
349
+ const byId = jobIndex(jobs);
350
+ return jobs.filter((job) => !jobCannotEnforce(job, byId)).map((job) => workflowRunText(job.body)).join('\n');
267
351
  }
268
352
 
269
353
  export function runsArkCheck(text, script = '') {
@@ -322,12 +406,16 @@ function localArkContexts(root) {
322
406
  const known = new Set();
323
407
  const occurrences = new Map();
324
408
  const arkOccurrences = new Map();
409
+ // context -> contexts whose failure would skip it (null = an upstream has no literal context)
410
+ const upstream = new Map();
325
411
  let dynamic = false;
326
412
  for (const workflow of workflowFiles(root)) {
327
- for (const job of workflowJobs(workflow.text)) {
413
+ const jobs = workflowJobs(workflow.text);
414
+ const byId = jobIndex(jobs);
415
+ for (const job of jobs) {
328
416
  const explicit = jobName(job);
329
417
  const matrix = jobHasMatrix(job);
330
- const enforcing = !jobCannotEnforce(job) &&
418
+ const enforcing = !jobCannotEnforce(job, byId) &&
331
419
  commandsEnforceArk(workflowRunText(job.body), script);
332
420
  if (enforcing) {
333
421
  known.add(job.id);
@@ -339,7 +427,15 @@ function localArkContexts(root) {
339
427
  }
340
428
  const context = explicit.name ?? job.id;
341
429
  occurrences.set(context, (occurrences.get(context) ?? 0) + 1);
342
- if (enforcing) arkOccurrences.set(context, (arkOccurrences.get(context) ?? 0) + 1);
430
+ if (enforcing) {
431
+ arkOccurrences.set(context, (arkOccurrences.get(context) ?? 0) + 1);
432
+ const skippers = jobConditionKind(job) === 'always' ? [] : [...upstreamJobs(job, byId).values()];
433
+ const names = skippers.map((needed) => {
434
+ const neededName = jobName(needed);
435
+ return neededName.dynamic || jobHasMatrix(needed) ? null : neededName.name ?? needed.id;
436
+ });
437
+ upstream.set(context, names.includes(null) ? null : names);
438
+ }
343
439
  }
344
440
  }
345
441
  const contexts = new Set();
@@ -347,7 +443,19 @@ function localArkContexts(root) {
347
443
  if (count === 1 && occurrences.get(context) === 1) contexts.add(context);
348
444
  else dynamic = true;
349
445
  }
350
- return { contexts, known, dynamic };
446
+ return { contexts, known, dynamic, upstream };
447
+ }
448
+
449
+ /**
450
+ * Required-status names that must also be required so an upstream failure (which skips
451
+ * the Ark job, and a skipped job satisfies branch protection) still blocks the merge.
452
+ * Returns [] when the chain is closed, otherwise the missing names ('<dynamic>' when an
453
+ * upstream only has a matrix/expression name that cannot be required exactly).
454
+ */
455
+ function unrequiredUpstream(local, context, requiredNames) {
456
+ const names = local.upstream.get(context);
457
+ if (names === null) return ['<dynamic>'];
458
+ return (names ?? []).filter((name) => !requiredNames.has(name));
351
459
  }
352
460
 
353
461
  /** Historical name retained; the set also contains literal, non-dynamic job names. */
@@ -357,17 +465,31 @@ export function jobIdsThatRunArkCheck(root) {
357
465
 
358
466
  export function isArkRequiredStatusCheck(root, requiredNames) {
359
467
  if (!Array.isArray(requiredNames) || requiredNames.length === 0) return false;
360
- const local = localArkContexts(root).contexts;
361
- return requiredNames.some((name) => local.has(String(name)));
468
+ const local = localArkContexts(root);
469
+ const required = new Set(requiredNames.map(String));
470
+ return [...required].some((name) =>
471
+ local.contexts.has(name) && unrequiredUpstream(local, name, required).length === 0
472
+ );
362
473
  }
363
474
 
364
- function classifyRequired(root, legacyContexts, checks, bindingField) {
365
- const local = localArkContexts(root);
475
+ /**
476
+ * `requiredNames` is every required context across classic protection and rulesets: an
477
+ * Ark context only counts as required when each job that can skip it is required too.
478
+ */
479
+ function classifyRequired(local, legacyContexts, checks, bindingField, requiredNames) {
366
480
  const exact = (name) => local.contexts.has(String(name));
481
+ const closed = (name) => unrequiredUpstream(local, String(name), requiredNames).length === 0;
367
482
  const matching = checks.filter((check) => exact(check.context));
368
- if (matching.some((check) => check[bindingField] == null || check[bindingField] === -1)) return true;
483
+ // Unbound (any source) or bound to the GitHub Actions app that runs the local workflow job.
484
+ const satisfiable = matching.filter((check) =>
485
+ check[bindingField] == null || check[bindingField] === -1 ||
486
+ check[bindingField] === GITHUB_ACTIONS_APP_ID
487
+ );
488
+ if (satisfiable.some((check) => closed(check.context))) return true;
369
489
  if (matching.length > 0) return 'unverified';
370
- if (legacyContexts.some(exact)) return true;
490
+ const legacy = legacyContexts.filter(exact);
491
+ if (legacy.some(closed)) return true;
492
+ if (legacy.length > 0) return 'unverified';
371
493
  return local.dynamic && (legacyContexts.length > 0 || checks.length > 0)
372
494
  ? 'unverified'
373
495
  : false;
@@ -421,7 +543,7 @@ export function classifyGithubProviderFailure(errorText, opts = {}) {
421
543
  * @param {{ name?: string, workflowName?: string, displayTitle?: string }} run
422
544
  * @returns {boolean}
423
545
  */
424
- export function isArkishCiRun(run) {
546
+ function isArkishCiRun(run) {
425
547
  const blob = `${run?.name || ''} ${run?.workflowName || ''} ${run?.displayTitle || ''}`.toLowerCase();
426
548
  if (!blob.trim()) return false;
427
549
  // Product tokens with word boundaries (avoid spark, dark, lark false positives).
@@ -558,18 +680,32 @@ export function reportGithubBranchProtection(opts = {}) {
558
680
  })).filter((check) => check.context)
559
681
  : [];
560
682
  const hasWorkflowRule = rulesAvailable && rules.some((rule) => rule?.type === 'workflows');
683
+ const all = [...new Set([...contexts, ...checks.map((check) => check.context), ...statusRules.map((check) => check.context)])];
684
+ const requiredNames = new Set(all);
685
+ const local = localArkContexts(cwd);
561
686
  const classicRequired = classicAvailable
562
- ? classifyRequired(cwd, contexts, checks, 'app_id')
687
+ ? classifyRequired(local, contexts, checks, 'app_id', requiredNames)
563
688
  : 'unverified';
564
689
  const rulesRequired = rulesAvailable
565
- ? classifyRequired(cwd, [], statusRules, 'integration_id')
690
+ ? classifyRequired(local, [], statusRules, 'integration_id', requiredNames)
566
691
  : 'unverified';
567
692
  const arkCheckRequired = combineRequired(
568
693
  classicRequired, rulesRequired, classicAvailable, rulesAvailable, hasWorkflowRule
569
694
  );
570
- const arkCheckSourceBound = arkCheckRequired === true ? false : 'unverified';
695
+ // true only when every required Ark check is bound to GitHub Actions; unbound or
696
+ // legacy-context-only requirements can be satisfied by any status writer.
697
+ const arkBindings = [
698
+ ...checks.map((check) => ({ context: check.context, id: check.app_id })),
699
+ ...statusRules.map((check) => ({ context: check.context, id: check.integration_id })),
700
+ ].filter((check) => local.contexts.has(check.context));
701
+ const arkCheckSourceBound = arkCheckRequired !== true
702
+ ? 'unverified'
703
+ : arkBindings.length > 0 && arkBindings.every((check) => check.id === GITHUB_ACTIONS_APP_ID);
571
704
  const available = arkCheckRequired === true || (classicAvailable && rulesAvailable);
572
- const all = [...new Set([...contexts, ...checks.map((check) => check.context), ...statusRules.map((check) => check.context)])];
705
+ // Required Ark contexts that an unrequired upstream failure could skip (reported as success).
706
+ const arkCheckUpstreamNotRequired = [...new Set(all
707
+ .filter((name) => local.contexts.has(name))
708
+ .flatMap((name) => unrequiredUpstream(local, name, requiredNames)))];
573
709
  const error = `${classicResult.stderr || ''}${rulesResult.stderr || ''}`.slice(0, 400);
574
710
 
575
711
  let reason = available ? 'ok' : 'provider-enforcement-unverified';
@@ -607,6 +743,7 @@ export function reportGithubBranchProtection(opts = {}) {
607
743
  enforcesAdmins: classicAvailable ? Boolean(classic.enforcesAdmins) : null,
608
744
  arkCheckRequired,
609
745
  arkCheckSourceBound,
746
+ arkCheckUpstreamNotRequired,
610
747
  // hard merge remains false when status is not proven required
611
748
  hard: arkCheckRequired === true ? undefined : false,
612
749
  runtimeObserved: ciRuntime.runtimeObserved === true,
@@ -10,7 +10,7 @@ import path from 'node:path';
10
10
  /** Relative path from project root (stable contract). */
11
11
  export const GOLDEN_PATTERN_REL = '.ark/golden-pattern.json';
12
12
 
13
- export const GOLDEN_PATTERN_SCHEMA_VERSION = '1';
13
+ const GOLDEN_PATTERN_SCHEMA_VERSION = '1';
14
14
 
15
15
  /**
16
16
  * @typedef {{
@@ -1,22 +1,24 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
2
- import{execCommandParts as l,execRunner as i}from"../ark-shared.mjs";const s="arkgate-mcp";function h(e){const r=i(e);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
- `}function y(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${s} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
- `}function $(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
- `}function O(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),c=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
2
+ import{execCommandParts as h,execRunner as d}from"../ark-shared.mjs";const m="arkgate-mcp";function y(t,r){let o,e;try{o=t&&t.trim()?JSON.parse(t):{},e=JSON.parse(r)}catch{return null}return!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e)?null:{existing:o,generated:e}}function x(t){const r=d(t);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
+ `}function _(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${m} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
+ `}function S(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${m} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
+ `}function C(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]),e=n=>n.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),s=o.map(n=>`"${e(n)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
6
6
  # CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
7
7
  # Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
8
8
  [mcp_servers.ark]
9
- command = "${t(r)}"
10
- args = [${c}]
11
- `}function R(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=o.map(c=>`"${c.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
9
+ command = "${e(r)}"
10
+ args = [${s}]
11
+ `}function E(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]),e=o.map(s=>`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
12
12
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
13
13
  [mcp_servers.ark]
14
14
  command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
15
- args = [${t}]
16
- `}function _(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
- `}function A(e){const r=i(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
- `}function j(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
19
- `}function E(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t.hooks,p=(Array.isArray(c?.preToolUse)?c.preToolUse:[]).find(n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `));if(!p)return null;const m=o.hooks&&typeof o.hooks=="object"&&!Array.isArray(o.hooks)?{...o.hooks}:{},f=Array.isArray(m.preToolUse)?[...m.preToolUse]:[],k=n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `)&&(typeof n.matcher!="string"||/Write/.test(n.matcher)||/StrReplace/.test(n.matcher)),g=f.filter(n=>!k(n));m.preToolUse=[...g,p];const u={...o,version:o.version??t.version??1,hooks:m};return`${JSON.stringify(u,null,2)}
20
- `}function S(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t["ark-write-gate"];if(!c||typeof c!="object")return null;const a={...o,"ark-write-gate":c};return`${JSON.stringify(a,null,2)}
21
- `}function C(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const c={...o};!c.$schema&&t.$schema&&(c.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,c.mcp=a,`${JSON.stringify(c,null,2)}
22
- `}export{s as PREFERRED_MCP_BIN,A as antigravityHooks,y as claudeSettings,$ as codexHooks,O as codexProjectConfig,h as cursorHooks,_ as grokHooks,R as grokProjectConfig,S as mergeAntigravityArkHook,E as mergeCursorArkHook,C as mergeOpencodeArkMcp,j as opencodeProjectConfig};
15
+ args = [${e}]
16
+ `}function v(t){const r=d(t);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${m} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
+ `}function J(t){const r=d(t);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${m} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
+ `}function P(t){const{command:r,args:o}=h(t,m,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
19
+ `}function N(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s.hooks,p=(Array.isArray(n?.preToolUse)?n.preToolUse:[]).find(c=>c&&typeof c=="object"&&typeof c.command=="string"&&/arkgate-mcp|ark-mcp/.test(c.command)&&/\s--hook(?:\s|$)/.test(` ${c.command} `));if(!p)return null;const i=e.hooks&&typeof e.hooks=="object"&&!Array.isArray(e.hooks)?{...e.hooks}:{},l=Array.isArray(i.preToolUse)?[...i.preToolUse]:[],u=c=>c&&typeof c=="object"&&typeof c.command=="string"&&/arkgate-mcp|ark-mcp/.test(c.command)&&/\s--hook(?:\s|$)/.test(` ${c.command} `)&&(typeof c.matcher!="string"||/Write/.test(c.matcher)||/StrReplace/.test(c.matcher)),a=l.filter(c=>!u(c));i.preToolUse=[...a,p];const f={...e,version:e.version??s.version??1,hooks:i};return`${JSON.stringify(f,null,2)}
20
+ `}const $=/(?:^|[\s/])(?:arkgate-mcp|ark-mcp)(?:\.mjs)?(?:\s|$)/,b=/\s--(?:hook|session-context)(?:\s|$)/;function j(t){return!!t&&typeof t=="object"&&typeof t.command=="string"&&$.test(` ${t.command} `)&&b.test(` ${t.command} `)}function T(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s.hooks;if(!n||typeof n!="object"||e.hooks!==void 0&&(!e.hooks||typeof e.hooks!="object"||Array.isArray(e.hooks)))return null;const k={...e.hooks??{}};for(const[p,i]of Object.entries(n)){if(!Array.isArray(i))continue;const l=k[p];if(l!==void 0&&!Array.isArray(l))return null;const u=[];for(const a of l??[]){if(!a||typeof a!="object"||!Array.isArray(a.hooks)){u.push(a);continue}const f=a.hooks.filter(c=>!j(c));f.length===a.hooks.length?u.push(a):f.length>0&&u.push({...a,hooks:f})}k[p]=[...u,...i]}return`${JSON.stringify({...e,hooks:k},null,2)}
21
+ `}function g(t){const r=o=>typeof o=="string"?o.replace(/arkgate@[^\s"']+/g,"arkgate@*"):Array.isArray(o)?o.map(r):o&&typeof o=="object"?Object.fromEntries(Object.keys(o).sort().map(e=>[e,r(o[e])])):o;return JSON.stringify(r(t))}function A(t,r){try{const o=JSON.parse(t),e=JSON.parse(r);return!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e)?null:{existing:o,generated:e}}catch{return null}}function O(t){return{status:"removed",text:Object.keys(t).length===0?null:`${JSON.stringify(t,null,2)}
22
+ `}}function U(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(e.hooks===void 0)return{status:"absent"};if(!e.hooks||typeof e.hooks!="object"||Array.isArray(e.hooks))return{status:"unreadable"};const n={...e.hooks};let k=0;for(const[i,l]of Object.entries(s.hooks??{})){if(!Array.isArray(l)||n[i]===void 0)continue;if(!Array.isArray(n[i]))return{status:"unreadable"};const u=new Set(l.map(g)),a=[];for(const f of n[i]){if(!(f&&typeof f=="object"&&Array.isArray(f.hooks)&&f.hooks.some(j))){a.push(f);continue}if(!u.has(g(f)))return{status:"customized"};k+=1}a.length>0?n[i]=a:delete n[i]}if(k===0)return{status:"absent"};const p={...e,hooks:n};return Object.keys(n).length===0&&delete p.hooks,O(p)}function D(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o,n=e.hooks?.preToolUse;if(n===void 0)return{status:"absent"};if(!Array.isArray(n))return{status:"unreadable"};const k=new Set((Array.isArray(s.hooks?.preToolUse)?s.hooks.preToolUse:[]).map(g)),p=[];let i=0;for(const a of n){if(!(a&&typeof a=="object"&&typeof a.command=="string"&&/arkgate-mcp|ark-mcp/.test(a.command)&&/\s--hook(?:\s|$)/.test(` ${a.command} `))){p.push(a);continue}if(!k.has(g(a)))return{status:"customized"};i+=1}if(i===0)return{status:"absent"};const l={...e.hooks};p.length>0?l.preToolUse=p:delete l.preToolUse;const u={...e,hooks:l};return Object.keys(l).length===0&&delete u.hooks,Object.keys(u).length===1&&u.version===(s.version??1)&&delete u.version,O(u)}function H(t,r){const o=A(t,r);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(!Object.prototype.hasOwnProperty.call(e,"ark-write-gate"))return{status:"absent"};if(g(e["ark-write-gate"])!==g(s["ark-write-gate"]))return{status:"customized"};const n={...e};return delete n["ark-write-gate"],O(n)}function w(t,r){const o=y(t,r);if(!o)return null;const{existing:e,generated:s}=o,n=s["ark-write-gate"];if(!n||typeof n!="object")return null;const k={...e,"ark-write-gate":n};return`${JSON.stringify(k,null,2)}
23
+ `}function I(t,r){let o,e;try{o=t&&t.trim()?JSON.parse(t):{},e=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const s={...o};!s.$schema&&e.$schema&&(s.$schema=e.$schema);const n=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return n.ark=e.mcp.ark,s.mcp=n,`${JSON.stringify(s,null,2)}
24
+ `}export{m as PREFERRED_MCP_BIN,J as antigravityHooks,_ as claudeSettings,S as codexHooks,C as codexProjectConfig,x as cursorHooks,v as grokHooks,E as grokProjectConfig,w as mergeAntigravityArkHook,T as mergeClaudeStyleArkHooks,N as mergeCursorArkHook,I as mergeOpencodeArkMcp,P as opencodeProjectConfig,y as parseJsonMergeInputs,H as removeAntigravityArkHook,U as removeClaudeStyleArkHooks,D as removeCursorArkHook};
@@ -37,7 +37,9 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
37
37
  }),
38
38
  // Host-native "deny when the checker cannot run" flag (FS-permission analog).
39
39
  // required = schema field must be true for hard-write evidence (Cursor failClosed).
40
- // unsupported = host has no documented flag; hook-crash behavior is host-owned.
40
+ // unsupported = host has no documented flag. ark-mcp --hook fails closed itself
41
+ // (exit 2 / host deny) when its checker cannot run for a governed source write;
42
+ // only a crash of node itself before the launcher runs stays host-owned.
41
43
  // none = advisory host; no local write block.
42
44
  nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
43
45
  // EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
@@ -14,6 +14,8 @@ import { formatArkRunHtml } from './ark-run-report.mjs';
14
14
  import { formatArkOrderHtml } from './ark-order-report.mjs';
15
15
  import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
16
16
  import { flatParentPilotHtml } from './flat-parent-pilot.mjs';
17
+ import { orphanModulesHtml } from './orphan-modules-io.mjs';
18
+ import { crossWallDuplicationHtml } from './duplication-io.mjs';
17
19
 
18
20
  // htmlEscape is injected by the caller (html-report.mjs) — importing it back
19
21
  // would be a dependency cycle, and the repo's own gate blocks that. The
@@ -362,6 +364,8 @@ export function renderAdvisorySections(advisories, escape) {
362
364
  physicalCohesionHtml(advisories.physicalCohesion),
363
365
  parseHealthHtml(advisories.parseHealth),
364
366
  graphBlindSpotsHtml(advisories.graphBlindSpots, esc),
367
+ orphanModulesHtml(advisories.orphanModules, esc),
368
+ crossWallDuplicationHtml(advisories.crossWallDuplication, esc),
365
369
  rulesUnderContractHtml(advisories.rulesUnderContract),
366
370
  formatArkRunHtml(advisories.arkRun, esc),
367
371
  formatArkOrderHtml(advisories.arkOrder, esc),
@@ -28,7 +28,7 @@ import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
28
28
  import { computePhysicalCohesion } from './physical-cohesion.mjs';
29
29
  import { computeReshapeDecisionMemory } from './reshape-decisions.mjs';
30
30
  import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
31
- import { anyChildWallAdvisory, anySliceAlias } from '../ark-layer-match.mjs';
31
+ import { anyChildWallAdvisory, sliceAliasOwesMove } from '../ark-layer-match.mjs';
32
32
 
33
33
  function esc(value) {
34
34
  return String(value)
@@ -176,7 +176,7 @@ export function buildReportDepthPayload(
176
176
  primaryNextAction: postGreenPath?.action ?? dualTruthNext,
177
177
  activeBlockingViolations: activeBlockingCount,
178
178
  physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion), childWallAdvisory: anyChildWallAdvisory(config?.rules),
179
- sliceAliasDebt: anySliceAlias(config?.rules),
179
+ sliceAliasDebt: sliceAliasOwesMove(config?.rules),
180
180
  });
181
181
  // Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
182
182
  const baselineStale =
@@ -11,7 +11,7 @@ import {
11
11
  resolveOperatingMode,
12
12
  } from '../ark-shared.mjs';
13
13
  import { collectAdoptionGaps, arkCheckCommand } from './agent-gates.mjs';
14
- import { LEFTOVER_DESIGN_BADGE } from './product-copy.mjs';
14
+ import { LEFTOVER_DESIGN_LABEL } from './product-copy.mjs';
15
15
  import { CORE_LAYER_NAMES } from './core-layers.mjs';
16
16
  import {
17
17
  renderBaselineSignalLegend,
@@ -572,7 +572,7 @@ export function renderHtmlReport({
572
572
  const designSmells = Array.isArray(depth.designSmells) ? depth.designSmells : [];
573
573
  const designWeakBadge =
574
574
  designFitness?.designWeak === true
575
- ? ` <span class="badge design" title="Import rules can be green while leftover design work remains. Not a FAIL.">${LEFTOVER_DESIGN_BADGE}</span>`
575
+ ? ` <span class="badge design" title="Import rules can be green while leftover design work remains. Not a FAIL.">${LEFTOVER_DESIGN_LABEL}</span>`
576
576
  : '';
577
577
  const designStripHtml =
578
578
  renderDesignDepthStrip({
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Compact importer index for doctor and report (ADR 0037 D1).
3
+ *
4
+ * The scan releases the resolved facts right after the verdict (memory). This
5
+ * keeps only what the files-nothing-imports advisory and the flat-parent
6
+ * suggestion read: governed paths, incoming edge counts, the edge list as
7
+ * typed-array pairs, and which export names each target is imported by. It is
8
+ * a projection of facts that already exist — never an input to the verdict.
9
+ */
10
+
11
+ /** Unresolved project-looking specifiers kept for `maybe-unresolved` matching. */
12
+ const MAX_UNRESOLVED_SPECIFIERS = 200;
13
+ /** A bare specifier that looks like a project alias rather than a package. */
14
+ const ALIAS_LIKE = /^(?:[@~#$]\/|[~#$])/;
15
+
16
+ function looksLikeProjectSpecifier(specifier) {
17
+ return typeof specifier === 'string' && (specifier.startsWith('.') || ALIAS_LIKE.test(specifier));
18
+ }
19
+
20
+ /**
21
+ * @param {{ files?: Array<{ path: string, parseStatus?: string }>, dependencies?: Array<object>, completeness?: string }} facts
22
+ * @returns {{
23
+ * files: string[],
24
+ * importerCount: Uint32Array,
25
+ * edges: Int32Array,
26
+ * namedUse: Map<number, Set<string> | '*'>,
27
+ * unresolved: Array<{ from: string, specifier: string }>,
28
+ * unresolvedTotal: number,
29
+ * dynamicSites: number,
30
+ * invalidFiles: number,
31
+ * completeness: string,
32
+ * }}
33
+ */
34
+ export function projectImporterIndex(facts) {
35
+ const files = [];
36
+ const indexOf = new Map();
37
+ let invalidFiles = 0;
38
+ for (const file of Array.isArray(facts?.files) ? facts.files : []) {
39
+ if (typeof file?.path !== 'string' || indexOf.has(file.path)) continue;
40
+ indexOf.set(file.path, files.length);
41
+ files.push(file.path);
42
+ if (file.parseStatus === 'invalid') invalidFiles += 1;
43
+ }
44
+ const importerCount = new Uint32Array(files.length);
45
+ const pairs = [];
46
+ const namedUse = new Map();
47
+ const unresolved = [];
48
+ let unresolvedTotal = 0;
49
+ let dynamicSites = 0;
50
+ for (const dep of Array.isArray(facts?.dependencies) ? facts.dependencies : []) {
51
+ if (dep?.resolution === 'dynamic') {
52
+ dynamicSites += 1;
53
+ continue;
54
+ }
55
+ if (dep?.resolution === 'unresolved') {
56
+ if (!looksLikeProjectSpecifier(dep.specifier)) continue;
57
+ unresolvedTotal += 1;
58
+ if (unresolved.length < MAX_UNRESOLVED_SPECIFIERS) {
59
+ unresolved.push({ from: dep.from, specifier: dep.specifier });
60
+ }
61
+ continue;
62
+ }
63
+ if (dep?.resolution !== 'resolved-project') continue;
64
+ const from = indexOf.get(dep.from);
65
+ const to = indexOf.get(dep.target);
66
+ if (from === undefined || to === undefined || from === to) continue;
67
+ pairs.push(from, to);
68
+ importerCount[to] += 1;
69
+ const previous = namedUse.get(to);
70
+ if (previous === '*') continue;
71
+ // Default, namespace, side-effect, dynamic, require and `export *` carry no
72
+ // names: any export may be used through them.
73
+ const named = Array.isArray(dep.namedBindings) && dep.namedBindings.length > 0 ? dep.namedBindings : null;
74
+ if (!named) {
75
+ namedUse.set(to, '*');
76
+ continue;
77
+ }
78
+ const set = previous ?? new Set();
79
+ for (const name of named) set.add(name);
80
+ namedUse.set(to, set);
81
+ }
82
+ return {
83
+ files,
84
+ importerCount,
85
+ edges: Int32Array.from(pairs),
86
+ namedUse,
87
+ unresolved,
88
+ unresolvedTotal,
89
+ dynamicSites,
90
+ invalidFiles,
91
+ completeness: typeof facts?.completeness === 'string' ? facts.completeness : 'unavailable',
92
+ };
93
+ }
94
+
95
+ /**
96
+ * target path → every governed file that imports it (the flat-parent graph).
97
+ * @param {ReturnType<typeof projectImporterIndex>} index
98
+ */
99
+ export function importerGraphFromIndex(index) {
100
+ const graph = new Map();
101
+ const { files, edges } = index;
102
+ for (let i = 0; i + 1 < edges.length; i += 2) {
103
+ const target = files[edges[i + 1]];
104
+ let set = graph.get(target);
105
+ if (!set) {
106
+ set = new Set();
107
+ graph.set(target, set);
108
+ }
109
+ set.add(files[edges[i]]);
110
+ }
111
+ return graph;
112
+ }
@@ -56,7 +56,19 @@ export function readTsconfigAliases(ts, root) {
56
56
  if (!configPath) return { baseUrl: root, aliases: [] };
57
57
  const read = ts.readConfigFile(configPath, ts.sys.readFile);
58
58
  if (read.error) return { baseUrl: root, aliases: [] };
59
- const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
59
+ // Options-only parse: `fileNames` are unused here. With the real `ts.sys` the
60
+ // parser walked the whole `include` tree on every write-hook call (#205 twin).
61
+ const optionsOnlyHost = {
62
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
63
+ readDirectory: () => [],
64
+ fileExists: (fileName) => ts.sys.fileExists(fileName),
65
+ readFile: (fileName) => ts.sys.readFile(fileName),
66
+ };
67
+ const parsed = ts.parseJsonConfigFileContent(
68
+ read.config,
69
+ optionsOnlyHost,
70
+ path.dirname(configPath)
71
+ );
60
72
  const opts = parsed.options || {};
61
73
  const baseUrl = opts.baseUrl || path.dirname(configPath);
62
74
  const aliases = [];
@@ -2,11 +2,7 @@
2
2
  * Doctor adapter for the Domain improvement compass (notAScore projection).
3
3
  * Keeps doctor-plan.mjs inside its module budget; pure assembly only.
4
4
  */
5
- import {
6
- buildImprovementCompass,
7
- formatImprovementCompassDoctorLines,
8
- primaryImprovementCompassNextAction,
9
- } from './improvement-compass.mjs';
5
+ import { buildImprovementCompass } from './improvement-compass.mjs';
10
6
 
11
7
  /**
12
8
  * @param {{
@@ -92,22 +88,3 @@ export function buildDoctorImprovementCompass(input = {}) {
92
88
  });
93
89
  }
94
90
 
95
- export {
96
- formatImprovementCompassDoctorLines,
97
- primaryImprovementCompassNextAction,
98
- };
99
-
100
- /**
101
- * Human doctor section (never a score bar).
102
- * @param {import('./improvement-compass.mjs').ImprovementCompass} compass
103
- * @param {{ line: Function, warn: string, ok: string, color: { bold: Function } }} io
104
- */
105
- export function printImprovementCompassSection(compass, io) {
106
- const { line, warn, ok, color } = io;
107
- console.log('');
108
- console.log(color.bold('Improvement compass (not a score)'));
109
- const mark = compass.topResidual.length > 0 ? warn : ok;
110
- for (const text of formatImprovementCompassDoctorLines(compass)) {
111
- line(mark, text);
112
- }
113
- }