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
@@ -36,16 +36,21 @@ import {
36
36
  import { provePortProofInject } from './port-proof.mjs';
37
37
  import { extractClassShapesFromSource } from './arkrules-sensors.mjs';
38
38
  import {
39
+ arkRunEffectiveIntentPrefixes,
40
+ createArkRunKernelRootSpecifierMatcher,
39
41
  extractArkRunDeclarationsFromSource,
42
+ reexportsArkRunKernelRoot,
40
43
  extractArkRunKernelCallsFromSource,
41
44
  extractArkRunManagedNewsFromSource,
42
45
  } from './ark-run-facts.mjs';
43
46
  import {
47
+ extractArkOrderBudgetLeaksFromSource,
44
48
  extractArkOrderGenericUpdatesFromSource,
45
49
  extractArkOrderIngestWritesXiFromSource,
46
50
  extractArkOrderPlaneCallsFromSource,
47
51
  extractArkOrderReleaseKeyCountsFromSource,
48
52
  extractArkOrderXiFieldWritesFromSource,
53
+ extractArkOrderXiTtlKeysFromSource,
49
54
  } from './ark-order-facts.mjs';
50
55
  import {
51
56
  collectGovernedFiles,
@@ -53,7 +58,7 @@ import {
53
58
  normalize,
54
59
  } from './scan-files.mjs';
55
60
 
56
- export const RESOLVED_FACTS_RESOLVER_IDENTITY = 'arkgate-typescript-resolver@1';
61
+ const RESOLVED_FACTS_RESOLVER_IDENTITY = 'arkgate-typescript-resolver@1';
57
62
 
58
63
  const IN_MEMORY_STORES = new Set([
59
64
  'InMemoryAuditStore',
@@ -89,6 +94,17 @@ function canonicalProjectPath(value) {
89
94
  return normalized;
90
95
  }
91
96
 
97
+ /**
98
+ * `path.resolve` that reuses an equal string instead of allocating a copy, so
99
+ * one file's absolute/real path is one string across the resolver's maps.
100
+ * `sameAs` lets a real path that equals the lexical path share that instance.
101
+ */
102
+ function resolvedPath(value, sameAs) {
103
+ const resolved = path.resolve(value);
104
+ if (sameAs !== undefined && resolved === sameAs) return sameAs;
105
+ return resolved === value ? value : resolved;
106
+ }
107
+
92
108
  function isInsideRoot(root, target) {
93
109
  const relative = path.relative(root, target);
94
110
  return (
@@ -129,13 +145,13 @@ function readPackageName(root, observeInput) {
129
145
  function rememberCanonicalAlias(aliases, real, relative, absolute) {
130
146
  const current = aliases.get(real);
131
147
  if (!current || relative < current.relative) {
132
- aliases.set(real, { relative, absolute: path.resolve(absolute) });
148
+ aliases.set(real, { relative, absolute: resolvedPath(absolute) });
133
149
  }
134
150
  }
135
151
 
136
152
  function rememberDirectoryAlias(aliases, real, relative, absolute) {
137
153
  const current = aliases.get(real) ?? new Map();
138
- if (!current.has(relative)) current.set(relative, { relative, absolute: path.resolve(absolute) });
154
+ if (!current.has(relative)) current.set(relative, { relative, absolute: resolvedPath(absolute) });
139
155
  aliases.set(real, current);
140
156
  }
141
157
 
@@ -244,10 +260,20 @@ function discoverScopedFiles(root, config, scopeRelatives, observeInput) {
244
260
  );
245
261
  }
246
262
 
247
- function collectCandidateFiles(root, config, changes, observeInput, scopeRelatives) {
263
+ function collectCandidateFiles(
264
+ root,
265
+ config,
266
+ changes,
267
+ observeInput,
268
+ scopeRelatives,
269
+ { readContents = true } = {}
270
+ ) {
248
271
  const files = new Map();
249
272
  const directoryAliases = new Map();
250
273
  const expandedAliasDirectories = new Set();
274
+ // The walk already resolved each file's real path; reuse it instead of a
275
+ // second realpath per file.
276
+ const walkedRealpaths = new Map();
251
277
  const discovered = Array.isArray(scopeRelatives)
252
278
  ? discoverScopedFiles(root, config, scopeRelatives, observeInput)
253
279
  : (config.include ?? [])
@@ -259,13 +285,16 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
259
285
  if (relative === '.') relative = '';
260
286
  rememberDirectoryAlias(directoryAliases, real, relative, absolute);
261
287
  },
288
+ onFile(absolute, real) {
289
+ walkedRealpaths.set(absolute, real);
290
+ },
262
291
  })
263
292
  )
264
293
  .map((absolute) => {
265
294
  observeResolvedInput(observeInput, absolute, 'realpath');
266
295
  return {
267
296
  absolute,
268
- real: fs.realpathSync(absolute),
297
+ real: walkedRealpaths.get(absolute) ?? fs.realpathSync(absolute),
269
298
  relative: canonicalProjectPath(normalize(path.relative(root, absolute))),
270
299
  };
271
300
  })
@@ -306,12 +335,14 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
306
335
  }
307
336
  }
308
337
  for (const { absolute, real, relative } of canonicalByRealpath.values()) {
309
- observeResolvedInput(observeInput, absolute, 'source');
338
+ // Path-only callers (overlay canonicalization, tsconfig closure) never read
339
+ // source text: do not load every file's contents for them.
340
+ if (readContents) observeResolvedInput(observeInput, absolute, 'source');
310
341
  files.set(relative, {
311
342
  path: relative,
312
- absolute: path.resolve(absolute),
313
- real: path.resolve(real),
314
- content: fs.readFileSync(absolute, 'utf8'),
343
+ absolute: resolvedPath(absolute),
344
+ real: resolvedPath(real, absolute),
345
+ ...(readContents ? { content: fs.readFileSync(absolute, 'utf8') } : {}),
315
346
  });
316
347
  }
317
348
 
@@ -364,7 +395,9 @@ function collectCandidateFiles(root, config, changes, observeInput, scopeRelativ
364
395
  /** Canonicalize a Tooling overlay without exposing filesystem identity to Kernel. */
365
396
  export function canonicalizeCandidateChanges({ root, config, changes = [] }) {
366
397
  const canonicalRoot = fs.realpathSync(root);
367
- return collectCandidateFiles(canonicalRoot, config, changes).changes.map((change) =>
398
+ return collectCandidateFiles(canonicalRoot, config, changes, undefined, undefined, {
399
+ readContents: false,
400
+ }).changes.map((change) =>
368
401
  change.delete === true
369
402
  ? { path: change.path, delete: true }
370
403
  : { path: change.path, content: change.content }
@@ -390,13 +423,17 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
390
423
  const rememberVirtualDirectories = (absolute) => {
391
424
  let directory = path.dirname(absolute);
392
425
  while (isInsideRoot(root, directory)) {
393
- virtualDirectories.add(path.resolve(directory));
394
- if (path.resolve(directory) === root) break;
426
+ const resolved = path.resolve(directory);
427
+ // Every walk records the whole chain up to root, so a known directory
428
+ // already has all of its ancestors: stop instead of re-walking to root.
429
+ if (virtualDirectories.has(resolved) && resolved !== root) break;
430
+ virtualDirectories.add(resolved);
431
+ if (resolved === root) break;
395
432
  directory = path.dirname(directory);
396
433
  }
397
434
  };
398
435
  const remember = (absolute, file) => {
399
- const key = path.resolve(absolute);
436
+ const key = resolvedPath(absolute);
400
437
  byAbsolute.set(key, file);
401
438
  pathByAbsolute.set(key, file.path);
402
439
  rememberVirtualDirectories(key);
@@ -442,7 +479,9 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
442
479
  observeResolvedInput(observeInput, absolute, 'module-read');
443
480
  if (deleted.has(absolute)) return undefined;
444
481
  const candidate = byAbsolute.get(absolute);
445
- if (candidate) return candidate.content;
482
+ // Overlay candidates carry their proposed text. On-disk candidates are read
483
+ // lazily (their text is not held for the whole resolution).
484
+ if (typeof candidate?.content === 'string') return candidate.content;
446
485
  if (sys?.readFile) return sys.readFile(fileName);
447
486
  try {
448
487
  return fs.readFileSync(fileName, 'utf8');
@@ -495,6 +534,13 @@ function createOverlayModuleHost(ts, root, files, changes, observeInput) {
495
534
  };
496
535
 
497
536
  return {
537
+ /** Drop the per-file lookup tables once module resolution is finished. */
538
+ dispose() {
539
+ byAbsolute.clear();
540
+ pathByAbsolute.clear();
541
+ deleted.clear();
542
+ virtualDirectories.clear();
543
+ },
498
544
  fileExists,
499
545
  readFile,
500
546
  directoryExists,
@@ -597,7 +643,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
597
643
  const configByFile = new Map();
598
644
  const nearestConfigByDirectory = new Map();
599
645
  if (explicitPath) {
600
- for (const file of candidateFiles) configByFile.set(path.resolve(file.absolute), explicitPath);
646
+ for (const file of candidateFiles) configByFile.set(resolvedPath(file.absolute), explicitPath);
601
647
  } else if (!explicitPath) {
602
648
  for (const file of candidateFiles) {
603
649
  const nearest = nearestTsconfig(
@@ -606,7 +652,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
606
652
  nearestConfigByDirectory,
607
653
  observeInput
608
654
  );
609
- if (nearest) configByFile.set(path.resolve(file.absolute), nearest);
655
+ if (nearest) configByFile.set(resolvedPath(file.absolute), nearest);
610
656
  }
611
657
  }
612
658
  const configPaths = [...new Set(configByFile.values())].sort((left, right) => {
@@ -678,7 +724,7 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
678
724
  };
679
725
  for (const configPath of configPaths) loadConfigOptions(configPath);
680
726
 
681
- const optionsFor = (fileName) => {
727
+ const configPathFor = (fileName) => {
682
728
  const resolved = path.resolve(fileName);
683
729
  let configPath = configByFile.get(resolved);
684
730
  if (!configPath) {
@@ -686,6 +732,10 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
686
732
  ?? nearestTsconfig(root, resolved, nearestConfigByDirectory, observeInput);
687
733
  if (configPath) configByFile.set(resolved, configPath);
688
734
  }
735
+ return configPath;
736
+ };
737
+ const optionsFor = (fileName) => {
738
+ const configPath = configPathFor(fileName);
689
739
  return configPath ? loadConfigOptions(configPath) ?? {} : {};
690
740
  };
691
741
  const configs = configPaths.map((configPath) => ({
@@ -713,6 +763,8 @@ function compilerContext(ts, root, tsconfig, candidateFiles, observeInput) {
713
763
  .sort();
714
764
  return {
715
765
  optionsFor,
766
+ /** Options are a function of the governing tsconfig path ('' = none). */
767
+ optionsKeyFor: (fileName) => configPathFor(fileName) ?? '',
716
768
  reasons,
717
769
  configInputPaths,
718
770
  tsconfigHash: deterministicHash(stableSerialize(configClosure)),
@@ -734,7 +786,9 @@ export function resolvedCompilerInputPaths({
734
786
  if (!ts?.readConfigFile || !ts?.parseJsonConfigFileContent) return [];
735
787
  observeResolvedInput(observeInput, root, 'realpath');
736
788
  const canonicalRoot = fs.realpathSync(root);
737
- const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput);
789
+ const candidate = collectCandidateFiles(canonicalRoot, config, changes, observeInput, undefined, {
790
+ readContents: false,
791
+ });
738
792
  return compilerContext(ts, canonicalRoot, tsconfig, candidate.files, observeInput)
739
793
  .configInputPaths;
740
794
  }
@@ -941,7 +995,8 @@ function objectLiteralHasProperty(ts, object, name) {
941
995
  );
942
996
  }
943
997
 
944
- function tsSuppressionPositions(sourceFile, source) {
998
+ /** Start offsets of `@ts-ignore` directives and `@ts-nocheck` pragmas in a parsed file. */
999
+ export function tsSuppressionPositions(sourceFile, source) {
945
1000
  const positions = new Set();
946
1001
  for (const directive of sourceFile.commentDirectives ?? []) {
947
1002
  const start = directive.range?.pos;
@@ -958,28 +1013,21 @@ function tsSuppressionPositions(sourceFile, source) {
958
1013
  return [...positions];
959
1014
  }
960
1015
 
961
- function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
962
- const facts = tsSuppressionPositions(sourceFile, source).map((position) => ({
963
- file: relativePath,
964
- line: lineOf(sourceFile, position),
965
- kind: 'ts-suppression',
966
- }));
967
- for (const dependency of dependencies) {
968
- if (!dependency.unresolved) continue;
969
- facts.push({
970
- file: relativePath,
971
- line: dependency.line,
972
- kind: dependency.kind === 'require' ? 'dynamic-require' : 'dynamic-import',
973
- });
974
- }
1016
+ const ARKGATE_RUNTIME_SPECIFIER = /^arkgate(?:\/runtime)?$/;
975
1017
 
1018
+ /**
1019
+ * Local bindings of the arkgate kernel factories whose omitted options fall back
1020
+ * to in-memory stores: named imports (local name → factory) plus namespace
1021
+ * imports of `arkgate` / `arkgate/runtime`.
1022
+ */
1023
+ export function arkInMemoryFactoryBindings(ts, sourceFile) {
976
1024
  const importedFactories = new Map();
977
1025
  const arkNamespaces = new Set();
978
1026
  for (const statement of sourceFile.statements) {
979
1027
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) {
980
1028
  continue;
981
1029
  }
982
- if (!/^arkgate(?:\/runtime)?$/.test(statement.moduleSpecifier.text)) continue;
1030
+ if (!ARKGATE_RUNTIME_SPECIFIER.test(statement.moduleSpecifier.text)) continue;
983
1031
  const bindings = statement.importClause?.namedBindings;
984
1032
  if (bindings && ts.isNamespaceImport(bindings)) arkNamespaces.add(bindings.name.text);
985
1033
  if (!bindings || !ts.isNamedImports(bindings)) continue;
@@ -989,6 +1037,71 @@ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
989
1037
  if (requirements) importedFactories.set(element.name.text, { imported, requirements });
990
1038
  }
991
1039
  }
1040
+ return { importedFactories, arkNamespaces };
1041
+ }
1042
+
1043
+ /** Named `InMemory*Store` imports of an arkgate import declaration. */
1044
+ export function arkInMemoryStoreImports(ts, node) {
1045
+ const specifier = node.moduleSpecifier;
1046
+ if (!ts.isStringLiteralLike(specifier) || !ARKGATE_RUNTIME_SPECIFIER.test(specifier.text)) {
1047
+ return [];
1048
+ }
1049
+ const elements =
1050
+ node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
1051
+ ? node.importClause.namedBindings.elements
1052
+ : [];
1053
+ const out = [];
1054
+ for (const element of elements) {
1055
+ const imported = element.propertyName?.text ?? element.name.text;
1056
+ if (IN_MEMORY_STORES.has(imported)) out.push({ element, imported });
1057
+ }
1058
+ return out;
1059
+ }
1060
+
1061
+ /**
1062
+ * The factory a call expression invokes when that call definitely takes the
1063
+ * in-memory defaults (no options, `undefined`, or an object literal missing a
1064
+ * required store); otherwise undefined.
1065
+ */
1066
+ export function inMemoryDefaultsFactoryCall(ts, node, { importedFactories, arkNamespaces }) {
1067
+ let factory;
1068
+ if (ts.isIdentifier(node.expression)) {
1069
+ factory = importedFactories.get(node.expression.text);
1070
+ } else if (
1071
+ ts.isPropertyAccessExpression(node.expression) &&
1072
+ ts.isIdentifier(node.expression.expression) &&
1073
+ arkNamespaces.has(node.expression.expression.text)
1074
+ ) {
1075
+ const imported = node.expression.name.text;
1076
+ const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
1077
+ if (requirements) factory = { imported, requirements };
1078
+ }
1079
+ if (!factory) return undefined;
1080
+ const options = node.arguments[0];
1081
+ const definitelyDefaults =
1082
+ !options ||
1083
+ (ts.isIdentifier(options) && options.text === 'undefined') ||
1084
+ (ts.isObjectLiteralExpression(options) &&
1085
+ factory.requirements.some((name) => !objectLiteralHasProperty(ts, options, name)));
1086
+ return definitelyDefaults ? factory : undefined;
1087
+ }
1088
+
1089
+ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1090
+ const facts = tsSuppressionPositions(sourceFile, source).map((position) => ({
1091
+ file: relativePath,
1092
+ line: lineOf(sourceFile, position),
1093
+ kind: 'ts-suppression',
1094
+ }));
1095
+ for (const dependency of dependencies) {
1096
+ if (!dependency.unresolved) continue;
1097
+ facts.push({
1098
+ file: relativePath,
1099
+ line: dependency.line,
1100
+ kind: dependency.kind === 'require' ? 'dynamic-require' : 'dynamic-import',
1101
+ });
1102
+ }
1103
+
1104
+ const factoryBindings = arkInMemoryFactoryBindings(ts, sourceFile);
992
1105
 
993
1106
  const visit = (node) => {
994
1107
  if (
@@ -1003,56 +1116,25 @@ function collectSafetyUses(ts, sourceFile, relativePath, source, dependencies) {
1003
1116
  }
1004
1117
 
1005
1118
  if (ts.isImportDeclaration(node)) {
1006
- const specifier = node.moduleSpecifier;
1007
- const fromArk =
1008
- ts.isStringLiteralLike(specifier) && /^arkgate(?:\/runtime)?$/.test(specifier.text);
1009
- if (fromArk) {
1010
- const elements =
1011
- node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
1012
- ? node.importClause.namedBindings.elements
1013
- : [];
1014
- for (const element of elements) {
1015
- const imported = element.propertyName?.text ?? element.name.text;
1016
- if (IN_MEMORY_STORES.has(imported)) {
1017
- facts.push({
1018
- file: relativePath,
1019
- line: lineOf(sourceFile, element.getStart(sourceFile)),
1020
- kind: 'in-memory-store',
1021
- symbol: imported,
1022
- });
1023
- }
1024
- }
1119
+ for (const { element, imported } of arkInMemoryStoreImports(ts, node)) {
1120
+ facts.push({
1121
+ file: relativePath,
1122
+ line: lineOf(sourceFile, element.getStart(sourceFile)),
1123
+ kind: 'in-memory-store',
1124
+ symbol: imported,
1125
+ });
1025
1126
  }
1026
1127
  }
1027
1128
 
1028
1129
  if (ts.isCallExpression(node)) {
1029
- let factory;
1030
- if (ts.isIdentifier(node.expression)) {
1031
- factory = importedFactories.get(node.expression.text);
1032
- } else if (
1033
- ts.isPropertyAccessExpression(node.expression) &&
1034
- ts.isIdentifier(node.expression.expression) &&
1035
- arkNamespaces.has(node.expression.expression.text)
1036
- ) {
1037
- const imported = node.expression.name.text;
1038
- const requirements = IN_MEMORY_DEFAULT_FACTORIES.get(imported);
1039
- if (requirements) factory = { imported, requirements };
1040
- }
1130
+ const factory = inMemoryDefaultsFactoryCall(ts, node, factoryBindings);
1041
1131
  if (factory) {
1042
- const options = node.arguments[0];
1043
- const definitelyDefaults =
1044
- !options ||
1045
- (ts.isIdentifier(options) && options.text === 'undefined') ||
1046
- (ts.isObjectLiteralExpression(options) &&
1047
- factory.requirements.some((name) => !objectLiteralHasProperty(ts, options, name)));
1048
- if (definitelyDefaults) {
1049
- facts.push({
1050
- file: relativePath,
1051
- line: lineOf(sourceFile, node.getStart(sourceFile)),
1052
- kind: 'in-memory-store',
1053
- symbol: `${factory.imported} defaults`,
1054
- });
1055
- }
1132
+ facts.push({
1133
+ file: relativePath,
1134
+ line: lineOf(sourceFile, node.getStart(sourceFile)),
1135
+ kind: 'in-memory-store',
1136
+ symbol: `${factory.imported} defaults`,
1137
+ });
1056
1138
  }
1057
1139
  }
1058
1140
  ts.forEachChild(node, visit);
@@ -1081,6 +1163,30 @@ function unavailableFacts(config, ts, reason) {
1081
1163
  });
1082
1164
  }
1083
1165
 
1166
+ const SOURCE_READ_FAILURE = Symbol('ark.sourceReadFailure');
1167
+
1168
+ /**
1169
+ * Share per-file parse results between resolutions of one config (a base tree
1170
+ * and its overlay candidate). Pass the same object to both calls; it is only
1171
+ * used while `config` is the identical object.
1172
+ */
1173
+ export function createResolverIngestCache(config) {
1174
+ return { config, records: new Map() };
1175
+ }
1176
+
1177
+ /** Overlay text when present; otherwise read the on-disk source now (not held afterwards). */
1178
+ function readCandidateSource(candidate, observeInput) {
1179
+ if (typeof candidate.content === 'string') return candidate.content;
1180
+ observeResolvedInput(observeInput, candidate.absolute, 'source');
1181
+ try {
1182
+ return fs.readFileSync(candidate.absolute, 'utf8');
1183
+ } catch (error) {
1184
+ const failure = error instanceof Error ? error : new Error(String(error));
1185
+ failure[SOURCE_READ_FAILURE] = true;
1186
+ throw failure;
1187
+ }
1188
+ }
1189
+
1084
1190
  function loadOneCandidateFile(root, config, relative, observeInput) {
1085
1191
  const discovered = discoverScopedFiles(root, config, [relative], observeInput);
1086
1192
  const found = discovered[0];
@@ -1088,8 +1194,8 @@ function loadOneCandidateFile(root, config, relative, observeInput) {
1088
1194
  observeResolvedInput(observeInput, found.absolute, 'source');
1089
1195
  return {
1090
1196
  path: found.relative,
1091
- absolute: path.resolve(found.absolute),
1092
- real: path.resolve(found.real),
1197
+ absolute: resolvedPath(found.absolute),
1198
+ real: resolvedPath(found.real, found.absolute),
1093
1199
  content: fs.readFileSync(found.absolute, 'utf8'),
1094
1200
  };
1095
1201
  }
@@ -1103,6 +1209,7 @@ export function resolveCandidateFacts({
1103
1209
  changes = [],
1104
1210
  observeInput,
1105
1211
  scopeFiles,
1212
+ ingestCache,
1106
1213
  }) {
1107
1214
  if (!ts?.createSourceFile || !ts?.resolveModuleName) {
1108
1215
  return unavailableFacts(config, ts, 'No API-compatible TypeScript resolver is available.');
@@ -1116,12 +1223,15 @@ export function resolveCandidateFacts({
1116
1223
  try {
1117
1224
  observeResolvedInput(observeInput, root, 'realpath');
1118
1225
  canonicalRoot = fs.realpathSync(root);
1226
+ // Source text is read one file at a time in `ingest` and released after it,
1227
+ // instead of holding every file's text for the whole resolution.
1119
1228
  const candidate = collectCandidateFiles(
1120
1229
  canonicalRoot,
1121
1230
  config,
1122
1231
  changes,
1123
1232
  observeInput,
1124
- scoped ? scopeFiles : undefined
1233
+ scoped ? scopeFiles : undefined,
1234
+ { readContents: false }
1125
1235
  );
1126
1236
  candidateFiles = candidate.files;
1127
1237
  canonicalChanges = candidate.changes;
@@ -1141,6 +1251,28 @@ export function resolveCandidateFacts({
1141
1251
  canonicalChanges,
1142
1252
  observeInput
1143
1253
  );
1254
+ // TypeScript resolves a specifier from its containing directory (its own
1255
+ // module cache is keyed the same way), and this run's host never changes.
1256
+ // Resolve each (tsconfig, directory, specifier) once instead of once per
1257
+ // importing file: a shared `react` or `@/lib/x` no longer re-walks every
1258
+ // ancestor node_modules for each file. Only the small verdict is kept.
1259
+ const resolutionMemo = new Map();
1260
+ const resolveOnce = (dependency, containingFile) => {
1261
+ if (!dependency.specifier) return resolveDependency(ts, dependency, containingFile, {}, host);
1262
+ const key = `${compiler.optionsKeyFor(containingFile)}\0${path.dirname(containingFile)}\0${dependency.specifier}`;
1263
+ let resolved = resolutionMemo.get(key);
1264
+ if (!resolved) {
1265
+ resolved = resolveDependency(
1266
+ ts,
1267
+ dependency,
1268
+ containingFile,
1269
+ compiler.optionsFor(containingFile),
1270
+ host
1271
+ );
1272
+ resolutionMemo.set(key, resolved);
1273
+ }
1274
+ return resolved;
1275
+ };
1144
1276
  const forbiddenGlobals = [
1145
1277
  ...new Set([
1146
1278
  ...AMBIENT_CAPABILITY_ENTRIES,
@@ -1168,6 +1300,8 @@ export function resolveCandidateFacts({
1168
1300
  const arkOrderXiFieldWrites = [];
1169
1301
  const arkOrderIngestWritesXi = [];
1170
1302
  const arkOrderReleaseKeyCounts = [];
1303
+ const arkOrderXiTtlKeys = [];
1304
+ const arkOrderBudgetLeaks = [];
1171
1305
  const arkRunActive = config.arkRun != null && typeof config.arkRun === 'object';
1172
1306
  const arkOrderActive = config.arkOrder != null && typeof config.arkOrder === 'object';
1173
1307
  const arkRulesActive =
@@ -1178,13 +1312,21 @@ export function resolveCandidateFacts({
1178
1312
  const compositionRootPatterns = arkRunActive
1179
1313
  ? [...(config.arkRun?.compositionRoots ?? [])]
1180
1314
  : [];
1315
+ // Identifiers imported from a kernel root module are traced kernel receivers.
1316
+ const kernelRootPatterns = arkRunActive
1317
+ ? [...(config.arkRun?.kernelRoots ?? config.arkRun?.compositionRoots ?? [])]
1318
+ : [];
1181
1319
  const planeRootPatterns = arkOrderActive ? [...(config.arkOrder?.planeRoots ?? [])] : [];
1182
1320
 
1183
1321
  const seedPathSet = new Set(candidateFiles.map((file) => file.path));
1184
- const ingest = (candidate, fullExtract) => {
1322
+ const ingestFresh = (candidate, fullExtract, knownContent) => {
1323
+ const content = knownContent ?? readCandidateSource(candidate, observeInput);
1324
+ // Keep the text only when a later pass re-reads it (ArkRun kernel calls, kernel-root
1325
+ // barrels and managed-new facts).
1326
+ if (arkRunActive && candidate.content === undefined) candidate.content = content;
1185
1327
  const sourceFile = ts.createSourceFile(
1186
1328
  candidate.absolute,
1187
- candidate.content,
1329
+ content,
1188
1330
  ts.ScriptTarget.Latest,
1189
1331
  true,
1190
1332
  ts.getScriptKindFromFileName?.(candidate.absolute)
@@ -1199,10 +1341,10 @@ export function resolveCandidateFacts({
1199
1341
  namedBindings: namedModuleBindings(ts, node),
1200
1342
  }));
1201
1343
  let portProofEligible = false;
1202
- if (/\bimport\s*\{[^}]+\}\s*from\s*['"]\.\.?\//.test(candidate.content)) {
1344
+ if (/\bimport\s*\{[^}]+\}\s*from\s*['"]\.\.?\//.test(content)) {
1203
1345
  try {
1204
1346
  portProofEligible = Boolean(
1205
- provePortProofInject(ts, candidate.content, { filePath: candidate.absolute, sourceFile })
1347
+ provePortProofInject(ts, content, { filePath: candidate.absolute, sourceFile })
1206
1348
  .eligible
1207
1349
  );
1208
1350
  } catch {
@@ -1219,7 +1361,7 @@ export function resolveCandidateFacts({
1219
1361
  });
1220
1362
  files.push({
1221
1363
  path: candidate.path,
1222
- contentHash: deterministicHash(candidate.content),
1364
+ contentHash: deterministicHash(content),
1223
1365
  parseStatus: parseDiagnosticCount === 0 ? 'parsed' : 'invalid',
1224
1366
  parseDiagnosticCount,
1225
1367
  exportsOnlyTypes,
@@ -1260,7 +1402,7 @@ export function resolveCandidateFacts({
1260
1402
  publishCalls.push(...policy.publishCalls);
1261
1403
  intentReferences.push(...policy.intentReferences);
1262
1404
  safetyUses.push(
1263
- ...collectSafetyUses(ts, sourceFile, candidate.path, candidate.content, semanticDependencies)
1405
+ ...collectSafetyUses(ts, sourceFile, candidate.path, content, semanticDependencies)
1264
1406
  );
1265
1407
  // Class-shape extraction is text-conservative (false negatives over false positives).
1266
1408
  // Only TS/TSX candidates; sensors consume the same shape via facts.classShapes.
@@ -1268,22 +1410,17 @@ export function resolveCandidateFacts({
1268
1410
  if (/\.(tsx?|mts|cts)$/i.test(candidate.path)) {
1269
1411
  if (arkRulesActive || arkRunActive) {
1270
1412
  try {
1271
- classShapes.push(...extractClassShapesFromSource(candidate.path, candidate.content));
1413
+ classShapes.push(...extractClassShapesFromSource(candidate.path, content));
1272
1414
  } catch {
1273
1415
  // Never fail the resolver for shape extraction; sensors stay silent on this file.
1274
1416
  }
1275
1417
  }
1276
1418
  if (arkRunActive) {
1277
- try {
1278
- arkRunKernelCalls.push(
1279
- ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content)
1280
- );
1281
- } catch {
1282
- // Never fail the resolver for ArkRun call extraction.
1283
- }
1419
+ // Kernel calls are extracted after every file is parsed, so imports of a
1420
+ // kernel root resolve through tsconfig paths and barrel re-exports.
1284
1421
  try {
1285
1422
  arkRunDeclarations.push(
1286
- ...extractArkRunDeclarationsFromSource(candidate.path, candidate.content)
1423
+ ...extractArkRunDeclarationsFromSource(candidate.path, content)
1287
1424
  );
1288
1425
  } catch {
1289
1426
  // Never fail the resolver for ArkRun declaration extraction.
@@ -1292,44 +1429,125 @@ export function resolveCandidateFacts({
1292
1429
  if (arkOrderActive) {
1293
1430
  try {
1294
1431
  arkOrderPlaneCalls.push(
1295
- ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1432
+ ...extractArkOrderPlaneCallsFromSource(candidate.path, content)
1296
1433
  );
1297
1434
  } catch {
1298
1435
  // Never fail the resolver for ArkOrder factory extraction.
1299
1436
  }
1300
1437
  try {
1301
1438
  arkOrderGenericUpdates.push(
1302
- ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1439
+ ...extractArkOrderGenericUpdatesFromSource(candidate.path, content, {
1440
+ planeRoots: planeRootPatterns,
1441
+ })
1303
1442
  );
1304
1443
  } catch {
1305
1444
  // Never fail the resolver for ArkOrder generic-update extraction.
1306
1445
  }
1307
1446
  try {
1308
1447
  arkOrderXiFieldWrites.push(
1309
- ...extractArkOrderXiFieldWritesFromSource(candidate.path, candidate.content, xiKeys)
1448
+ ...extractArkOrderXiFieldWritesFromSource(candidate.path, content, xiKeys)
1310
1449
  );
1311
1450
  } catch {
1312
1451
  // Never fail the resolver for ArkOrder xi-field-write extraction.
1313
1452
  }
1314
1453
  try {
1315
1454
  arkOrderIngestWritesXi.push(
1316
- ...extractArkOrderIngestWritesXiFromSource(candidate.path, candidate.content)
1455
+ ...extractArkOrderIngestWritesXiFromSource(candidate.path, content)
1317
1456
  );
1318
1457
  } catch {
1319
1458
  // Never fail the resolver for ArkOrder ingest-writes-ξ extraction.
1320
1459
  }
1321
1460
  try {
1322
1461
  arkOrderReleaseKeyCounts.push(
1323
- ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, candidate.content)
1462
+ ...extractArkOrderReleaseKeyCountsFromSource(candidate.path, content)
1324
1463
  );
1325
1464
  } catch {
1326
1465
  // Never fail the resolver for ArkOrder release key-count extraction.
1327
1466
  }
1467
+ try {
1468
+ arkOrderXiTtlKeys.push(
1469
+ ...extractArkOrderXiTtlKeysFromSource(candidate.path, content, {
1470
+ planeRoots: planeRootPatterns,
1471
+ })
1472
+ );
1473
+ } catch {
1474
+ // Never fail the resolver for ArkOrder ξ-TTL extraction.
1475
+ }
1476
+ try {
1477
+ arkOrderBudgetLeaks.push(
1478
+ ...extractArkOrderBudgetLeaksFromSource(candidate.path, content)
1479
+ );
1480
+ } catch {
1481
+ // Never fail the resolver for ArkOrder information-budget extraction.
1482
+ }
1328
1483
  }
1329
1484
  }
1330
1485
  };
1331
1486
 
1332
- for (const candidate of candidateFiles) ingest(candidate, true);
1487
+ // Per-file facts depend only on (config, path, text). A base/candidate pair
1488
+ // (atomic preflight) shares one cache: every file the overlay does not touch
1489
+ // is parsed once, not twice. Overlay files are never cached or reused.
1490
+ const accumulators = {
1491
+ completenessReasons,
1492
+ files,
1493
+ capabilityUses,
1494
+ ambientUses,
1495
+ publishCalls,
1496
+ intentReferences,
1497
+ safetyUses,
1498
+ classShapes,
1499
+ arkRunDeclarations,
1500
+ arkOrderPlaneCalls,
1501
+ arkOrderGenericUpdates,
1502
+ arkOrderXiFieldWrites,
1503
+ arkOrderIngestWritesXi,
1504
+ arkOrderReleaseKeyCounts,
1505
+ arkOrderXiTtlKeys,
1506
+ arkOrderBudgetLeaks,
1507
+ };
1508
+ const accumulatorNames = Object.keys(accumulators);
1509
+ const reusable = ingestCache?.config === config ? ingestCache.records : undefined;
1510
+ const ingest = (candidate, fullExtract) => {
1511
+ if (!reusable || !fullExtract || typeof candidate.content === 'string') {
1512
+ ingestFresh(candidate, fullExtract);
1513
+ return;
1514
+ }
1515
+ const content = readCandidateSource(candidate, observeInput);
1516
+ const contentHash = deterministicHash(content);
1517
+ const cached = reusable.get(candidate.path);
1518
+ if (cached && cached.absolute === candidate.absolute && cached.contentHash === contentHash) {
1519
+ if (arkRunActive) candidate.content = content;
1520
+ parsed.set(candidate.path, { ...cached.parsed, candidate });
1521
+ for (const name of accumulatorNames) {
1522
+ const slice = cached.slices[name];
1523
+ if (slice) accumulators[name].push(...slice);
1524
+ }
1525
+ return;
1526
+ }
1527
+ const before = accumulatorNames.map((name) => accumulators[name].length);
1528
+ ingestFresh(candidate, fullExtract, content);
1529
+ const slices = {};
1530
+ accumulatorNames.forEach((name, index) => {
1531
+ if (accumulators[name].length > before[index]) {
1532
+ slices[name] = accumulators[name].slice(before[index]);
1533
+ }
1534
+ });
1535
+ const { candidate: _candidate, ...parsedFacts } = parsed.get(candidate.path);
1536
+ reusable.set(candidate.path, {
1537
+ absolute: candidate.absolute,
1538
+ contentHash,
1539
+ parsed: parsedFacts,
1540
+ slices,
1541
+ });
1542
+ };
1543
+
1544
+ try {
1545
+ for (const candidate of candidateFiles) ingest(candidate, true);
1546
+ } catch (error) {
1547
+ // Same outcome as the former eager read inside the discovery try-block.
1548
+ if (error?.[SOURCE_READ_FAILURE] !== true) throw error;
1549
+ return unavailableFacts(config, ts, error.message);
1550
+ }
1333
1551
 
1334
1552
  if (scoped) {
1335
1553
  const loaded = new Set(candidateFiles.map((file) => file.path));
@@ -1340,13 +1558,7 @@ export function resolveCandidateFacts({
1340
1558
  const parsedFile = parsed.get(source.path);
1341
1559
  if (!parsedFile) continue;
1342
1560
  for (const dependency of parsedFile.dependencies) {
1343
- const resolved = resolveDependency(
1344
- ts,
1345
- dependency,
1346
- parsedFile.candidate.absolute,
1347
- compiler.optionsFor(parsedFile.candidate.absolute),
1348
- host
1349
- );
1561
+ const resolved = resolveOnce(dependency, parsedFile.candidate.absolute);
1350
1562
  if (resolved.resolution !== 'resolved-project' || !resolved.target) continue;
1351
1563
  if (loaded.has(resolved.target)) continue;
1352
1564
  const extra = loadOneCandidateFile(
@@ -1369,6 +1581,27 @@ export function resolveCandidateFacts({
1369
1581
  const extractCandidates = scoped
1370
1582
  ? candidateFiles.filter((file) => seedPathSet.has(file.path))
1371
1583
  : candidateFiles;
1584
+ if (arkRunActive) {
1585
+ const rootMatcherFor = arkRunKernelRootMatchers({
1586
+ parsed,
1587
+ patterns: kernelRootPatterns,
1588
+ resolveTarget: (file, dependency) => resolveOnce(dependency, file.candidate.absolute),
1589
+ });
1590
+ const intentPrefixes = arkRunEffectiveIntentPrefixes(config);
1591
+ for (const candidate of extractCandidates) {
1592
+ if (!/\.(tsx?|mts|cts)$/i.test(candidate.path)) continue;
1593
+ try {
1594
+ arkRunKernelCalls.push(
1595
+ ...extractArkRunKernelCallsFromSource(candidate.path, candidate.content, {
1596
+ isKernelRootSpecifier: rootMatcherFor(candidate.path),
1597
+ intentPrefixes,
1598
+ })
1599
+ );
1600
+ } catch {
1601
+ // Never fail the resolver for ArkRun call extraction.
1602
+ }
1603
+ }
1604
+ }
1372
1605
  const admittedTypeNames = new Set(classShapes.map((shape) => shape.className));
1373
1606
  if (arkRunActive) {
1374
1607
  for (const candidate of extractCandidates) {
@@ -1428,13 +1661,7 @@ export function resolveCandidateFacts({
1428
1661
  const dependencies = [];
1429
1662
  for (const source of parsed.values()) {
1430
1663
  for (const dependency of source.dependencies) {
1431
- const resolved = resolveDependency(
1432
- ts,
1433
- dependency,
1434
- source.candidate.absolute,
1435
- compiler.optionsFor(source.candidate.absolute),
1436
- host
1437
- );
1664
+ const resolved = resolveOnce(dependency, source.candidate.absolute);
1438
1665
  if (resolved.resolverFailed) {
1439
1666
  completenessReasons.push({
1440
1667
  code: 'MODULE_RESOLUTION_FAILURE',
@@ -1486,6 +1713,11 @@ export function resolveCandidateFacts({
1486
1713
  }
1487
1714
 
1488
1715
  const projectPackageName = readPackageName(canonicalRoot, observeInput);
1716
+ // Canonicalization copies every fact. Release the per-file parse records and
1717
+ // the module host (closures keep them reachable) before that copy is built.
1718
+ parsed.clear();
1719
+ resolutionMemo.clear();
1720
+ host.dispose();
1489
1721
  return createTrustedResolvedCandidateFacts({
1490
1722
  schemaVersion: '1.2',
1491
1723
  completeness: completenessReasons.length === 0 ? 'complete' : 'partial',
@@ -1514,5 +1746,61 @@ export function resolveCandidateFacts({
1514
1746
  arkOrderXiFieldWrites,
1515
1747
  arkOrderIngestWritesXi,
1516
1748
  arkOrderReleaseKeyCounts,
1749
+ arkOrderXiTtlKeys,
1750
+ arkOrderBudgetLeaks,
1517
1751
  });
1518
1752
  }
1753
+
1754
+ /**
1755
+ * Per-file predicate: does an import specifier reach an ArkRun kernel root?
1756
+ * Uses real module resolution (tsconfig `paths`, package links) plus the
1757
+ * relative-glob fallback, and treats barrels that re-export a root as roots
1758
+ * (fixpoint), so `import { ark } from '@/main'` or a re-exporting index is traced.
1759
+ */
1760
+ function arkRunKernelRootMatchers({ parsed, patterns, resolveTarget }) {
1761
+ const regexes = [];
1762
+ for (const pattern of patterns) {
1763
+ try {
1764
+ regexes.push(globToRegExp(pattern));
1765
+ } catch {
1766
+ // Invalid glob: the relative fallback still applies.
1767
+ }
1768
+ }
1769
+ const roots = new Set([...parsed.keys()].filter((file) => regexes.some((re) => re.test(file))));
1770
+ const targets = new Map();
1771
+ if (patterns.length > 0) {
1772
+ for (const [file, info] of parsed) {
1773
+ const bySpecifier = new Map();
1774
+ for (const dependency of info.dependencies) {
1775
+ if (!dependency.specifier || (dependency.kind !== 'import' && dependency.kind !== 'export')) {
1776
+ continue;
1777
+ }
1778
+ if (bySpecifier.has(dependency.specifier)) continue;
1779
+ const resolved = resolveTarget(info, dependency);
1780
+ if (resolved.resolution === 'resolved-project' && resolved.target) {
1781
+ bySpecifier.set(dependency.specifier, resolved.target);
1782
+ }
1783
+ }
1784
+ targets.set(file, bySpecifier);
1785
+ }
1786
+ }
1787
+ const matcherFor = (file) => {
1788
+ const relative = createArkRunKernelRootSpecifierMatcher(file, patterns);
1789
+ const bySpecifier = targets.get(file);
1790
+ return (specifier) => {
1791
+ const target = bySpecifier?.get(specifier);
1792
+ return target !== undefined ? roots.has(target) : relative(specifier);
1793
+ };
1794
+ };
1795
+ for (let changed = patterns.length > 0; changed; ) {
1796
+ changed = false;
1797
+ for (const [file, info] of parsed) {
1798
+ if (roots.has(file)) continue;
1799
+ if (reexportsArkRunKernelRoot(info.candidate.content, matcherFor(file))) {
1800
+ roots.add(file);
1801
+ changed = true;
1802
+ }
1803
+ }
1804
+ }
1805
+ return matcherFor;
1806
+ }