arkgate 4.8.22 → 4.8.24

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 (136) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +27 -78
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +1117 -38
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  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/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -2
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +45 -6
  35. package/bin/lib/doctor-advisories.mjs +84 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +7 -9
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-honesty.mjs +22 -0
  40. package/bin/lib/enforcement-profiles.mjs +40 -4
  41. package/bin/lib/first-run-help.mjs +3 -2
  42. package/bin/lib/flat-parent-importers.mjs +137 -0
  43. package/bin/lib/flat-parent-pilot.mjs +192 -0
  44. package/bin/lib/gate-files.mjs +29 -2
  45. package/bin/lib/git-change-scope.mjs +109 -0
  46. package/bin/lib/github-enforcement.mjs +1 -1
  47. package/bin/lib/hook-templates.mjs +18 -16
  48. package/bin/lib/host-support-matrix.mjs +3 -1
  49. package/bin/lib/html-report-advisories.mjs +2 -0
  50. package/bin/lib/html-report-depth.mjs +3 -1
  51. package/bin/lib/import-resolve.mjs +13 -1
  52. package/bin/lib/install-migrate.mjs +50 -73
  53. package/bin/lib/invariant-coverage.mjs +59 -11
  54. package/bin/lib/mcp-adoption.mjs +9 -1
  55. package/bin/lib/mcp-hook-payload.mjs +401 -61
  56. package/bin/lib/package-manager.mjs +59 -1
  57. package/bin/lib/passthrough-commands.mjs +83 -0
  58. package/bin/lib/persistence-write-hint.mjs +81 -0
  59. package/bin/lib/physical-cohesion.mjs +16 -18
  60. package/bin/lib/pilot-loop.mjs +21 -8
  61. package/bin/lib/pin-dependent-gates.mjs +117 -0
  62. package/bin/lib/policy-delta-io.mjs +168 -10
  63. package/bin/lib/prepare-change.mjs +120 -6
  64. package/bin/lib/prepare-write.mjs +1 -0
  65. package/bin/lib/presets.mjs +3 -5
  66. package/bin/lib/remediation.mjs +23 -4
  67. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  68. package/bin/lib/rules-inventory.mjs +22 -3
  69. package/bin/lib/rules-under-contract.mjs +179 -5
  70. package/bin/lib/scan-files.mjs +32 -0
  71. package/bin/lib/snippet-analysis.mjs +140 -6
  72. package/bin/lib/source-policy.mjs +34 -0
  73. package/bin/lib/stable-hash.mjs +90 -0
  74. package/bin/lib/start-install-recovery.mjs +71 -1
  75. package/bin/lib/start-preview.mjs +30 -0
  76. package/bin/lib/status-command.mjs +33 -4
  77. package/bin/lib/status-manifest.mjs +32 -4
  78. package/bin/lib/team-parliament-io.mjs +79 -75
  79. package/bin/lib/team-parliament.mjs +6 -1
  80. package/bin/lib/write-path-capabilities.mjs +4 -2
  81. package/dist/chunk-A5EIY3HI.cjs +5 -0
  82. package/dist/chunk-MBXCFLO4.js +16 -0
  83. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  84. package/dist/chunk-SVSQVGFJ.js +5 -0
  85. package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
  86. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  87. package/dist/eslint/index.cjs +17 -4
  88. package/dist/eslint/index.d.cts +359 -0
  89. package/dist/eslint/index.d.ts +178 -158
  90. package/dist/eslint/index.js +10 -4
  91. package/dist/index.cjs +38 -37
  92. package/dist/index.d.cts +5475 -0
  93. package/dist/index.d.ts +3408 -296
  94. package/dist/index.js +38 -37
  95. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  96. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  97. package/dist/nestjs/index.cjs +1 -18
  98. package/dist/nestjs/index.d.cts +22 -0
  99. package/dist/nestjs/index.d.ts +1 -3
  100. package/dist/nestjs/index.js +1 -18
  101. package/dist/order/index.cjs +1 -1
  102. package/dist/order/index.d.cts +155 -0
  103. package/dist/order/index.d.ts +15 -3
  104. package/dist/order/index.js +1 -1
  105. package/dist/runtime/index.cjs +13 -30
  106. package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
  107. package/dist/runtime/index.d.ts +2678 -108
  108. package/dist/runtime/index.js +13 -30
  109. package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
  110. package/dist/types-DRinG-vT.d.ts +1792 -0
  111. package/docs/README.md +3 -1
  112. package/docs/agent-guide.md +92 -15
  113. package/docs/ai-gates.md +160 -26
  114. package/docs/arkorder.md +32 -15
  115. package/docs/brownfield-adoption.md +5 -1
  116. package/docs/configuration.md +273 -12
  117. package/docs/develop.md +2 -1
  118. package/docs/diagnostics.md +122 -13
  119. package/docs/package-surface.md +31 -8
  120. package/docs/product-voice.md +1 -1
  121. package/package.json +59 -18
  122. package/schemas/ark.analysis-result.schema.json +15 -0
  123. package/schemas/ark.config.schema.json +163 -4
  124. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  125. package/schemas/ark.status-manifest.schema.json +22 -0
  126. package/server.json +3 -9
  127. package/templates/agent-skills/README.md +1 -1
  128. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  129. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  130. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  131. package/templates/arkrules/PersistenceAdapters.json +1 -1
  132. package/templates/arkrules/PresentationAdapters.json +1 -1
  133. package/templates/skills/ark-adopt.md +4 -1
  134. package/templates/skills/ark-order.md +4 -2
  135. package/templates/skills/ark-runtime.md +6 -1
  136. package/dist/types-fQ9-KrBj.d.ts +0 -183
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { ArkOrderError } from './ark-order-error.mjs';
12
- import { CAPACITY_OPS, DEFAULT_MAX_XI_KEYS } from './ark-order-types.mjs';
13
- import { deterministicHash, stableSerialize } from './stableHash';
12
+ import { CAPACITY_OPS, DEFAULT_MAX_XI_KEYS, XI_TTL_KEY_RE } from './ark-order-types.mjs';
13
+ import { deterministicHash, stableSerialize } from './stable-hash.mjs';
14
14
  export { DEFAULT_MAX_XI_KEYS };
