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
@@ -2,15 +2,22 @@
2
2
  * Architecture check pipeline: content scan → import graph → layer edges → cycles.
3
3
  * Extracted from ark-check entry (R3). Entry remains orchestration + presentation.
4
4
  */
5
+ import { createHash } from 'node:crypto';
5
6
  import path from 'node:path';
6
7
  import { summarizeParseHealth } from './parse-health.mjs';
7
8
  import {
8
9
  analyzeTrustedResolvedProject,
10
+ configVersionFloors,
9
11
  loadContract,
10
12
  } from './analysis-engine.mjs';
13
+ import { pathUnderSharedRoot, sharedImportsSliceMode } from '../ark-layer-match.mjs';
14
+ import { collectArkgatePins, runningArkgateVersion } from './arkgate-pins.mjs';
11
15
  import { effectiveAnalysisConfig } from './analysis-policy.mjs';
12
16
  import { resolveCandidateFacts } from './resolved-candidate-facts.mjs';
13
- import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
17
+ import {
18
+ arkRulesDriftWarnings,
19
+ loadEffectiveArkRulesFromDisk,
20
+ } from './effective-contract-load.mjs';
14
21
  import {
15
22
  coverageOptionsFromConfig,
16
23
  invariantIdsFromCatalog,
@@ -66,6 +73,28 @@ function fileLocalScope(root, files, config, { changed = false } = {}) {
66
73
  return scoped;
67
74
  }
68
75
 
76
+ /**
77
+ * `--changed` resolves the touched files plus their forward import closure.
78
+ * The deny-cross-parent pass attributes a finding to the slice file that
79
+ * imports a shared root, so an edit to a shared file alone can complete a new
80
+ * cross-universe path whose importer is outside that closure. When a touched
81
+ * file sits under such a rule's sharedRoots, the graph runs full-tree.
82
+ */
83
+ function changedSharedRootNeedsFullGraph(scoped, rules) {
84
+ if (!scoped) return false;
85
+ const walls = (rules ?? []).filter(
86
+ (rule) =>
87
+ rule?.allowed === false &&
88
+ rule.peerIsolation === true &&
89
+ sharedImportsSliceMode(rule.sharedImportsSlice) === 'deny-cross-parent'
90
+ );
91
+ if (walls.length === 0) return false;
92
+ for (const file of scoped) {
93
+ if (walls.some((rule) => pathUnderSharedRoot(file, rule.sharedRoots))) return true;
94
+ }
95
+ return false;
96
+ }
97
+
69
98
  function coversGovernedSet(root, scoped, config) {
70
99
  const governed = collectGovernedFiles(root, config);
71
100
  if (governed.length === 0) return false;
@@ -100,7 +129,11 @@ function hintCacheKey(root, scopedFiles, arkRules) {
100
129
  `${rule.sensor ?? ''}\0${rule.mode ?? ''}\0${(rule.appliesTo ?? []).join(',')}`
101
130
  )
102
131
  .join('\n');
103
- return `${path.resolve(root)}\0${filesPart}\0${rulesPart}`;
132
+ // A full-tree key lists every governed file; retaining up to HINT_CACHE_CAP of
133
+ // those raw strings in a long-lived MCP process held megabytes per entry.
134
+ return createHash('sha256')
135
+ .update(`${path.resolve(root)}\0${filesPart}\0${rulesPart}`)
136
+ .digest('hex');
104
137
  }
105
138
 
106
139
  function rememberHintCache(key, value) {
@@ -155,15 +188,17 @@ export function resolveArchitectureSnapshot({
155
188
  const scoped = fileLocalScope(root, files, effectiveConfig, {
156
189
  changed: args?.changed === true,
157
190
  });
191
+ // File-local sensors stay on `scoped`; only the graph widens (see helper).
192
+ const graphScope = changedSharedRootNeedsFullGraph(scoped, effectiveConfig.rules) ? null : scoped;
158
193
  const facts = resolveCandidateFacts({
159
194
  root,
160
195
  config: effectiveConfig,
161
196
  ts,
162
197
  ...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
163
198
  observeInput,
164
- ...(scoped
165
- ? { scopeFiles: [...scoped] }
166
- : args?.changed
199
+ ...(graphScope
200
+ ? { scopeFiles: [...graphScope] }
201
+ : args?.changed && !scoped
167
202
  ? { scopeFiles: [] }
168
203
  : {}),
169
204
  });
@@ -212,9 +247,17 @@ export function resolveArchitectureSnapshot({
212
247
  const rootsPresent = catalogHasEnforcedInvariant(arkRulesLoad.arkRules?.invariants)
213
248
  ? declaredCoverageRootsPresent(root, effectiveConfig.coverage)
214
249
  : true;
250
+ // Version floor evidence (#338): zero extra I/O unless a floored key is in use.
251
+ const pinEvidence = configVersionFloors(effectiveConfig.rules)
252
+ ? {
253
+ arkgatePins: collectArkgatePins(root, { observeInput }),
254
+ runningArkgateVersion: runningArkgateVersion(),
255
+ }
256
+ : {};
215
257
  const analyzed = analyzeTrustedResolvedProject({
216
258
  contract: analysisContract,
217
259
  facts,
260
+ ...pinEvidence,
218
261
  ...(adopted ? { adopted: true } : {}),
219
262
  ...(adopted && pathPresent === false ? { invariantTestsPathPresent: false } : {}),
220
263
  ...(rootsPresent === false ? { coverageRootsPresent: false } : {}),
@@ -229,7 +272,8 @@ export function resolveArchitectureSnapshot({
229
272
  );
230
273
  const result = {
231
274
  violations: analyzed.ir.violations,
232
- warnings: analyzed.ir.warnings,
275
+ // ADR 0012 D2: an unreferenced arkrules/*.json is advisory drift, never a failure.
276
+ warnings: [...analyzed.ir.warnings, ...arkRulesDriftWarnings(arkRulesLoad.warnings)],
233
277
  safety: analyzed.safety,
234
278
  parseHealth,
235
279
  completeness: analyzed.completeness,
@@ -13,7 +13,7 @@ export const ARK_ORDER_DOCTOR_SCHEMA_VERSION = '1.0';
13
13
  const RESIDUAL_RULE_CAP = 12;
14
14
  export const ARKORDER_ONE_BREATH = 'Layers stop a bad import. ArkOrder stops rewriting a big product choice — like the billing plan — as if it were a seat count. Change those choices through a valve, not a generic update.';
15
15
  /** First-contact next step. Reuses the billing gallery + existing doors. */
16
- export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: examples/arkorder-billing, then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
16
+ export const ARKORDER_FIRST_CONTACT_NEXT = 'Next: see the billing gallery at github.com/pedroknigge/arkgate/tree/main/examples/arkorder-billing (git only, not in the npm package), then /ark-adopt to turn the extra on and /ark-order to wire one candidate.';
17
17
  function closedMode(value) {
18
18
  return value === 'enforced' || value === 'advisory' ? value : null;
19
19
  }
@@ -8,14 +8,106 @@
8
8
  * Pure CLI helper (bin/lib/ark-order-facts.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
+ import { XI_TTL_KEY_RE } from './ark-order-types.mjs';
12
+ import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
11
13
  export const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
12
14
  export const ARKORDER_FORBIDDEN_METHODS = ['update', 'patch', 'set', 'mutate'];
13
- /** Keep in lockstep with arkRuleSensors (WRITEAGG-001 / EOSF2-001). */
14
- const IO_IMPORT_HINT_RE = /\bfrom\s+['"](?:@?prisma\/client|@supabase\/|drizzle-orm(?:\/[^'"]+)?|postgres(?:\/[^'"]+)?|typeorm|knex|mongodb|pg|mysql2|mongoose|better-sqlite3|ioredis|redis|kysely|sequelize)['"]|require\(\s*['"](?:@?prisma\/client|pg|postgres(?:\/[^'"]+)?|drizzle-orm(?:\/[^'"]+)?|knex|typeorm|mongoose)/;
15
- const IO_ALIAS_IMPORT_RE = /\bfrom\s+['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)(?:\.[cm]?[jt]sx?)?['"]|require\(\s*['"](?:@\/|~\/)?(?:[\w.-]+\/)*(?:db|database|prisma|drizzle)/;
16
- const PERSISTENCE_WRITE_HINT_RE = /\b(?:db|tx|client|prisma(?:Client)?|drizzle)\b(?:\s*\.\s*[A-Za-z_]\w*)*\s*\.\s*(?:insert(?:One|Many)?|update(?:One|Many)?|upsert|delete(?:One|Many)?|createMany|create|replaceOne|findOneAnd(?:Update|Delete|Replace))\s*\(|\bINSERT\s+INTO\b|\bUPDATE\s+[A-Za-z_][\w.]*\s+SET\b|\bDELETE\s+FROM\b/i;
17
- function sourceImportsPersistenceDriver(content) {
18
- return IO_IMPORT_HINT_RE.test(content) || IO_ALIAS_IMPORT_RE.test(content);
15
+ /**
16
+ * XIWRITE-001: same engine as `globToRegExp` in src/domain/layerMatch.ts.
17
+ * Inlined so generate:cli-pure emits self-contained bin/lib/ark-order-facts.mjs /
18
+ * ark-order-sensors.mjs
19
+ * (layerMatch is derived to bin/ark-layer-match.mjs, not a bin/lib sibling).
20
+ */
21
+ const appliesToRegexpCache = new Map();
22
+ function escapeAppliesToLiteral(ch) {
23
+ return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
24
+ }
25
+ function normalizeAppliesToGlob(pattern) {
26
+ let out = '';
27
+ for (let i = 0; i < pattern.length; i += 1) {
28
+ const c = pattern[i];
29
+ if (c === '\\' && i + 1 < pattern.length) {
30
+ const next = pattern[i + 1];
31
+ if ('*?{}[],'.includes(next) || next === '\\') {
32
+ out += '\\' + next;
33
+ i += 1;
34
+ continue;
35
+ }
36
+ out += '/';
37
+ continue;
38
+ }
39
+ out += c;
40
+ }
41
+ return out;
42
+ }
43
+ function appliesToBracesBalanced(glob) {
44
+ let depth = 0;
45
+ for (let i = 0; i < glob.length; i += 1) {
46
+ const c = glob[i];
47
+ if (c === '\\') {
48
+ i += 1;
49
+ continue;
50
+ }
51
+ if (c === '{')
52
+ depth += 1;
53
+ else if (c === '}') {
54
+ depth -= 1;
55
+ if (depth < 0)
56
+ return false;
57
+ }
58
+ }
59
+ return depth === 0;
60
+ }
61
+ export function arkOrderGlobToRegExp(pattern) {
62
+ const cached = appliesToRegexpCache.get(pattern);
63
+ if (cached)
64
+ return cached;
65
+ const glob = normalizeAppliesToGlob(pattern);
66
+ const useBraces = appliesToBracesBalanced(glob);
67
+ let out = '';
68
+ let braceDepth = 0;
69
+ for (let i = 0; i < glob.length; i += 1) {
70
+ const c = glob[i];
71
+ if (c === '\\' && i + 1 < glob.length) {
72
+ out += escapeAppliesToLiteral(glob[i + 1]);
73
+ i += 1;
74
+ }
75
+ else if (c === '*') {
76
+ if (glob[i + 1] === '*') {
77
+ if (glob[i + 2] === '/') {
78
+ out += '(?:.*/)?';
79
+ i += 2;
80
+ }
81
+ else {
82
+ out += '.*';
83
+ i += 1;
84
+ }
85
+ }
86
+ else {
87
+ out += '[^/]*';
88
+ }
89
+ }
90
+ else if (c === '?') {
91
+ out += '[^/]';
92
+ }
93
+ else if (c === '{' && useBraces) {
94
+ out += '(?:';
95
+ braceDepth += 1;
96
+ }
97
+ else if (c === '}' && useBraces && braceDepth > 0) {
98
+ out += ')';
99
+ braceDepth -= 1;
100
+ }
101
+ else if (c === ',' && useBraces && braceDepth > 0) {
102
+ out += '|';
103
+ }
104
+ else {
105
+ out += escapeAppliesToLiteral(c);
106
+ }
107
+ }
108
+ const re = new RegExp(`^${out}$`);
109
+ appliesToRegexpCache.set(pattern, re);
110
+ return re;
19
111
  }
20
112
  function escapeRegExp(value) {
21
113
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -36,6 +128,45 @@ function stripCommentsPreservingLines(content) {
36
128
  .replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
37
129
  .replace(/(^|[^:])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (c) => ' '.repeat(c.length)));
38
130
  }
131
+ /**
132
+ * Specifiers naming `arkgate/order` in one source text (static import / export-from,
133
+ * `require()`, `import()`). Editor-path twin of the resolved dependency facts the
134
+ * kernel-in-domain sensor reads in CI — syntax only, no layer verdict here.
135
+ */
136
+ export function extractArkOrderModuleImportsFromSource(file, content) {
137
+ const source = stripCommentsPreservingLines(content);
138
+ const out = [];
139
+ const fromRe = /\b(import|export)(\s+type)?\s+(?:[^;]*?\s+from\s*)?['"]([^'"]+)['"]/g;
140
+ let match;
141
+ while ((match = fromRe.exec(source)) !== null) {
142
+ const specifier = match[3] ?? '';
143
+ if (!isArkOrderModuleSpecifier(specifier))
144
+ continue;
145
+ out.push({
146
+ from: file,
147
+ specifier,
148
+ kind: match[1] === 'export' ? 'export' : 'import',
149
+ typeOnly: Boolean(match[2]),
150
+ line: lineAt(content, match.index),
151
+ resolution: 'resolved-external',
152
+ });
153
+ }
154
+ const callRe = /\b(require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
155
+ while ((match = callRe.exec(source)) !== null) {
156
+ const specifier = match[2] ?? '';
157
+ if (!isArkOrderModuleSpecifier(specifier))
158
+ continue;
159
+ out.push({
160
+ from: file,
161
+ specifier,
162
+ kind: match[1] === 'import' ? 'dynamic-import' : 'require',
163
+ typeOnly: false,
164
+ line: lineAt(content, match.index),
165
+ resolution: 'resolved-external',
166
+ });
167
+ }
168
+ return out;
169
+ }
39
170
  export function extractArkOrderPlaneCallsFromSource(file, content) {
40
171
  const source = stripCommentsPreservingLines(content);
41
172
  const facts = [];
@@ -46,19 +177,190 @@ export function extractArkOrderPlaneCallsFromSource(file, content) {
46
177
  }
47
178
  return facts;
48
179
  }
49
- export function extractArkOrderGenericUpdatesFromSource(file, content) {
180
+ const PLANE_BINDING_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]+)?=\s*createOrderPlane\s*(?:<[^>]*>)?\s*\(/g;
181
+ /**
182
+ * Conventional plane names: `plane`, `orderPlane`, `billingPlane`, … (not the factory).
183
+ * A name alone is never evidence (`clipPlane.set`, `controlPlane.update` are not ArkOrder):
184
+ * it counts only in a file that imports `arkgate/order`, or on a named import from a
185
+ * declared `planeRoots` module.
186
+ */
187
+ const CONVENTIONAL_PLANE_NAME_RE = /^(?:plane|[A-Za-z_$][\w$]*Plane)$/;
188
+ /** `plane: OrderPlane` — parameters, fields, and variables annotated with the plane type. */
189
+ const ORDER_PLANE_ANNOTATION_RE = /\b([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?OrderPlane\b/g;
190
+ const ARKORDER_IMPORT_RE = /\bfrom\s+['"]arkgate\/order(?:\/[^'"]*)?['"]|\brequire\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]|\bimport\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]/;
191
+ const NAMED_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
192
+ const RELATIVE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
193
+ /** Trailing identifier, allowing a non-null `!` and optional-chain `?` before the dot. */
194
+ const TRAILING_IDENTIFIER_RE = /([A-Za-z_$][\w$]*)\s*!?\s*\??\s*$/;
195
+ /** Leading identifier inside a parenthesized receiver: `x as T`, `<T>x`, `x!`, `x`. */
196
+ const WRAPPED_IDENTIFIER_RE = /^\s*(?:<[^<>]*>\s*)?([A-Za-z_$][\w$]*)\s*!?\s*(?:\b(?:as|satisfies)\b[\s\S]*)?$/;
197
+ /** Identifiers bound to `createOrderPlane(...)` in this file. */
198
+ export function arkOrderPlaneBindings(content) {
50
199
  const source = stripCommentsPreservingLines(content);
200
+ const names = new Set();
201
+ const re = new RegExp(PLANE_BINDING_RE.source, 'g');
202
+ let match;
203
+ while ((match = re.exec(source)) !== null) {
204
+ if (match[1])
205
+ names.add(match[1]);
206
+ }
207
+ return [...names].sort();
208
+ }
209
+ function normalizeRelativePath(parts) {
210
+ const out = [];
211
+ for (const part of parts) {
212
+ if (part === '' || part === '.')
213
+ continue;
214
+ if (part === '..') {
215
+ if (out.length === 0)
216
+ return '';
217
+ out.pop();
218
+ continue;
219
+ }
220
+ out.push(part);
221
+ }
222
+ return out.join('/');
223
+ }
224
+ /** Project-relative candidates for a relative specifier (lexical; no tsconfig paths). */
225
+ function relativeModuleCandidates(file, specifier) {
226
+ if (!specifier.startsWith('./') && !specifier.startsWith('../'))
227
+ return [];
228
+ const dir = file.replace(/\\/g, '/').split('/').slice(0, -1);
229
+ const base = normalizeRelativePath([...dir, ...specifier.split('/')]);
230
+ if (!base)
231
+ return [];
232
+ const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
233
+ const out = new Set();
234
+ for (const root of [base, stem]) {
235
+ for (const ext of RELATIVE_EXTENSIONS)
236
+ out.add(`${root}${ext}`);
237
+ for (const ext of RELATIVE_EXTENSIONS.slice(1))
238
+ out.add(`${root}/index${ext}`);
239
+ }
240
+ return [...out];
241
+ }
242
+ function specifierResolvesToPlaneRoot(file, specifier, planeRoots) {
243
+ const candidates = relativeModuleCandidates(file, specifier);
244
+ if (candidates.length === 0)
245
+ return false;
246
+ return planeRoots.some((pattern) => {
247
+ let re;
248
+ try {
249
+ re = arkOrderGlobToRegExp(pattern);
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ return candidates.some((candidate) => re.test(candidate));
255
+ });
256
+ }
257
+ /**
258
+ * Identifiers that name an order plane in this file, from direct evidence only:
259
+ * bound to `createOrderPlane(...)`, annotated `: OrderPlane`, or a `plane` / `*Plane`
260
+ * named import from a declared plane root. `isArkOrderFile` reports an `arkgate/order`
261
+ * import, which admits the conventional names file-wide.
262
+ */
263
+ export function arkOrderPlaneReceivers(file, content, options = {}) {
264
+ const source = stripCommentsPreservingLines(content);
265
+ const names = new Set(arkOrderPlaneBindings(content));
266
+ const annotated = new RegExp(ORDER_PLANE_ANNOTATION_RE.source, 'g');
267
+ let match;
268
+ while ((match = annotated.exec(source)) !== null) {
269
+ if (match[1])
270
+ names.add(match[1]);
271
+ }
272
+ const planeRoots = options.planeRoots ?? [];
273
+ if (planeRoots.length > 0) {
274
+ const imports = new RegExp(NAMED_IMPORT_RE.source, 'g');
275
+ while ((match = imports.exec(source)) !== null) {
276
+ if (!specifierResolvesToPlaneRoot(file, match[2] ?? '', planeRoots))
277
+ continue;
278
+ for (const element of (match[1] ?? '').split(',')) {
279
+ const parts = element
280
+ .trim()
281
+ .replace(/^type\s+/, '')
282
+ .split(/\s+as\s+/);
283
+ const imported = parts[0]?.trim() ?? '';
284
+ const local = (parts[1] ?? parts[0] ?? '').trim();
285
+ if (!local || !/^[A-Za-z_$][\w$]*$/.test(local))
286
+ continue;
287
+ if (CONVENTIONAL_PLANE_NAME_RE.test(imported) || CONVENTIONAL_PLANE_NAME_RE.test(local)) {
288
+ names.add(local);
289
+ }
290
+ }
291
+ }
292
+ }
293
+ names.delete(ARKORDER_PLANE_FACTORY);
294
+ return { names: [...names].sort(), isArkOrderFile: ARKORDER_IMPORT_RE.test(source) };
295
+ }
296
+ const KEYWORDS_BEFORE_PAREN = new Set([
297
+ 'return', 'await', 'void', 'typeof', 'yield', 'in', 'of', 'case', 'else', 'do', 'throw', 'delete',
298
+ ]);
299
+ /** True when `(` at this point opens call arguments (`f(x).update(`), not a wrapper. */
300
+ function isCallBefore(prefix) {
301
+ const tail = prefix.replace(/\s+$/, '');
302
+ if (/[)\]]$/.test(tail))
303
+ return true;
304
+ const word = /([A-Za-z_$][\w$]*)$/.exec(tail);
305
+ return word !== null && !KEYWORDS_BEFORE_PAREN.has(word[1]);
306
+ }
307
+ /**
308
+ * Receiver identifier right before a `.method(` — unwraps `?.`, `!`, and one balanced
309
+ * `( x as T )` / `( x satisfies T )` / `(<T>x)` / `(x)` wrapper (T may itself contain
310
+ * parentheses). `this.plane` yields `plane`.
311
+ */
312
+ function receiverBefore(before) {
313
+ const trimmed = before.replace(/\s*!?\s*\??\s*$/, '');
314
+ if (!trimmed.endsWith(')')) {
315
+ const match = TRAILING_IDENTIFIER_RE.exec(trimmed);
316
+ return match ? match[1] ?? null : null;
317
+ }
318
+ let depth = 0;
319
+ for (let index = trimmed.length - 1; index >= 0; index -= 1) {
320
+ const ch = trimmed[index];
321
+ if (ch === ')')
322
+ depth += 1;
323
+ else if (ch === '(') {
324
+ depth -= 1;
325
+ if (depth === 0) {
326
+ if (isCallBefore(trimmed.slice(0, index)))
327
+ return null;
328
+ const inner = trimmed.slice(index + 1, trimmed.length - 1);
329
+ const match = WRAPPED_IDENTIFIER_RE.exec(inner);
330
+ return match ? match[1] ?? null : null;
331
+ }
332
+ }
333
+ }
334
+ return null;
335
+ }
336
+ function planeReceiverContext(file, content, options) {
337
+ const resolved = arkOrderPlaneReceivers(file, content, options);
338
+ return { names: new Set(resolved.names), isArkOrderFile: resolved.isArkOrderFile };
339
+ }
340
+ function isPlaneReceiver(name, context) {
341
+ if (!name || name === ARKORDER_PLANE_FACTORY)
342
+ return false;
343
+ if (context.names.has(name))
344
+ return true;
345
+ return context.isArkOrderFile && CONVENTIONAL_PLANE_NAME_RE.test(name);
346
+ }
347
+ /**
348
+ * Direct evidence only: `.update|patch|set|mutate(` whose receiver is a plane (see
349
+ * arkOrderPlaneReceivers). EOSF5-001: Map / URLSearchParams / React `order.set` /
350
+ * `prisma.x.update` are not ξ mutation, even inside the plane-root file; nor is
351
+ * `clipPlane.set` / `controlPlane.update` in a file with no ArkOrder evidence.
352
+ */
353
+ export function extractArkOrderGenericUpdatesFromSource(file, content, options = {}) {
354
+ const source = stripCommentsPreservingLines(content);
355
+ const context = planeReceiverContext(file, content, options);
51
356
  const facts = [];
52
357
  const re = /\.((?:update|patch|set|mutate))\s*\(/g;
53
358
  let match;
54
359
  while ((match = re.exec(source)) !== null) {
55
360
  const method = match[1];
56
- const before = source.slice(Math.max(0, match.index - 80), match.index);
57
- // EOSF5-001: Map/URLSearchParams/React order.set is not ξ mutation.
58
- const calleeIsOrderPlane = /\b(?:plane|orderPlane)\s*(?:\?|!)?$/.test(before);
59
- if (!calleeIsOrderPlane && !/\bcreateOrderPlane\b/.test(source)) {
361
+ const before = source.slice(Math.max(0, match.index - 160), match.index);
362
+ if (!isPlaneReceiver(receiverBefore(before), context))
60
363
  continue;
61
- }
62
364
  facts.push({ file, line: lineAt(content, match.index), method });
63
365
  }
64
366
  return facts;
@@ -71,7 +373,7 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
71
373
  if (xiKeys.length === 0)
72
374
  return [];
73
375
  const source = stripCommentsPreservingLines(content);
74
- if (!sourceImportsPersistenceDriver(source) || !PERSISTENCE_WRITE_HINT_RE.test(source))
376
+ if (!sourceImportsPersistenceDriverText(source) || !sourceHasPersistenceWrite(source))
75
377
  return [];
76
378
  const facts = [];
77
379
  const seen = new Set();
@@ -91,11 +393,24 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
91
393
  }
92
394
  return facts;
93
395
  }
94
- /** ingest() assigned into a release / ξ / current / pattern store. */
396
+ /**
397
+ * Holder names that read as a Release or ξ store: whole-word `xi` / `release` /
398
+ * `pattern` / `house` / `current`; an `xi` camelCase head (`xiNext`, `xiState`); or a
399
+ * `…Xi` / `…Release` / `…Pattern` camelCase tail (`nextXi`, `currentRelease`).
400
+ * Trade-off (ADR 0013, prefer false negatives): names that only *start* with
401
+ * release/current/pattern (`releaseState`, `currentResidual`, `patternResult`,
402
+ * `releaseDate`) are not evidence, so `this.releaseState = plane.ingest(e)` stays silent.
403
+ */
404
+ const INGEST_XI_HOLDER_SOURCE = '(?:xi(?:[A-Z][\\w$]*)?|release|pattern|house|current|[A-Za-z_$][\\w$]*?(?:Xi|Release|Pattern))(?![\\w$])';
405
+ /**
406
+ * ingest() assigned into a Release / ξ holder (INGEST_XI_HOLDER_SOURCE), as a binding
407
+ * or a property write such as `store.xi =` / `release.xi[k] =` / `this.nextXi =`.
408
+ * Comparisons (`===`) and arrows (`=>`) are not evidence.
409
+ */
95
410
  export function extractArkOrderIngestWritesXiFromSource(file, content) {
96
411
  const source = stripCommentsPreservingLines(content);
97
412
  const facts = [];
98
- const re = /(?:\b(?:xi|release|current|pattern|house)\w*|\.xi)\s*=\s*[^\n;]{0,160}?\bingest\s*\(/gi;
413
+ const re = new RegExp(`(?:(?<![\\w$])|\\.)${INGEST_XI_HOLDER_SOURCE}(?:\\s*:\\s*[A-Za-z_$][\\w$.<>, |[\\]]*?)?\\s*(?:\\[[^\\]\\n]*\\])?\\s*=(?![=>])\\s*[^\\n;]{0,160}?\\bingest\\s*\\(`, 'g');
99
414
  let match;
100
415
  while ((match = re.exec(source)) !== null) {
101
416
  facts.push({ file, line: lineAt(content, match.index) });
@@ -117,3 +432,83 @@ export function extractArkOrderReleaseKeyCountsFromSource(file, content) {
117
432
  }
118
433
  return facts;
119
434
  }
435
+ /** Keys of a flat object-literal body: `key: v`, `'key': v`, and shorthand `key`. */
436
+ function literalKeys(body) {
437
+ const keys = [];
438
+ for (const entry of body.split(',')) {
439
+ const match = /^\s*(['"]?)([A-Za-z_$][\w$]*)\1\s*(?::|$)/.exec(entry);
440
+ if (match?.[2])
441
+ keys.push(match[2]);
442
+ }
443
+ return keys;
444
+ }
445
+ /**
446
+ * Freshness keys in the ξ literal (first argument) of `plane.release({...})` /
447
+ * `plane.proposeRelease({...})`. σ (the second argument) may carry freshUntil — that
448
+ * is where freshness belongs. The receiver must be a plane (arkOrderPlaneReceivers), so
449
+ * a lock/lease `release({ ttl })` is never evidence. Shorthand `{ plan, ttl }` counts.
450
+ */
451
+ export function extractArkOrderXiTtlKeysFromSource(file, content, options = {}) {
452
+ const source = stripCommentsPreservingLines(content);
453
+ const context = planeReceiverContext(file, content, options);
454
+ const facts = [];
455
+ const re = /\.(?:release|proposeRelease)\s*\(\s*\{([^}]*)\}/g;
456
+ let match;
457
+ while ((match = re.exec(source)) !== null) {
458
+ const before = source.slice(Math.max(0, match.index - 160), match.index);
459
+ if (!isPlaneReceiver(receiverBefore(before), context))
460
+ continue;
461
+ for (const key of literalKeys(match[1] ?? '')) {
462
+ if (!XI_TTL_KEY_RE.test(key))
463
+ continue;
464
+ facts.push({ file, line: lineAt(content, match.index), key });
465
+ }
466
+ }
467
+ return facts;
468
+ }
469
+ function stringLiterals(body) {
470
+ const out = [];
471
+ const re = /(['"`])([^'"`\n\\]+)\1/g;
472
+ let match;
473
+ while ((match = re.exec(body)) !== null) {
474
+ if (match[2])
475
+ out.push(match[2]);
476
+ }
477
+ return out;
478
+ }
479
+ /**
480
+ * Direct evidence only: in a file that calls createOrderPlane, a literal
481
+ * `allowedKinds: [...]` entry that a literal `cannotObserve: [...]` denies. Computed
482
+ * or imported arrays stay silent (inference never blocks) — the runtime plane still
483
+ * throws ARKORDER_INFORMATION_BUDGET before such a Release is persisted.
484
+ */
485
+ export function extractArkOrderBudgetLeaksFromSource(file, content) {
486
+ const source = stripCommentsPreservingLines(content);
487
+ if (!/\bcreateOrderPlane\b/.test(source))
488
+ return [];
489
+ const denied = new Set();
490
+ const deniedRe = /\bcannotObserve\s*:\s*\[([^\]]*)\]/g;
491
+ let match;
492
+ while ((match = deniedRe.exec(source)) !== null) {
493
+ for (const kind of stringLiterals(match[1] ?? ''))
494
+ denied.add(kind);
495
+ }
496
+ if (denied.size === 0)
497
+ return [];
498
+ const facts = [];
499
+ const seen = new Set();
500
+ const allowedRe = /\ballowedKinds\s*:\s*\[([^\]]*)\]/g;
501
+ while ((match = allowedRe.exec(source)) !== null) {
502
+ const line = lineAt(content, match.index);
503
+ for (const kind of stringLiterals(match[1] ?? '')) {
504
+ if (!denied.has(kind))
505
+ continue;
506
+ const stamp = `${line}:${kind}`;
507
+ if (seen.has(stamp))
508
+ continue;
509
+ seen.add(stamp);
510
+ facts.push({ file, line, kind });
511
+ }
512
+ }
513
+ return facts;
514
+ }