arkgate 4.8.23 → 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 (131) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +24 -77
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +596 -112
  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 -493
  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 +16 -9
  35. package/bin/lib/doctor-advisories.mjs +25 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +6 -8
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-profiles.mjs +40 -4
  40. package/bin/lib/first-run-help.mjs +3 -2
  41. package/bin/lib/flat-parent-importers.mjs +137 -0
  42. package/bin/lib/flat-parent-pilot.mjs +40 -100
  43. package/bin/lib/gate-files.mjs +29 -2
  44. package/bin/lib/git-change-scope.mjs +109 -0
  45. package/bin/lib/github-enforcement.mjs +1 -1
  46. package/bin/lib/hook-templates.mjs +18 -16
  47. package/bin/lib/host-support-matrix.mjs +3 -1
  48. package/bin/lib/import-resolve.mjs +13 -1
  49. package/bin/lib/install-migrate.mjs +50 -73
  50. package/bin/lib/invariant-coverage.mjs +59 -11
  51. package/bin/lib/mcp-adoption.mjs +9 -1
  52. package/bin/lib/mcp-hook-payload.mjs +401 -61
  53. package/bin/lib/package-manager.mjs +59 -1
  54. package/bin/lib/passthrough-commands.mjs +83 -0
  55. package/bin/lib/persistence-write-hint.mjs +81 -0
  56. package/bin/lib/pin-dependent-gates.mjs +117 -0
  57. package/bin/lib/policy-delta-io.mjs +168 -10
  58. package/bin/lib/prepare-change.mjs +120 -6
  59. package/bin/lib/prepare-write.mjs +1 -0
  60. package/bin/lib/presets.mjs +3 -5
  61. package/bin/lib/remediation.mjs +23 -4
  62. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  63. package/bin/lib/rules-inventory.mjs +22 -3
  64. package/bin/lib/rules-under-contract.mjs +179 -5
  65. package/bin/lib/scan-files.mjs +32 -0
  66. package/bin/lib/snippet-analysis.mjs +140 -6
  67. package/bin/lib/source-policy.mjs +34 -0
  68. package/bin/lib/stable-hash.mjs +90 -0
  69. package/bin/lib/start-install-recovery.mjs +71 -1
  70. package/bin/lib/start-preview.mjs +30 -0
  71. package/bin/lib/status-command.mjs +33 -4
  72. package/bin/lib/status-manifest.mjs +32 -4
  73. package/bin/lib/team-parliament-io.mjs +77 -75
  74. package/bin/lib/team-parliament.mjs +6 -1
  75. package/bin/lib/write-path-capabilities.mjs +4 -2
  76. package/dist/chunk-A5EIY3HI.cjs +5 -0
  77. package/dist/chunk-MBXCFLO4.js +16 -0
  78. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  79. package/dist/chunk-SVSQVGFJ.js +5 -0
  80. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
  81. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  82. package/dist/eslint/index.cjs +17 -4
  83. package/dist/eslint/index.d.cts +359 -0
  84. package/dist/eslint/index.d.ts +130 -156
  85. package/dist/eslint/index.js +10 -4
  86. package/dist/index.cjs +35 -34
  87. package/dist/index.d.cts +5475 -0
  88. package/dist/index.d.ts +3312 -299
  89. package/dist/index.js +35 -34
  90. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  91. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  92. package/dist/nestjs/index.cjs +1 -18
  93. package/dist/nestjs/index.d.cts +22 -0
  94. package/dist/nestjs/index.d.ts +1 -3
  95. package/dist/nestjs/index.js +1 -18
  96. package/dist/order/index.cjs +1 -1
  97. package/dist/order/index.d.cts +155 -0
  98. package/dist/order/index.d.ts +15 -3
  99. package/dist/order/index.js +1 -1
  100. package/dist/runtime/index.cjs +13 -30
  101. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
  102. package/dist/runtime/index.d.ts +2678 -108
  103. package/dist/runtime/index.js +13 -30
  104. package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
  105. package/dist/types-DRinG-vT.d.ts +1792 -0
  106. package/docs/README.md +2 -1
  107. package/docs/agent-guide.md +84 -15
  108. package/docs/ai-gates.md +160 -26
  109. package/docs/arkorder.md +32 -15
  110. package/docs/brownfield-adoption.md +5 -1
  111. package/docs/configuration.md +196 -26
  112. package/docs/develop.md +2 -1
  113. package/docs/diagnostics.md +96 -19
  114. package/docs/package-surface.md +31 -8
  115. package/docs/product-voice.md +1 -1
  116. package/package.json +59 -18
  117. package/schemas/ark.analysis-result.schema.json +15 -0
  118. package/schemas/ark.config.schema.json +47 -10
  119. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  120. package/schemas/ark.status-manifest.schema.json +22 -0
  121. package/server.json +3 -9
  122. package/templates/agent-skills/README.md +1 -1
  123. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  124. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  125. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  126. package/templates/arkrules/PersistenceAdapters.json +1 -1
  127. package/templates/arkrules/PresentationAdapters.json +1 -1
  128. package/templates/skills/ark-adopt.md +4 -1
  129. package/templates/skills/ark-order.md +4 -2
  130. package/templates/skills/ark-runtime.md +6 -1
  131. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -8,12 +8,40 @@