15
15
  /** D7: consumer still owns handlers — this only names the travel verb. */
16
16
  export function ingestTravelAction(residual) {
@@ -27,7 +27,7 @@ export function isForbiddenPlaneMethod(name) {
27
27
  function isPrimitive(value) {
28
28
  return (value === null ||
29
29
  typeof value === 'string' ||
30
- typeof value === 'number' ||
30
+ (typeof value === 'number' && Number.isFinite(value)) ||
31
31
  typeof value === 'boolean');
32
32
  }
33
33
  export function freezeRecord(input, label) {
@@ -38,10 +38,14 @@ export function freezeRecord(input, label) {
38
38
  const out = {};
39
39
  for (const key of keys.sort()) {
40
40
  const value = input[key];
41
+ if (typeof value === 'number' && !Number.isFinite(value)) {
42
+ // JSON (and so the hash) turns NaN/±Infinity into null: no stable identity.
43
+ throw new ArkOrderError('ARKORDER_NESTED_XI', `${label} key ${JSON.stringify(key)} is a non-finite number (NaN/±Infinity has no stable identity)`);
44
+ }
41
45
  if (!isPrimitive(value)) {
42
46
  throw new ArkOrderError('ARKORDER_NESTED_XI', `${label} key ${JSON.stringify(key)} is not a slow primitive (nested values are microstate)`);
43
47
  }
44
- out[key] = value;
48
+ out[key] = Object.is(value, -0) ? 0 : value;
45
49
  }
46
50
  return Object.freeze(out);
47
51
  }
@@ -141,7 +145,6 @@ export function refreshSigmaRecord(input) {
141
145
  releasedAt: input.now,
142
146
  });
143
147
  }
144
- const XI_TTL_KEY_RE = /^(ttl|freshUntil|fresh_until|maxAge|max_age)$/i;
145
148
  export function assertXiHasNoTtl(xi) {
146
149
  for (const key of Object.keys(xi)) {
147
150
  if (XI_TTL_KEY_RE.test(key)) {
@@ -159,9 +162,12 @@ export function assertInformationBudget(projection, budget) {
159
162
  }
160
163
  }
161
164
  }
165
+ /**
166
+ * σ freshness. A numeric σ.freshUntil is honored on its own and wins over the age
167
+ * check; otherwise `maxAgeMs` (sigmaMaxAgeMs) bounds the age from σ.releasedAt or
168
+ * the Release freeze time. Neither set means σ never goes stale.
169
+ */
162
170
  export function assertSigmaFresh(input) {
163
- if (input.maxAgeMs === undefined)
164
- return;
165
171
  const until = input.sigma.freshUntil;
166
172
  if (typeof until === 'number') {
167
173
  if (input.now > until) {
@@ -169,6 +175,8 @@ export function assertSigmaFresh(input) {
169
175
  }
170
176
  return;
171
177
  }
178
+ if (input.maxAgeMs === undefined)
179
+ return;
172
180
  const origin = typeof input.sigma.releasedAt === 'number' ? input.sigma.releasedAt : input.releasedAt;
173
181
  if (typeof origin === 'number' && input.now - origin > input.maxAgeMs) {
174
182
  throw new ArkOrderError('ARKORDER_STALE_SIGMA', 'σ is older than sigmaMaxAgeMs; ξ does not TTL');
@@ -312,6 +320,8 @@ export function proposePatternChange(input) {
312
320
  }
313
321
  const previous = input.projector(input.current, input.current.sigma);
314
322
  const next = input.projector(candidate, candidate.sigma);
323
+ // A pattern the budget denies is never offered for review.
324
+ assertInformationBudget(next, input.informationBudget);
315
325
  const { blastRadius, invalidations } = blastRadiusOf(previous, next);
316
326
  if (blastRadius.length === 0) {
317
327
  throw new ArkOrderError('ARKORDER_EMPTY_BLAST', 'pattern change has empty blast radius; that key is not an order parameter');
@@ -320,10 +330,37 @@ export function proposePatternChange(input) {
320
330
  nextXi: candidate.xi,
321
331
  blastRadius,
322
332
  invalidations,
333
+ baseXiHash: input.current.xiHash,
334
+ baseVersion: input.current.version,
323
335
  };
324
336
  }
325
- /** D1 valve: freeze ProposeResult.nextXi. Empty blast still fails. */
337
+ function sameSortedList(left, right) {
338
+ if (!Array.isArray(left) || left.length !== right.length)
339
+ return false;
340
+ const sorted = [...left].map(String).sort();
341
+ return sorted.every((item, index) => item === right[index]);
342
+ }
343
+ function staleProposal(detail) {
344
+ return new ArkOrderError('ARKORDER_STALE_PROPOSAL', `${detail}; re-run proposeRelease against the current Release and review the new blast radius`);
345
+ }
346
+ /**
347
+ * D1 valve: freeze ProposeResult.nextXi. The proposal must be bound to the current
348
+ * Release (baseXiHash + baseVersion) and its reviewed blastRadius / invalidations must
349
+ * equal the transition actually committed — otherwise ARKORDER_STALE_PROPOSAL.
350
+ * Empty blast still fails; a pattern the information budget denies never persists.
351
+ */
326
352
  export function applyProposedRelease(input) {
353
+ const proposal = input.proposal;
354
+ if (!proposal ||
355
+ typeof proposal !== 'object' ||
356
+ !proposal.nextXi ||
357
+ typeof proposal.nextXi !== 'object') {
358
+ throw staleProposal('apply() requires a ProposeResult from proposeRelease');
359
+ }
360
+ if (proposal.baseXiHash !== input.current.xiHash ||
361
+ proposal.baseVersion !== input.current.version) {
362
+ throw staleProposal(`proposal was computed against a different Release (base v${String(proposal.baseVersion)}, current v${input.current.version})`);
363
+ }
327
364
  const candidate = createFrozenRelease({
328
365
  xi: { ...input.proposal.nextXi },
329
366
  sigma: { ...input.current.sigma },
@@ -338,9 +375,14 @@ export function applyProposedRelease(input) {
338
375
  }
339
376
  const previous = input.projector(input.current, input.current.sigma);
340
377
  const next = input.projector(candidate, candidate.sigma);
341
- const { blastRadius } = blastRadiusOf(previous, next);
378
+ assertInformationBudget(next, input.informationBudget);
379
+ const { blastRadius, invalidations } = blastRadiusOf(previous, next);
342
380
  if (blastRadius.length === 0) {
343
381
  throw new ArkOrderError('ARKORDER_EMPTY_BLAST', 'pattern change has empty blast radius; that key is not an order parameter');
344
382
  }
383
+ if (!sameSortedList(proposal.blastRadius, blastRadius) ||
384
+ !sameSortedList(proposal.invalidations, invalidations)) {
385
+ throw staleProposal('the reviewed blast radius is not the transition apply() would commit');
386
+ }
345
387
  return candidate;
346
388
  }
@@ -8,105 +8,9 @@
8
8
  * Pure CLI helper (bin/lib/ark-order-sensors.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- import { extractArkOrderGenericUpdatesFromSource, extractArkOrderIngestWritesXiFromSource, extractArkOrderPlaneCallsFromSource, extractArkOrderReleaseKeyCountsFromSource, extractArkOrderXiFieldWritesFromSource, isArkOrderModuleSpecifier, } from './ark-order-facts.mjs';
11
+ import { extractArkOrderBudgetLeaksFromSource, extractArkOrderGenericUpdatesFromSource, extractArkOrderIngestWritesXiFromSource, extractArkOrderModuleImportsFromSource, extractArkOrderPlaneCallsFromSource, extractArkOrderReleaseKeyCountsFromSource, extractArkOrderXiFieldWritesFromSource, extractArkOrderXiTtlKeysFromSource, arkOrderGlobToRegExp as globToRegExp, isArkOrderModuleSpecifier, } from './ark-order-facts.mjs';
12
12
  import { extraMergeTeethAllowed, } from './extra-merge-teeth.mjs';
13
13
  import { deterministicNextAction } from './remediation.mjs';
14
- /**
15
- * XIWRITE-001: same engine as `globToRegExp` in src/domain/layerMatch.ts.
16
- * Inlined so generate:cli-pure emits a self-contained bin/lib/ark-order-sensors.mjs
17
- * (layerMatch is derived to bin/ark-layer-match.mjs, not a bin/lib sibling).
18
- */
19
- const appliesToRegexpCache = new Map();
20
- function escapeAppliesToLiteral(ch) {
21
- return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
22
- }
23
- function normalizeAppliesToGlob(pattern) {
24
- let out = '';
25
- for (let i = 0; i < pattern.length; i += 1) {
26
- const c = pattern[i];
27
- if (c === '\\' && i + 1 < pattern.length) {
28
- const next = pattern[i + 1];
29
- if ('*?{}[],'.includes(next) || next === '\\') {
30
- out += '\\' + next;
31
- i += 1;
32
- continue;
33
- }
34
- out += '/';
35
- continue;
36
- }
37
- out += c;
38
- }
39
- return out;
40
- }
41
- function appliesToBracesBalanced(glob) {
42
- let depth = 0;
43
- for (let i = 0; i < glob.length; i += 1) {
44
- const c = glob[i];
45
- if (c === '\\') {
46
- i += 1;
47
- continue;
48
- }
49
- if (c === '{')
50
- depth += 1;
51
- else if (c === '}') {
52
- depth -= 1;
53
- if (depth < 0)
54
- return false;
55
- }
56
- }
57
- return depth === 0;
58
- }
59
- function globToRegExp(pattern) {
60
- const cached = appliesToRegexpCache.get(pattern);
61
- if (cached)
62
- return cached;
63
- const glob = normalizeAppliesToGlob(pattern);
64
- const useBraces = appliesToBracesBalanced(glob);
65
- let out = '';
66
- let braceDepth = 0;
67
- for (let i = 0; i < glob.length; i += 1) {
68
- const c = glob[i];
69
- if (c === '\\' && i + 1 < glob.length) {
70
- out += escapeAppliesToLiteral(glob[i + 1]);
71
- i += 1;
72
- }
73
- else if (c === '*') {
74
- if (glob[i + 1] === '*') {
75
- if (glob[i + 2] === '/') {
76
- out += '(?:.*/)?';
77
- i += 2;
78
- }
79
- else {
80
- out += '.*';
81
- i += 1;
82
- }
83
- }
84
- else {
85
- out += '[^/]*';
86
- }
87
- }
88
- else if (c === '?') {
89
- out += '[^/]';
90
- }
91
- else if (c === '{' && useBraces) {
92
- out += '(?:';
93
- braceDepth += 1;
94
- }
95
- else if (c === '}' && useBraces && braceDepth > 0) {
96
- out += ')';
97
- braceDepth -= 1;
98
- }
99
- else if (c === ',' && useBraces && braceDepth > 0) {
100
- out += '|';
101
- }
102
- else {
103
- out += escapeAppliesToLiteral(c);
104
- }
105
- }
106
- const re = new RegExp(`^${out}$`);
107
- appliesToRegexpCache.set(pattern, re);
108
- return re;
109
- }
110
14
  export const ARKORDER_TIER1_SENSOR_IDS = [
111
15
  'arkorder-missing-plane',
112
16
  'arkorder-kernel-in-domain',
@@ -212,6 +116,12 @@ export function evaluateArkOrderSensors(input) {
212
116
  for (const ingest of input.ingestWritesXi ?? []) {
213
117
  findings.push(finding(extra, 'arkorder-ingest-writes-xi', ingest.file, ingest.line, 'ingest() result is assigned into a Release or ξ store; ingest may absorb or escalate, never mint a pattern.', undefined, teethAllowed));
214
118
  }
119
+ for (const ttl of input.xiTtlKeys ?? []) {
120
+ findings.push(finding(extra, 'arkorder-xi-ttl', ttl.file, ttl.line, `ξ key ${JSON.stringify(ttl.key)} is a freshness field; freshness belongs on σ (freshUntil), never on ξ.`, { target: ttl.key }, teethAllowed));
121
+ }
122
+ for (const leak of input.budgetLeaks ?? []) {
123
+ findings.push(finding(extra, 'arkorder-information-budget', leak.file, leak.line, `Projection allowedKinds lists ${JSON.stringify(leak.kind)}, which informationBudget.cannotObserve denies.`, { target: leak.kind }, teethAllowed));
124
+ }
215
125
  const managed = new Set(extra.managedLayers);
216
126
  for (const write of xiKeys.length === 0 ? [] : input.xiFieldWrites ?? []) {
217
127
  const fromLayer = input.layerForFile(write.file);
@@ -230,22 +140,39 @@ export function evaluateArkOrderEditorSensors(input) {
230
140
  if (!input.arkOrder)
231
141
  return [];
232
142
  const planeCalls = extractArkOrderPlaneCallsFromSource(input.file, input.source);
233
- const genericUpdates = extractArkOrderGenericUpdatesFromSource(input.file, input.source);
143
+ const receivers = { planeRoots: input.arkOrder.planeRoots ?? [] };
144
+ const genericUpdates = extractArkOrderGenericUpdatesFromSource(input.file, input.source, receivers);
234
145
  const xiKeys = input.arkOrder.xiKeys ?? [];
146
+ // kernel-in-domain needs the file's own layer (name + intent prefixes) and the
147
+ // arkgate/order specifiers of this one source — the write-path twin of CI facts.
148
+ const layers = input.fromLayer
149
+ ? [
150
+ {
151
+ name: input.fromLayer,
152
+ patterns: [],
153
+ intentPrefixes: [...(input.intentPrefixes ?? [])],
154
+ },
155
+ ]
156
+ : [];
235
157
  return evaluateArkOrderSensors({
236
158
  arkOrder: input.arkOrder,
237
- layers: [],
159
+ layers,
238
160
  planeCalls,
239
161
  genericUpdates,
240
162
  planeRootHits: [],
241
163
  xiFieldWrites: extractArkOrderXiFieldWritesFromSource(input.file, input.source, xiKeys),
242
164
  ingestWritesXi: extractArkOrderIngestWritesXiFromSource(input.file, input.source),
243
165
  releaseKeyCounts: extractArkOrderReleaseKeyCountsFromSource(input.file, input.source),
244
- dependencies: [],
166
+ xiTtlKeys: extractArkOrderXiTtlKeysFromSource(input.file, input.source, receivers),
167
+ budgetLeaks: extractArkOrderBudgetLeaksFromSource(input.file, input.source),
168
+ dependencies: extractArkOrderModuleImportsFromSource(input.file, input.source),
245
169
  layerForFile: () => input.fromLayer,
170
+ ...(input.classification ? { classification: input.classification } : {}),
246
171
  }).findings.filter((item) => item.sensor === 'arkorder-generic-update' ||
247
172
  item.sensor === 'arkorder-kernel-in-domain' ||
248
173
  item.sensor === 'arkorder-xi-field-write' ||
249
174
  item.sensor === 'arkorder-ingest-writes-xi' ||
250
- item.sensor === 'arkorder-too-many-params');
175
+ item.sensor === 'arkorder-too-many-params' ||
176
+ item.sensor === 'arkorder-xi-ttl' ||
177
+ item.sensor === 'arkorder-information-budget');
251
178
  }
@@ -9,6 +9,8 @@
9
9
  */
10
10
 
11
11
  export const DEFAULT_MAX_XI_KEYS = 7;
12
+ /** ξ keys that name freshness. Freshness is σ, never ξ (runtime and static sensor share this). */
13
+ export const XI_TTL_KEY_RE = /^(ttl|freshUntil|fresh_until|maxAge|max_age)$/i;
12
14
  export const INGEST_RESIDUAL_KINDS = ['absorb', 'escalate_up', 'hold'];
13
15
  export const INGEST_REASON_CODES = ['not-in-pattern', 'stale-sigma', 'pack', 'capacity'];
14
16
  export const CAPACITY_OPS = ['lte', 'lt', 'gte', 'gt'];
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkRunRuleId, } from './extra-merge-teeth.mjs';
12
+ import { unresolvableLayerFlowLayers } from './source-policy.mjs';
12
13
  export const ARK_RUN_DOCTOR_SCHEMA_VERSION = '1.0';
13
14
  const RESIDUAL_RULE_CAP = 12;
14
15
  /** Compact / details — only when `arkRun` is on. Absence stays silent. */
@@ -47,6 +48,12 @@ function extraFromConfig(arkRun) {
47
48
  */
48
49
  export function summarizeArkRunSection(input = {}) {
49
50
  const extra = extraFromConfig(input.arkRun);
51
+ const layerFlowUnresolvable = extra.present && Array.isArray(input.layerFlow?.layers)
52
+ ? unresolvableLayerFlowLayers({
53
+ layers: input.layerFlow.layers,
54
+ rules: Array.isArray(input.layerFlow.rules) ? input.layerFlow.rules : [],
55
+ })
56
+ : [];
50
57
  const uniqueIds = extra.present ? uniqueArkRunRuleIds(input.findings) : [];
51
58
  const ruleIds = uniqueIds.slice(0, RESIDUAL_RULE_CAP);
52
59
  const residualCount = uniqueIds.length;
@@ -100,6 +107,7 @@ export function summarizeArkRunSection(input = {}) {
100
107
  failMergeWhen: mergePlanes.failMergeWhen,
101
108
  note,
102
109
  mergePlanes,
110
+ layerFlowUnresolvable,
103
111
  };
104
112
  }
105
113
  /** Thin status slice — counts only; residual null means unknown, not green. */
@@ -144,6 +152,12 @@ export function formatArkRunDoctorLines(section) {
144
152
  else {
145
153
  lines.push('Residual: none on this scan (not a score — green extras ≠ finished kernel wiring).');
146
154
  }
155
+ const unresolvable = Array.isArray(section.layerFlowUnresolvable)
156
+ ? section.layerFlowUnresolvable
157
+ : [];
158
+ if (unresolvable.length > 0) {
159
+ lines.push(`Runtime layer flow: ${unresolvable.join(', ')} name(s) in a deny rule but map to no intent (no intentPrefixes, not a canonical layer). A kernel from this config cannot enforce those rules — add intentPrefixes (ARKRUN_LAYER_FLOW_UNRESOLVABLE).`);
160
+ }
147
161
  if (section.failMergeWhen)
148
162
  lines.push(section.failMergeWhen);
149
163
  return lines;