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.
- package/CHANGELOG.md +68 -0
- package/README.md +7 -5
- package/bin/ark-check-runtime.mjs +24 -77
- package/bin/ark-dashboard.mjs +56 -53
- package/bin/ark-layer-match.mjs +596 -112
- package/bin/ark-mcp-runtime.mjs +412 -138
- package/bin/ark-mcp.mjs +68 -4
- package/bin/ark-shared.mjs +39 -0
- package/bin/ark.mjs +65 -97
- package/bin/lib/adapter-contract-types.mjs +5 -0
- package/bin/lib/adapter-contract.mjs +15 -0
- package/bin/lib/agent-projection-command.mjs +5 -1
- package/bin/lib/analysis-engine.mjs +15 -11
- package/bin/lib/architecture-scan.mjs +50 -6
- package/bin/lib/ark-order-doctor.mjs +1 -1
- package/bin/lib/ark-order-facts.mjs +410 -15
- package/bin/lib/ark-order-invariants.mjs +51 -9
- package/bin/lib/ark-order-sensors.mjs +28 -101
- package/bin/lib/ark-order-types.mjs +2 -0
- package/bin/lib/ark-run-doctor.mjs +14 -0
- package/bin/lib/ark-run-facts.mjs +434 -11
- package/bin/lib/ark-run-sensors.mjs +5 -3
- package/bin/lib/arkgate-pins.mjs +91 -0
- package/bin/lib/arkrules-sensors.mjs +50 -249
- package/bin/lib/check-args.mjs +23 -1
- package/bin/lib/ci-and-commands.mjs +16 -6
- package/bin/lib/class-source-scan.mjs +479 -0
- package/bin/lib/codex-home.mjs +4 -2
- package/bin/lib/config-contract-slices.mjs +701 -0
- package/bin/lib/config-contract.mjs +5 -493
- package/bin/lib/config-warnings.mjs +32 -0
- package/bin/lib/dashboard-cli.mjs +62 -0
- package/bin/lib/design-delta.mjs +1 -1
- package/bin/lib/diagnostic-catalog.mjs +16 -9
- package/bin/lib/doctor-advisories.mjs +25 -1
- package/bin/lib/doctor-next-actions.mjs +17 -2
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/effective-contract-load.mjs +121 -59
- package/bin/lib/enforcement-profiles.mjs +40 -4
- package/bin/lib/first-run-help.mjs +3 -2
- package/bin/lib/flat-parent-importers.mjs +137 -0
- package/bin/lib/flat-parent-pilot.mjs +40 -100
- package/bin/lib/gate-files.mjs +29 -2
- package/bin/lib/git-change-scope.mjs +109 -0
- package/bin/lib/github-enforcement.mjs +1 -1
- package/bin/lib/hook-templates.mjs +18 -16
- package/bin/lib/host-support-matrix.mjs +3 -1
- package/bin/lib/import-resolve.mjs +13 -1
- package/bin/lib/install-migrate.mjs +50 -73
- package/bin/lib/invariant-coverage.mjs +59 -11
- package/bin/lib/mcp-adoption.mjs +9 -1
- package/bin/lib/mcp-hook-payload.mjs +401 -61
- package/bin/lib/package-manager.mjs +59 -1
- package/bin/lib/passthrough-commands.mjs +83 -0
- package/bin/lib/persistence-write-hint.mjs +81 -0
- package/bin/lib/pin-dependent-gates.mjs +117 -0
- package/bin/lib/policy-delta-io.mjs +168 -10
- package/bin/lib/prepare-change.mjs +120 -6
- package/bin/lib/prepare-write.mjs +1 -0
- package/bin/lib/presets.mjs +3 -5
- package/bin/lib/remediation.mjs +23 -4
- package/bin/lib/resolved-candidate-facts.mjs +315 -55
- package/bin/lib/rules-inventory.mjs +22 -3
- package/bin/lib/rules-under-contract.mjs +179 -5
- package/bin/lib/scan-files.mjs +32 -0
- package/bin/lib/snippet-analysis.mjs +140 -6
- package/bin/lib/source-policy.mjs +34 -0
- package/bin/lib/stable-hash.mjs +90 -0
- package/bin/lib/start-install-recovery.mjs +71 -1
- package/bin/lib/start-preview.mjs +30 -0
- package/bin/lib/status-command.mjs +33 -4
- package/bin/lib/status-manifest.mjs +32 -4
- package/bin/lib/team-parliament-io.mjs +77 -75
- package/bin/lib/team-parliament.mjs +6 -1
- package/bin/lib/write-path-capabilities.mjs +4 -2
- package/dist/chunk-A5EIY3HI.cjs +5 -0
- package/dist/chunk-MBXCFLO4.js +16 -0
- package/dist/chunk-RNQHRDCQ.cjs +16 -0
- package/dist/chunk-SVSQVGFJ.js +5 -0
- package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-BJbaeoj4.d.cts} +18 -3
- package/dist/configTypes-BJbaeoj4.d.ts +246 -0
- package/dist/eslint/index.cjs +17 -4
- package/dist/eslint/index.d.cts +359 -0
- package/dist/eslint/index.d.ts +130 -156
- package/dist/eslint/index.js +10 -4
- package/dist/index.cjs +35 -34
- package/dist/index.d.cts +5475 -0
- package/dist/index.d.ts +3312 -299
- package/dist/index.js +35 -34
- package/dist/inspectorListen-6DQVJ4DC.js +1 -0
- package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
- package/dist/nestjs/index.cjs +1 -18
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +1 -3
- package/dist/nestjs/index.js +1 -18
- package/dist/order/index.cjs +1 -1
- package/dist/order/index.d.cts +155 -0
- package/dist/order/index.d.ts +15 -3
- package/dist/order/index.js +1 -1
- package/dist/runtime/index.cjs +13 -30
- package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +804 -290
- package/dist/runtime/index.d.ts +2678 -108
- package/dist/runtime/index.js +13 -30
- package/dist/{types-Bdj32QwA.d.ts → types-DRinG-vT.d.cts} +576 -140
- package/dist/types-DRinG-vT.d.ts +1792 -0
- package/docs/README.md +2 -1
- package/docs/agent-guide.md +84 -15
- package/docs/ai-gates.md +160 -26
- package/docs/arkorder.md +32 -15
- package/docs/brownfield-adoption.md +5 -1
- package/docs/configuration.md +196 -26
- package/docs/develop.md +2 -1
- package/docs/diagnostics.md +96 -19
- package/docs/package-surface.md +31 -8
- package/docs/product-voice.md +1 -1
- package/package.json +59 -18
- package/schemas/ark.analysis-result.schema.json +15 -0
- package/schemas/ark.config.schema.json +47 -10
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/schemas/ark.status-manifest.schema.json +22 -0
- package/server.json +3 -9
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
- package/templates/agent-skills/ark-order/SKILL.md +4 -2
- package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
- package/templates/arkrules/PersistenceAdapters.json +1 -1
- package/templates/arkrules/PresentationAdapters.json +1 -1
- package/templates/skills/ark-adopt.md +4 -1
- package/templates/skills/ark-order.md +4 -2
- package/templates/skills/ark-runtime.md +6 -1
- 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
|
-
|
|
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
|
|
338
|
-
const receiver =
|
|
744
|
+
const chain = receiverChainBefore(source, index);
|
|
745
|
+
const receiver = receiverName(chain);
|
|
339
746
|
const original = bindings.named.get(callee) ?? callee;
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|