8
8
  * Pure CLI helper (bin/lib/ark-run-facts.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { DEFAULT_INTENT_PREFIXES, effectiveIntentPrefixes, looksLikeArkIntent } from './source-policy.mjs';
12
+ /** Closed factory names from ADR 0022 `arkrun-missing-root`. */
11
13
  export const ARKRUN_KERNEL_FACTORY_CALLEES = [
12
14
  'createArkKernel',
13
15
  'createStrictArkKernel',
16
+ 'createLenientArkKernel',
14
17
  'createArkKernelFromConfig',
15
18
  'createStrictArkKernelFromConfig',
19
+ 'createLenientArkKernelFromConfig',
16
20
  ];
21
+ /**
22
+ * NestJS adapter factory sites: `ArkModule.forRoot()` / `ArkModule.forRootAsync()`
23
+ * count as a kernel factory only when `ArkModule` is imported from a kernel module
24
+ * (`arkgate/nestjs`). A bare `.forRoot(` on any other receiver never counts.
25
+ */
26
+ export const ARKRUN_NEST_KERNEL_MODULE = 'ArkModule';
27
+ export const ARKRUN_NEST_FACTORY_METHODS = ['forRoot', 'forRootAsync'];
28
+ /**
29
+ * Kernel types whose annotated bindings (`ark: ArkKernel`, constructor-injected
30
+ * `private readonly ark: ArkKernel`) are traced as kernel receivers when the type
31
+ * is imported from a kernel module.
32
+ */
33
+ export const ARKRUN_KERNEL_RECEIVER_TYPES = [
34
+ 'ArkKernel',
35
+ 'EventBus',
36
+ 'EventPublisher',
37
+ 'ArkRunPublisher',
38
+ ];
39
+ /** Kernel members that still reach the kernel's interaction API (`ark.eventBus.publish`). */
40
+ const KERNEL_TRANSIT_MEMBERS = new Set(['eventBus']);
41
+ /** Members whose value is a kernel-bound interaction object (`const pub = ark.publisher(..)`). */
42
+ const KERNEL_DERIVED_MEMBERS = new Set(['publisher', 'eventBus']);
43
+ const NEST_FACTORY_METHODS = new Set(ARKRUN_NEST_FACTORY_METHODS);
44
+ const RECEIVER_TYPES = new Set(ARKRUN_KERNEL_RECEIVER_TYPES);
17
45
  /** Closed interaction callees from ADR 0022 undeclared-emit/handle/depend. */
18
46
  export const ARKRUN_KERNEL_INTERACTION_CALLEES = [
19
47
  'publisher',
@@ -91,7 +119,48 @@ export function isArkRunKernelModuleSpecifier(specifier) {
91
119
  return (specifier === '@arkgate/runtime' ||
92
120
  specifier.startsWith('@arkgate/runtime/') ||
93
121
  specifier === 'arkgate/runtime' ||
94
- specifier.startsWith('arkgate/runtime/'));
122
+ specifier.startsWith('arkgate/runtime/') ||
123
+ // NestJS adapter re-exports the kernel (ArkModule, InjectArk, ArkKernel).
124
+ specifier === 'arkgate/nestjs' ||
125
+ specifier.startsWith('arkgate/nestjs/'));
126
+ }
127
+ function stripScriptExtension(path) {
128
+ return path.replace(/\.(?:[cm]?[jt]sx?)$/i, '');
129
+ }
130
+ function joinRelative(fromFile, specifier) {
131
+ const parts = fromFile.replace(/\\/g, '/').split('/');
132
+ parts.pop();
133
+ for (const piece of specifier.split('/')) {
134
+ if (piece === '' || piece === '.')
135
+ continue;
136
+ if (piece === '..')
137
+ parts.pop();
138
+ else
139
+ parts.push(piece);
140
+ }
141
+ return parts.join('/');
142
+ }
143
+ /**
144
+ * Pure predicate: does a relative `specifier` imported from `fromFile` point at
145
+ * one of the ArkRun kernel/composition roots? Identifiers imported from such a
146
+ * module are traced as kernel receivers (an `ark` binding imported from a relative `../main`).
147
+ */
148
+ export function createArkRunKernelRootSpecifierMatcher(fromFile, roots) {
149
+ return (specifier) => {
150
+ if (!specifier.startsWith('.'))
151
+ return false;
152
+ const target = stripScriptExtension(joinRelative(fromFile, specifier));
153
+ return roots.some((root) => {
154
+ const pattern = root.replace(/\\/g, '/');
155
+ const star = pattern.indexOf('*');
156
+ if (star >= 0) {
157
+ const prefix = pattern.slice(0, star).replace(/\/$/, '');
158
+ return prefix.length > 0 && (target === prefix || target.startsWith(`${prefix}/`));
159
+ }
160
+ const exact = stripScriptExtension(pattern);
161
+ return exact === target || exact === `${target}/index`;
162
+ });
163
+ };
95
164
  }
96
165
  /**
97
166
  * Closed broker / queue / emitter specifiers for `arkrun-transport-bypass`
@@ -323,9 +392,347 @@ function importedFromForName(content, localName) {
323
392
  });
324
393
  return found;
325
394
  }
326
- export function extractArkRunKernelCallsFromSource(file, content) {
395
+ /** Locals of kernel receiver types imported (value or type-only) from a kernel module. */
396
+ function collectKernelTypeNames(source) {
397
+ const out = new Set();
398
+ const importRe = /\bimport\s+(?:type\s+)?([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g;
399
+ let match;
400
+ while ((match = importRe.exec(source)) !== null) {
401
+ if (!isArkRunKernelModuleSpecifier(match[2] ?? ''))
402
+ continue;
403
+ const braced = /\{([^}]*)\}/.exec(match[1] ?? '');
404
+ if (!braced?.[1])
405
+ continue;
406
+ for (const part of braced[1].split(',')) {
407
+ const piece = part.trim().replace(/^type\s+/, '');
408
+ const alias = /^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/.exec(piece);
409
+ const original = alias?.[1] ?? piece;
410
+ const local = alias?.[2] ?? piece;
411
+ if (RECEIVER_TYPES.has(original))
412
+ out.add(local);
413
+ }
414
+ }
415
+ return out;
416
+ }
417
+ function isFactoryCalleeExpression(expression, bindings) {
418
+ const parts = expression.split('.');
419
+ if (parts.length === 2) {
420
+ return bindings.namespaces.has(parts[0]) && FACTORY_CALLEES.has(parts[1]);
421
+ }
422
+ if (parts.length !== 1)
423
+ return false;
424
+ const original = bindings.named.get(parts[0]);
425
+ return original !== undefined && FACTORY_CALLEES.has(original);
426
+ }
427
+ function addRootModuleImports(source, isKernelRootSpecifier, receivers, rootNamespaces) {
428
+ parseValueImportClause(source, (clause, specifier) => {
429
+ if (!isKernelRootSpecifier(specifier))
430
+ return;
431
+ const namespace = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
432
+ if (namespace?.[1]) {
433
+ receivers.add(namespace[1]);
434
+ rootNamespaces?.add(namespace[1]);
435
+ }
436
+ const defaultIdent = /^([A-Za-z_$][\w$]*)\s*(?:,|$)/.exec(clause.trim());
437
+ if (defaultIdent?.[1])
438
+ receivers.add(defaultIdent[1]);
439
+ const braced = /\{([^}]*)\}/.exec(clause);
440
+ for (const part of braced?.[1]?.split(',') ?? []) {
441
+ const piece = part.trim();
442
+ if (!piece || piece.startsWith('type '))
443
+ continue;
444
+ const local = /([A-Za-z_$][\w$]*)$/.exec(piece)?.[1];
445
+ if (local)
446
+ receivers.add(local);
447
+ }
448
+ });
449
+ }
450
+ /** Derived bindings to a fixpoint: `const pub = ark.publisher(..)`, `{ eventBus, send } = ark`. */
451
+ function addDerivedReceivers(source, trace) {
452
+ const { receivers, calleeBindings } = trace;
453
+ const derived = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(?:await\s+)?(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\b/g;
454
+ const destructured = /\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*[;\n)]/g;
455
+ // Plain aliasing: `const kernel = ark;`, `this.kernel = ark;`, `kernel = this.ark;`.
456
+ const aliased = /(?:\b(?:const|let|var)\s+|\bthis\s*\.\s*|(?:^|[;{}\n])\s*)([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?(?<![=!<>])=(?![=>])\s*(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*(?:as\s+[A-Za-z_$][\w$.<>]*\s*)?(?=[;\n,)])/g;
457
+ // `kernel: typeof ark` (parameters, fields) — the binding has the kernel's type.
458
+ const typeofBound = /([A-Za-z_$][\w$]*)\s*\??\s*:\s*typeof\s+([A-Za-z_$][\w$]*)\b/g;
459
+ let match;
460
+ for (let changed = true; changed;) {
461
+ changed = false;
462
+ for (const re of [aliased, typeofBound]) {
463
+ re.lastIndex = 0;
464
+ while ((match = re.exec(source)) !== null) {
465
+ if (!receivers.has(match[2]) || receivers.has(match[1]))
466
+ continue;
467
+ receivers.add(match[1]);
468
+ changed = true;
469
+ }
470
+ }
471
+ derived.lastIndex = 0;
472
+ while ((match = derived.exec(source)) !== null) {
473
+ if (!receivers.has(match[2]))
474
+ continue;
475
+ // `const k = main.ark` on a root namespace is a named-import equivalent.
476
+ if (!KERNEL_DERIVED_MEMBERS.has(match[3]) && !trace.rootNamespaces.has(match[2])) {
477
+ continue;
478
+ }
479
+ if (receivers.has(match[1]))
480
+ continue;
481
+ receivers.add(match[1]);
482
+ changed = true;
483
+ }
484
+ destructured.lastIndex = 0;
485
+ while ((match = destructured.exec(source)) !== null) {
486
+ if (!receivers.has(match[2]))
487
+ continue;
488
+ for (const part of match[1].split(',')) {
489
+ const pair = /^\s*([A-Za-z_$][\w$]*)\s*(?::\s*([A-Za-z_$][\w$]*))?\s*$/.exec(part);
490
+ if (!pair)
491
+ continue;
492
+ const member = pair[1];
493
+ const local = pair[2] ?? member;
494
+ if (KERNEL_TRANSIT_MEMBERS.has(member)) {
495
+ if (receivers.has(local))
496
+ continue;
497
+ receivers.add(local);
498
+ changed = true;
499
+ }
500
+ else if (arkRunKernelCallKind(member) && !FACTORY_CALLEES.has(member)) {
501
+ calleeBindings.add(local);
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+ /**
508
+ * Deterministic kernel-receiver trace (ADR 0022 D1): a non-factory interaction is
509
+ * a kernel call only when its receiver provably reaches the kernel. Matching on a
510
+ * method name alone (`res.send`, `require.resolve`, `subject.subscribe`) is not
511
+ * evidence.
512
+ */
513
+ function traceKernelReceivers(source, bindings, isKernelRootSpecifier) {
514
+ const trace = {
515
+ receivers: new Set(),
516
+ calleeBindings: new Set(),
517
+ rootNamespaces: new Set(),
518
+ };
519
+ if (isKernelRootSpecifier) {
520
+ addRootModuleImports(source, isKernelRootSpecifier, trace.receivers, trace.rootNamespaces);
521
+ }
522
+ const factoryBound = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(?:await\s+)?((?:[A-Za-z_$][\w$]*\s*\.\s*)?[A-Za-z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(/g;
523
+ let match;
524
+ while ((match = factoryBound.exec(source)) !== null) {
525
+ if (isFactoryCalleeExpression(match[2].replace(/\s+/g, ''), bindings)) {
526
+ trace.receivers.add(match[1]);
527
+ }
528
+ }
529
+ const typeNames = collectKernelTypeNames(source);
530
+ if (typeNames.size > 0) {
531
+ const annotated = /([A-Za-z_$][\w$]*)\s*\??\s*:\s*(?:Readonly\s*<\s*)?([A-Za-z_$][\w$]*)\b/g;
532
+ while ((match = annotated.exec(source)) !== null) {
533
+ if (typeNames.has(match[2]))
534
+ trace.receivers.add(match[1]);
535
+ }
536
+ }
537
+ addDerivedReceivers(source, trace);
538
+ return trace;
539
+ }
540
+ /** Index of the `(` matching the `)` at `closeIndex`, or -1. */
541
+ function matchingOpenParen(source, closeIndex) {
542
+ let depth = 0;
543
+ for (let i = closeIndex; i >= 0; i -= 1) {
544
+ const ch = source[i];
545
+ if (ch === ')')
546
+ depth += 1;
547
+ else if (ch === '(') {
548
+ depth -= 1;
549
+ if (depth === 0)
550
+ return i;
551
+ }
552
+ }
553
+ return -1;
554
+ }
555
+ function chainParts(text) {
556
+ const parts = text
557
+ .split('.')
558
+ .map((part) => part.trim())
559
+ .filter((part) => part.length > 0);
560
+ return parts[0] === 'this' ? parts.slice(1) : parts;
561
+ }
562
+ /**
563
+ * Member chain before a call (`this.ark.eventBus.publish(` → [ark, eventBus]).
564
+ * A chained `ark.publisher('S').send(..)` yields the `publisher` call's chain.
565
+ */
566
+ function receiverChainBefore(source, index) {
567
+ const head = source.slice(Math.max(0, index - 400), index);
568
+ const dotted = /((?:[A-Za-z_$][\w$]*\s*\??\.\s*)+)$/.exec(head);
569
+ if (dotted?.[1]) {
570
+ const before = head.slice(0, head.length - dotted[1].length);
571
+ // `a[0].send(` / `fn().send(`: the chain root is not a plain identifier.
572
+ if (/[)\]]\s*$/.test(before) || /\.\s*$/.test(before)) {
573
+ return { parts: ['<expr>'], viaPublisherCall: false };
574
+ }
575
+ return { parts: chainParts(dotted[1].replace(/\?/g, '')), viaPublisherCall: false };
576
+ }
577
+ const chained = /\)\s*\??\.\s*$/.exec(head);
578
+ if (!chained)
579
+ return undefined;
580
+ const closeIndex = index - head.length + chained.index;
581
+ const openIndex = matchingOpenParen(source, closeIndex);
582
+ if (openIndex < 0)
583
+ return { parts: ['<expr>'], viaPublisherCall: false };
584
+ const calleeHead = source.slice(Math.max(0, openIndex - 400), openIndex);
585
+ const callee = /((?:[A-Za-z_$][\w$]*\s*\??\.\s*)+)publisher\s*(?:<[^>]*>)?\s*$/.exec(calleeHead);
586
+ if (!callee?.[1])
587
+ return { parts: ['<expr>'], viaPublisherCall: false };
588
+ return { parts: chainParts(callee[1].replace(/\?/g, '')), viaPublisherCall: true };
589
+ }
590
+ function chainReachesKernel(chain, bindings, trace) {
591
+ const [root, ...rest] = chain.parts;
592
+ if (!root)
593
+ return false;
594
+ if (rest.length === 0 && bindings.namespaces.has(root))
595
+ return true;
596
+ // Backstop: ambient receivers are never the kernel, even if shadowed by a trace.
597
+ if (SKIP_INTERACTION_RECEIVERS.has(root) || !trace.receivers.has(root))
598
+ return false;
599
+ // `main.ark.publisher(..)`: the first hop off a root namespace is an exported
600
+ // binding of the composition root — the same receiver `import { ark }` traces.
601
+ const transit = trace.rootNamespaces.has(root) ? rest.slice(1) : rest;
602
+ return transit.every((member) => KERNEL_TRANSIT_MEMBERS.has(member));
603
+ }
604
+ /**
605
+ * Same-file intent names: `const X = ark.registry.define('N')` / `defineIntent('N')`,
606
+ * `define(N_CONST)`, and `const N = 'Name'` constants (ADR 0023 D2: define names
607
+ * count as call-site names). Imported creators stay unresolved (honest partial).
608
+ */
609
+ function collectDefinedIntentNames(source) {
610
+ // Not scope-aware: an identifier bound to two different names anywhere in the
611
+ // file is ambiguous (a function-local shadow could differ), so it resolves to
612
+ // nothing and the call stays an honest partial instead of a false green.
613
+ const ambiguous = new Set();
614
+ const bind = (map, id, name) => {
615
+ const prior = map.get(id);
616
+ if (prior !== undefined && prior !== name)
617
+ ambiguous.add(id);
618
+ else
619
+ map.set(id, name);
620
+ };
621
+ const constants = new Map();
622
+ const constRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(['"])((?:\\.|[^\\])*?)\2\s*(?:as\s+const\s*)?[;\n,)]/g;
623
+ let match;
624
+ while ((match = constRe.exec(source)) !== null) {
625
+ if (match[3])
626
+ bind(constants, match[1], match[3]);
627
+ }
628
+ for (const id of ambiguous)
629
+ constants.delete(id);
630
+ const defined = new Map(constants);
631
+ const defineRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(?:[A-Za-z_$][\w$]*\s*\.\s*)*(?:define|defineIntent)\s*(?:<[^>]*>)?\s*\(\s*(?:(['"])((?:\\.|[^\\])*?)\2|([A-Za-z_$][\w$]*)\s*[,)])/g;
632
+ while ((match = defineRe.exec(source)) !== null) {
633
+ const name = match[3] ?? (match[4] ? constants.get(match[4]) : undefined);
634
+ if (name)
635
+ bind(defined, match[1], name);
636
+ else if (ambiguous.has(match[4] ?? '') || defined.has(match[1]))
637
+ ambiguous.add(match[1]);
638
+ }
639
+ for (const id of ambiguous)
640
+ defined.delete(id);
641
+ return defined;
642
+ }
643
+ function firstIdentifierArg(content, openParenEnd) {
644
+ return /^\s*([A-Za-z_$][\w$]*)\s*[,)]/.exec(content.slice(openParenEnd))?.[1];
645
+ }
646
+ /**
647
+ * Effective intent prefixes of a config: each layer's declared `intentPrefixes`
648
+ * (or its canonical defaults) plus every canonical prefix. The kernel maps intents
649
+ * by these, so a name carrying one is kernel-valid evidence.
650
+ */
651
+ export function arkRunEffectiveIntentPrefixes(config) {
652
+ const prefixes = new Set();
653
+ for (const entry of DEFAULT_INTENT_PREFIXES) {
654
+ for (const prefix of entry.prefixes)
655
+ prefixes.add(prefix);
656
+ }
657
+ for (const layer of config.layers) {
658
+ for (const prefix of effectiveIntentPrefixes(layer))
659
+ prefixes.add(prefix);
660
+ }
661
+ return [...prefixes].sort();
662
+ }
663
+ function carriesIntentPrefix(value, prefixes) {
664
+ if (looksLikeArkIntent(value))
665
+ return true;
666
+ return (prefixes ?? []).some((raw) => {
667
+ const prefix = raw.trim();
668
+ if (!prefix)
669
+ return false;
670
+ const normalized = prefix.endsWith('.') ? prefix : `${prefix}.`;
671
+ return value.startsWith(normalized) && /^[A-Za-z0-9_.]+$/.test(value.slice(normalized.length));
672
+ });
673
+ }
674
+ function receiverName(chain) {
675
+ if (!chain)
676
+ return undefined;
677
+ if (chain.viaPublisherCall)
678
+ return 'publisher';
679
+ const last = chain.parts[chain.parts.length - 1];
680
+ return last === '<expr>' ? undefined : last;
681
+ }
682
+ /**
683
+ * A receiver the trace cannot follow (untyped parameter, cross-file hop the
684
+ * resolver did not see) still counts when the call names a kernel-valid intent
685
+ * (`Domain.…`, or a configured `intentPrefixes` prefix such as `Billing.…` — the only
686
+ * names a kernel maps). `res.send('ok')`
687
+ * or `require.resolve('pkg')` never carry such a name; ambient receivers never count.
688
+ */
689
+ function untracedKernelNameEvidence(chain, stringArg, intentPrefixes) {
690
+ if (!chain || stringArg === undefined || !carriesIntentPrefix(stringArg, intentPrefixes)) {
691
+ return false;
692
+ }
693
+ const root = chain.parts[0];
694
+ return root !== undefined && root !== '<expr>' && !SKIP_INTERACTION_RECEIVERS.has(root);
695
+ }
696
+ /**
697
+ * Does this module re-export a kernel root (a named or star re-export of a relative
698
+ * `main` module, or an `ark` binding imported from it and re-exported /
699
+ * `export const kernel = ark` / `export default ark`)? The resolver uses it to
700
+ * treat barrels as kernel roots, to a fixpoint.
701
+ */
702
+ export function reexportsArkRunKernelRoot(content, isKernelRootSpecifier) {
703
+ const source = stripCommentsPreservingLines(content);
704
+ const reexport = /\bexport\s+(?!type\b)(?:\*|\{[^}]*\}|\*\s+as\s+[A-Za-z_$][\w$]*)\s*from\s*['"]([^'"]+)['"]/g;
705
+ let found;
706
+ while ((found = reexport.exec(source)) !== null) {
707
+ if (isKernelRootSpecifier(found[1] ?? ''))
708
+ return true;
709
+ }
710
+ const imported = new Set();
711
+ addRootModuleImports(source, isKernelRootSpecifier, imported);
712
+ if (imported.size === 0)
713
+ return false;
714
+ const exportedLocal = [
715
+ /\bexport\s*\{([^}]*)\}\s*(?!\s*from)/g,
716
+ /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*[;\n]/g,
717
+ /\bexport\s+(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*(?::[^=;]+)?=\s*([A-Za-z_$][\w$]*)\s*[;\n]/g,
718
+ ];
719
+ for (const re of exportedLocal) {
720
+ let match;
721
+ while ((match = re.exec(source)) !== null) {
722
+ const names = (match[1] ?? '')
723
+ .split(',')
724
+ .map((part) => part.trim().split(/\s+as\s+/)[0]?.trim() ?? '');
725
+ if (names.some((name) => imported.has(name)))
726
+ return true;
727
+ }
728
+ }
729
+ return false;
730
+ }
731
+ export function extractArkRunKernelCallsFromSource(file, content, options = {}) {
327
732
  const source = stripCommentsPreservingLines(content);
328
733
  const bindings = collectKernelImportBindings(source);
734
+ const trace = traceKernelReceivers(source, bindings, options.isKernelRootSpecifier);
735
+ const definedNames = collectDefinedIntentNames(source);
329
736
  const facts = [];
330
737
  const callRe = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]*>)?\s*\(/g;
331
738
  let match;
@@ -334,20 +741,36 @@ export function extractArkRunKernelCallsFromSource(file, content) {
334
741
  const index = match.index;
335
742
  if (keywordBefore(source, index, 'function') || keywordBefore(source, index, 'class'))
336
743
  continue;
337
- const dotted = source.slice(0, index).match(/([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*$/);
338
- const receiver = dotted?.[1];
744
+ const chain = receiverChainBefore(source, index);
745
+ const receiver = receiverName(chain);
339
746
  const original = bindings.named.get(callee) ?? callee;
340
- const kind = arkRunKernelCallKind(original) ?? arkRunKernelCallKind(callee);
747
+ let kind = arkRunKernelCallKind(original) ?? arkRunKernelCallKind(callee);
748
+ let viaImport = bindings.named.has(callee) ||
749
+ trace.calleeBindings.has(callee) ||
750
+ (chain?.parts.length === 1 && bindings.namespaces.has(chain.parts[0]));
751
+ if (!kind &&
752
+ NEST_FACTORY_METHODS.has(callee) &&
753
+ chain?.parts.length === 1 &&
754
+ bindings.named.get(chain.parts[0]) === ARKRUN_NEST_KERNEL_MODULE) {
755
+ kind = 'factory';
756
+ viaImport = true;
757
+ }
341
758
  if (!kind)
342
759
  continue;
343
- const viaImport = bindings.named.has(callee) || (receiver !== undefined && bindings.namespaces.has(receiver));
344
- if (kind !== 'factory') {
345
- if (!viaImport && receiver === undefined)
346
- continue;
347
- if (receiver && SKIP_INTERACTION_RECEIVERS.has(receiver) && !viaImport)
760
+ if (kind === 'factory') {
761
+ // `obj.createArkKernel()` on an arbitrary receiver is not the kernel factory.
762
+ if (chain !== undefined && !viaImport)
348
763
  continue;
349
764
  }
350
- const nameLiteral = firstStringLiteralArg(source, index + match[0].length);
765
+ const argStart = index + match[0].length;
766
+ const stringArg = firstStringLiteralArg(source, argStart);
767
+ if (kind !== 'factory' &&
768
+ (chain === undefined ? !viaImport : !chainReachesKernel(chain, bindings, trace)) &&
769
+ !untracedKernelNameEvidence(chain, stringArg, options.intentPrefixes)) {
770
+ continue;
771
+ }
772
+ const identifierArg = firstIdentifierArg(source, argStart);
773
+ const nameLiteral = stringArg ?? (identifierArg ? definedNames.get(identifierArg) : undefined);
351
774
  facts.push({
352
775
  file,
353
776
  line: lineAt(content, index),
@@ -132,7 +132,7 @@ function evaluateMissingRoot(extra, hits, teethAllowed) {
132
132
  if (matched.some((hit) => hit.hasKernelFactory))
133
133
  continue;
134
134
  const first = matched[0];
135
- out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun kernel root ${JSON.stringify(pattern)} has no createArkKernel / createStrictArkKernel factory.`, { target: pattern }, teethAllowed));
135
+ out.push(finding(extra, 'arkrun-missing-root', first.file, 1, `ArkRun kernel root ${JSON.stringify(pattern)} has no kernel factory (createArkKernel / createStrictArkKernel / createLenientArkKernel / *FromConfig, or ArkModule.forRoot / forRootAsync from arkgate/nestjs).`, { target: pattern }, teethAllowed));
136
136
  }
137
137
  return out;
138
138
  }
@@ -194,11 +194,13 @@ function evaluateUndeclared(extra, kernelCalls, declarations, layerForFile, teet
194
194
  if (!fromLayer || !managed.has(fromLayer))
195
195
  continue;
196
196
  if (!call.nameLiteral) {
197
- if (extra.mode === 'enforced') {
197
+ // `publisher(creator)` binds a source; the emitted name is proven at the
198
+ // chained `.publish` / `.send`, so an unresolved source is not a gap.
199
+ if (extra.mode === 'enforced' && call.kind !== 'publisher') {
198
200
  completenessReasons.push({
199
201
  code: ARKRUN_INTERACTION_NAME_INCOMPLETE,
200
202
  file: call.file,
201
- message: `ArkRun ${call.kind} call in ${call.file} has no string-literal name; enforced extra cannot prove the declaration.`,
203
+ message: `ArkRun ${call.kind} call in ${call.file} has no string-literal name or same-file define/defineIntent binding; enforced extra cannot prove the declaration.`,
202
204
  });
203
205
  }
204
206
  continue;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tooling I/O for the config version floor (#338): read the files that can pin
3
+ * an arkgate version and hand their text to the pure Domain parser.
4
+ *
5
+ * Bounded and non-recursive. Missing or oversized files are skipped. Every
6
+ * probed path is reported to `observeInput` so a resident snapshot notices a
7
+ * later edit of package.json or a workflow.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { parseArkgatePins } from './analysis-engine.mjs';
12
+ import { __packageRoot } from './gate-files.mjs';
13
+
14
+ const MAX_PIN_FILE_BYTES = 256 * 1024;
15
+
16
+ const FIXED_CANDIDATES = [
17
+ ['package.json', 'package-json'],
18
+ ['package-lock.json', 'package-lock'],
19
+ ['node_modules/arkgate/package.json', 'installed'],
20
+ ['.claude/settings.json', 'hook'],
21
+ ['.cursor/hooks.json', 'hook'],
22
+ ['.codex/hooks.json', 'hook'],
23
+ ['.codex/config.toml', 'hook'],
24
+ ['.grok/settings.json', 'hook'],
25
+ ];
26
+
27
+ /** Directories whose direct children may pin arkgate, with a basename filter. */
28
+ const DIRECTORY_CANDIDATES = [
29
+ ['scripts', 'hook', (name) => /hook/i.test(name)],
30
+ ['.husky', 'hook', () => true],
31
+ ['.githooks', 'hook', () => true],
32
+ ['.github/workflows', 'ci', (name) => /\.ya?ml$/i.test(name)],
33
+ ];
34
+
35
+ function readSmall(absolute) {
36
+ try {
37
+ const stat = fs.statSync(absolute);
38
+ if (!stat.isFile() || stat.size > MAX_PIN_FILE_BYTES) return null;
39
+ return fs.readFileSync(absolute, 'utf8');
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function listDirectory(absolute) {
46
+ try {
47
+ return fs
48
+ .readdirSync(absolute, { withFileTypes: true })
49
+ .filter((entry) => entry.isFile())
50
+ .map((entry) => entry.name)
51
+ .sort();
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Every arkgate pin under `root` (repo-relative files, 1-based lines).
59
+ * @param {string} root
60
+ * @param {{ observeInput?: (absolutePath: string, kind: string) => void }} [options]
61
+ */
62
+ export function collectArkgatePins(root, { observeInput } = {}) {
63
+ const pins = [];
64
+ const probe = (rel, source) => {
65
+ const absolute = path.join(root, rel);
66
+ observeInput?.(absolute, 'arkgate-pin');
67
+ const text = readSmall(absolute);
68
+ if (text) pins.push(...parseArkgatePins(rel, text, source));
69
+ };
70
+ for (const [rel, source] of FIXED_CANDIDATES) probe(rel, source);
71
+ for (const [dir, source, accept] of DIRECTORY_CANDIDATES) {
72
+ const absoluteDir = path.join(root, dir);
73
+ observeInput?.(absoluteDir, 'arkgate-pin');
74
+ for (const name of listDirectory(absoluteDir)) {
75
+ if (accept(name)) probe(`${dir}/${name}`, source);
76
+ }
77
+ }
78
+ return pins;
79
+ }
80
+
81
+ /** The arkgate version running this check. */
82
+ export function runningArkgateVersion() {
83
+ const text = readSmall(path.join(__packageRoot, 'package.json'));
84
+ if (!text) return undefined;
85
+ try {
86
+ const pkg = JSON.parse(text);
87
+ return typeof pkg.version === 'string' ? pkg.version : undefined;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ }