arkgate 4.8.23 → 4.8.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -2,15 +2,23 @@
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
+ arkRulesLoadFailed,
20
+ loadEffectiveArkRulesFromDisk,
21
+ } from './effective-contract-load.mjs';
14
22
  import {
15
23
  coverageOptionsFromConfig,
16
24
  invariantIdsFromCatalog,
@@ -24,6 +32,7 @@ import {
24
32
  } from './invariant-tests-path.mjs';
25
33
  import { loadArkRuleFileHints } from './arkrule-file-hints.mjs';
26
34
  import { collectGovernedFiles } from './scan-files.mjs';
35
+ import { projectImporterIndex } from './import-graph-projection.mjs';
27
36
 
28
37
  const HINT_CACHE_CAP = 16;
29
38
  /** Process-local hint map keyed by scoped path + content hash. Not a second engine. */
@@ -66,6 +75,28 @@ function fileLocalScope(root, files, config, { changed = false } = {}) {
66
75
  return scoped;
67
76
  }
68
77
 
78
+ /**
79
+ * `--changed` resolves the touched files plus their forward import closure.
80
+ * The deny-cross-parent pass attributes a finding to the slice file that
81
+ * imports a shared root, so an edit to a shared file alone can complete a new
82
+ * cross-universe path whose importer is outside that closure. When a touched
83
+ * file sits under such a rule's sharedRoots, the graph runs full-tree.
84
+ */
85
+ function changedSharedRootNeedsFullGraph(scoped, rules) {
86
+ if (!scoped) return false;
87
+ const walls = (rules ?? []).filter(
88
+ (rule) =>
89
+ rule?.allowed === false &&
90
+ rule.peerIsolation === true &&
91
+ sharedImportsSliceMode(rule.sharedImportsSlice) === 'deny-cross-parent'
92
+ );
93
+ if (walls.length === 0) return false;
94
+ for (const file of scoped) {
95
+ if (walls.some((rule) => pathUnderSharedRoot(file, rule.sharedRoots))) return true;
96
+ }
97
+ return false;
98
+ }
99
+
69
100
  function coversGovernedSet(root, scoped, config) {
70
101
  const governed = collectGovernedFiles(root, config);
71
102
  if (governed.length === 0) return false;
@@ -100,7 +131,11 @@ function hintCacheKey(root, scopedFiles, arkRules) {
100
131
  `${rule.sensor ?? ''}\0${rule.mode ?? ''}\0${(rule.appliesTo ?? []).join(',')}`
101
132
  )
102
133
  .join('\n');
103
- return `${path.resolve(root)}\0${filesPart}\0${rulesPart}`;
134
+ // A full-tree key lists every governed file; retaining up to HINT_CACHE_CAP of
135
+ // those raw strings in a long-lived MCP process held megabytes per entry.
136
+ return createHash('sha256')
137
+ .update(`${path.resolve(root)}\0${filesPart}\0${rulesPart}`)
138
+ .digest('hex');
104
139
  }
105
140
 
106
141
  function rememberHintCache(key, value) {
@@ -155,15 +190,17 @@ export function resolveArchitectureSnapshot({
155
190
  const scoped = fileLocalScope(root, files, effectiveConfig, {
156
191
  changed: args?.changed === true,
157
192
  });
193
+ // File-local sensors stay on `scoped`; only the graph widens (see helper).
194
+ const graphScope = changedSharedRootNeedsFullGraph(scoped, effectiveConfig.rules) ? null : scoped;
158
195
  const facts = resolveCandidateFacts({
159
196
  root,
160
197
  config: effectiveConfig,
161
198
  ts,
162
199
  ...(args?.tsconfig ? { tsconfig: args.tsconfig } : {}),
163
200
  observeInput,
164
- ...(scoped
165
- ? { scopeFiles: [...scoped] }
166
- : args?.changed
201
+ ...(graphScope
202
+ ? { scopeFiles: [...graphScope] }
203
+ : args?.changed && !scoped
167
204
  ? { scopeFiles: [] }
168
205
  : {}),
169
206
  });
@@ -171,13 +208,7 @@ export function resolveArchitectureSnapshot({
171
208
  observeInput,
172
209
  });
173
210
  if (arkRulesLoad.errors.length > 0) {
174
- const message = arkRulesLoad.errors
175
- .map((issue) => `- ${issue.path}: ${issue.message}`)
176
- .join('\n');
177
- const err = new Error(`Invalid Effective Contract (${configPath}):\n${message}`);
178
- err.code = 'ARKRULES_LOAD_FAILED';
179
- err.issues = arkRulesLoad.errors;
180
- throw err;
211
+ throw arkRulesLoadFailed(configPath, arkRulesLoad.errors);
181
212
  }
182
213
  const loadedContract = loadContract(effectiveConfig, configPath, {
183
214
  arkRules: arkRulesLoad.arkRules,
@@ -212,9 +243,17 @@ export function resolveArchitectureSnapshot({
212
243
  const rootsPresent = catalogHasEnforcedInvariant(arkRulesLoad.arkRules?.invariants)
213
244
  ? declaredCoverageRootsPresent(root, effectiveConfig.coverage)
214
245
  : true;
246
+ // Version floor evidence (#338): zero extra I/O unless a floored key is in use.
247
+ const pinEvidence = configVersionFloors(effectiveConfig.rules)
248
+ ? {
249
+ arkgatePins: collectArkgatePins(root, { observeInput }),
250
+ runningArkgateVersion: runningArkgateVersion(),
251
+ }
252
+ : {};
215
253
  const analyzed = analyzeTrustedResolvedProject({
216
254
  contract: analysisContract,
217
255
  facts,
256
+ ...pinEvidence,
218
257
  ...(adopted ? { adopted: true } : {}),
219
258
  ...(adopted && pathPresent === false ? { invariantTestsPathPresent: false } : {}),
220
259
  ...(rootsPresent === false ? { coverageRootsPresent: false } : {}),
@@ -229,7 +268,8 @@ export function resolveArchitectureSnapshot({
229
268
  );
230
269
  const result = {
231
270
  violations: analyzed.ir.violations,
232
- warnings: analyzed.ir.warnings,
271
+ // ADR 0012 D2: an unreferenced arkrules/*.json is advisory drift, never a failure.
272
+ warnings: [...analyzed.ir.warnings, ...arkRulesDriftWarnings(arkRulesLoad.warnings)],
233
273
  safety: analyzed.safety,
234
274
  parseHealth,
235
275
  completeness: analyzed.completeness,
@@ -252,8 +292,14 @@ export function resolveArchitectureSnapshot({
252
292
 
253
293
  /**
254
294
  * Full architecture scan for governed files.
255
- * @returns {{ violations: object[], warnings: object[] }}
295
+ *
296
+ * `graphProjection: true` (doctor / report) also returns `importGraph`, a compact
297
+ * importer index projected from the resolved facts before they are released
298
+ * (ADR 0037 D1). The verdict fields are the same object either way.
299
+ * @returns {{ violations: object[], warnings: object[], importGraph?: object }}
256
300
  */
257
301
  export function runArchitectureScan(options) {
258
- return resolveArchitectureSnapshot({ ...options, captureInputs: false }).result;
302
+ const snapshot = resolveArchitectureSnapshot({ ...options, captureInputs: false });
303
+ if (options?.graphProjection !== true) return snapshot.result;
304
+ return { ...snapshot.result, importGraph: projectImporterIndex(snapshot.facts) };
259
305
  }
@@ -8,12 +8,12 @@
8
8
  * Pure CLI helper (bin/lib/ark-order-doctor.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, } from './extra-merge-teeth.mjs';
11
+ import { composeMergePlanesHonesty, extraMergeTeethAllowed, isArkOrderRuleId, mergePlanesArkRulesInput, projectExtraPlaneStatus, } from './extra-merge-teeth.mjs';
12
12
  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
  }
@@ -54,17 +54,7 @@ export function summarizeArkOrderSection(input = {}) {
54
54
  const residualCount = uniqueIds.length;
55
55
  const mergePlanes = composeMergePlanesHonesty({
56
56
  classification: input.classification,
57
- arkRules: {
58
- active: input.arkRules?.active === true,
59
- structureEnforced: input.arkRules?.structureEnforced,
60
- structureTotal: input.arkRules?.structureTotal,
61
- structureAdvisory: input.arkRules?.structureAdvisory,
62
- invariantEnforced: input.arkRules?.invariantEnforced,
63
- invariantTotal: input.arkRules?.invariantTotal,
64
- invariantAdvisory: input.arkRules?.invariantAdvisory,
65
- covered: input.arkRules?.covered,
66
- uncovered: input.arkRules?.uncovered,
67
- },
57
+ arkRules: mergePlanesArkRulesInput(input.arkRules),
68
58
  arkRun: {
69
59
  present: input.arkRun?.present === true,
70
60
  mode: input.arkRun?.mode ?? null,
@@ -110,22 +100,7 @@ export function summarizeArkOrderSection(input = {}) {
110
100
  }
111
101
  /** Thin status slice — counts only; residual null means unknown, not green. */
112
102
  export function projectStatusArkOrder(input = {}) {
113
- const present = input.present === true;
114
- const mode = closedMode(input.mode);
115
- const residualRaw = input.residual;
116
- let residual = null;
117
- if (typeof residualRaw === 'number' && Number.isFinite(residualRaw) && residualRaw >= 0) {
118
- residual = Math.floor(residualRaw);
119
- }
120
- if (!present)
121
- residual = residual == null ? 0 : residual;
122
- return {
123
- notAScore: true,
124
- present,
125
- mode: present ? mode : null,
126
- extraMergeTeeth: present && mode === 'enforced' && input.extraMergeTeeth === true,
127
- residual,
128
- };
103
+ return projectExtraPlaneStatus(input);
129
104
  }
130
105
  export function formatArkOrderDoctorLines(section) {
131
106
  if (!section || section.notAScore !== true)
@@ -8,14 +8,13 @@
8
8
  * Pure CLI helper (bin/lib/ark-order-facts.mjs). Zero Node I/O.
9
9
  */
10
10
 
11
- export const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
12
- 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);
11
+ import { XI_TTL_KEY_RE } from './ark-order-types.mjs';
12
+ import { globToRegExp } from '../ark-layer-match.mjs';
13
+ import { sourceHasPersistenceWrite, sourceImportsPersistenceDriverText } from './persistence-write-hint.mjs';
14
+ const ARKORDER_PLANE_FACTORY = 'createOrderPlane';
15
+ /** XIWRITE-001 `appliesTo` globs use the layer-glob engine (`globToRegExp`). */
16
+ export function arkOrderGlobToRegExp(pattern) {
17
+ return globToRegExp(pattern);
19
18
  }
20
19
  function escapeRegExp(value) {
21
20
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -36,6 +35,45 @@ function stripCommentsPreservingLines(content) {
36
35
  .replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, ' '))
37
36
  .replace(/(^|[^:])\/\/.*$/gm, (line) => line.replace(/\/\/.*$/, (c) => ' '.repeat(c.length)));
38
37
  }
38
+ /**
39
+ * Specifiers naming `arkgate/order` in one source text (static import / export-from,
40
+ * `require()`, `import()`). Editor-path twin of the resolved dependency facts the
41
+ * kernel-in-domain sensor reads in CI — syntax only, no layer verdict here.
42
+ */
43
+ export function extractArkOrderModuleImportsFromSource(file, content) {
44
+ const source = stripCommentsPreservingLines(content);
45
+ const out = [];
46
+ const fromRe = /\b(import|export)(\s+type)?\s+(?:[^;]*?\s+from\s*)?['"]([^'"]+)['"]/g;
47
+ let match;
48
+ while ((match = fromRe.exec(source)) !== null) {
49
+ const specifier = match[3] ?? '';
50
+ if (!isArkOrderModuleSpecifier(specifier))
51
+ continue;
52
+ out.push({
53
+ from: file,
54
+ specifier,
55
+ kind: match[1] === 'export' ? 'export' : 'import',
56
+ typeOnly: Boolean(match[2]),
57
+ line: lineAt(content, match.index),
58
+ resolution: 'resolved-external',
59
+ });
60
+ }
61
+ const callRe = /\b(require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
62
+ while ((match = callRe.exec(source)) !== null) {
63
+ const specifier = match[2] ?? '';
64
+ if (!isArkOrderModuleSpecifier(specifier))
65
+ continue;
66
+ out.push({
67
+ from: file,
68
+ specifier,
69
+ kind: match[1] === 'import' ? 'dynamic-import' : 'require',
70
+ typeOnly: false,
71
+ line: lineAt(content, match.index),
72
+ resolution: 'resolved-external',
73
+ });
74
+ }
75
+ return out;
76
+ }
39
77
  export function extractArkOrderPlaneCallsFromSource(file, content) {
40
78
  const source = stripCommentsPreservingLines(content);
41
79
  const facts = [];
@@ -46,19 +84,190 @@ export function extractArkOrderPlaneCallsFromSource(file, content) {
46
84
  }
47
85
  return facts;
48
86
  }
49
- export function extractArkOrderGenericUpdatesFromSource(file, content) {
87
+ const PLANE_BINDING_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]+)?=\s*createOrderPlane\s*(?:<[^>]*>)?\s*\(/g;
88
+ /**
89
+ * Conventional plane names: `plane`, `orderPlane`, `billingPlane`, … (not the factory).
90
+ * A name alone is never evidence (`clipPlane.set`, `controlPlane.update` are not ArkOrder):
91
+ * it counts only in a file that imports `arkgate/order`, or on a named import from a
92
+ * declared `planeRoots` module.
93
+ */
94
+ const CONVENTIONAL_PLANE_NAME_RE = /^(?:plane|[A-Za-z_$][\w$]*Plane)$/;
95
+ /** `plane: OrderPlane` — parameters, fields, and variables annotated with the plane type. */
96
+ const ORDER_PLANE_ANNOTATION_RE = /\b([A-Za-z_$][\w$]*)\s*[?!]?\s*:\s*(?:Readonly\s*<\s*)?OrderPlane\b/g;
97
+ const ARKORDER_IMPORT_RE = /\bfrom\s+['"]arkgate\/order(?:\/[^'"]*)?['"]|\brequire\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]|\bimport\s*\(\s*['"]arkgate\/order(?:\/[^'"]*)?['"]/;
98
+ const NAMED_IMPORT_RE = /\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
99
+ const RELATIVE_EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
100
+ /** Trailing identifier, allowing a non-null `!` and optional-chain `?` before the dot. */
101
+ const TRAILING_IDENTIFIER_RE = /([A-Za-z_$][\w$]*)\s*!?\s*\??\s*$/;
102
+ /** Leading identifier inside a parenthesized receiver: `x as T`, `<T>x`, `x!`, `x`. */
103
+ const WRAPPED_IDENTIFIER_RE = /^\s*(?:<[^<>]*>\s*)?([A-Za-z_$][\w$]*)\s*!?\s*(?:\b(?:as|satisfies)\b[\s\S]*)?$/;
104
+ /** Identifiers bound to `createOrderPlane(...)` in this file. */
105
+ function arkOrderPlaneBindings(content) {
106
+ const source = stripCommentsPreservingLines(content);
107
+ const names = new Set();
108
+ const re = new RegExp(PLANE_BINDING_RE.source, 'g');
109
+ let match;
110
+ while ((match = re.exec(source)) !== null) {
111
+ if (match[1])
112
+ names.add(match[1]);
113
+ }
114
+ return [...names].sort();
115
+ }
116
+ function normalizeRelativePath(parts) {
117
+ const out = [];
118
+ for (const part of parts) {
119
+ if (part === '' || part === '.')
120
+ continue;
121
+ if (part === '..') {
122
+ if (out.length === 0)
123
+ return '';
124
+ out.pop();
125
+ continue;
126
+ }
127
+ out.push(part);
128
+ }
129
+ return out.join('/');
130
+ }
131
+ /** Project-relative candidates for a relative specifier (lexical; no tsconfig paths). */
132
+ function relativeModuleCandidates(file, specifier) {
133
+ if (!specifier.startsWith('./') && !specifier.startsWith('../'))
134
+ return [];
135
+ const dir = file.replace(/\\/g, '/').split('/').slice(0, -1);
136
+ const base = normalizeRelativePath([...dir, ...specifier.split('/')]);
137
+ if (!base)
138
+ return [];
139
+ const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
140
+ const out = new Set();
141
+ for (const root of [base, stem]) {
142
+ for (const ext of RELATIVE_EXTENSIONS)
143
+ out.add(`${root}${ext}`);
144
+ for (const ext of RELATIVE_EXTENSIONS.slice(1))
145
+ out.add(`${root}/index${ext}`);
146
+ }
147
+ return [...out];
148
+ }
149
+ function specifierResolvesToPlaneRoot(file, specifier, planeRoots) {
150
+ const candidates = relativeModuleCandidates(file, specifier);
151
+ if (candidates.length === 0)
152
+ return false;
153
+ return planeRoots.some((pattern) => {
154
+ let re;
155
+ try {
156
+ re = arkOrderGlobToRegExp(pattern);
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ return candidates.some((candidate) => re.test(candidate));
162
+ });
163
+ }
164
+ /**
165
+ * Identifiers that name an order plane in this file, from direct evidence only:
166
+ * bound to `createOrderPlane(...)`, annotated `: OrderPlane`, or a `plane` / `*Plane`
167
+ * named import from a declared plane root. `isArkOrderFile` reports an `arkgate/order`
168
+ * import, which admits the conventional names file-wide.
169
+ */
170
+ function arkOrderPlaneReceivers(file, content, options = {}) {
171
+ const source = stripCommentsPreservingLines(content);
172
+ const names = new Set(arkOrderPlaneBindings(content));
173
+ const annotated = new RegExp(ORDER_PLANE_ANNOTATION_RE.source, 'g');
174
+ let match;
175
+ while ((match = annotated.exec(source)) !== null) {
176
+ if (match[1])
177
+ names.add(match[1]);
178
+ }
179
+ const planeRoots = options.planeRoots ?? [];
180
+ if (planeRoots.length > 0) {
181
+ const imports = new RegExp(NAMED_IMPORT_RE.source, 'g');
182
+ while ((match = imports.exec(source)) !== null) {
183
+ if (!specifierResolvesToPlaneRoot(file, match[2] ?? '', planeRoots))
184
+ continue;
185
+ for (const element of (match[1] ?? '').split(',')) {
186
+ const parts = element
187
+ .trim()
188
+ .replace(/^type\s+/, '')
189
+ .split(/\s+as\s+/);
190
+ const imported = parts[0]?.trim() ?? '';
191
+ const local = (parts[1] ?? parts[0] ?? '').trim();
192
+ if (!local || !/^[A-Za-z_$][\w$]*$/.test(local))
193
+ continue;
194
+ if (CONVENTIONAL_PLANE_NAME_RE.test(imported) || CONVENTIONAL_PLANE_NAME_RE.test(local)) {
195
+ names.add(local);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ names.delete(ARKORDER_PLANE_FACTORY);
201
+ return { names: [...names].sort(), isArkOrderFile: ARKORDER_IMPORT_RE.test(source) };
202
+ }
203
+ const KEYWORDS_BEFORE_PAREN = new Set([
204
+ 'return', 'await', 'void', 'typeof', 'yield', 'in', 'of', 'case', 'else', 'do', 'throw', 'delete',
205
+ ]);
206
+ /** True when `(` at this point opens call arguments (`f(x).update(`), not a wrapper. */
207
+ function isCallBefore(prefix) {
208
+ const tail = prefix.replace(/\s+$/, '');
209
+ if (/[)\]]$/.test(tail))
210
+ return true;
211
+ const word = /([A-Za-z_$][\w$]*)$/.exec(tail);
212
+ return word !== null && !KEYWORDS_BEFORE_PAREN.has(word[1]);
213
+ }
214
+ /**
215
+ * Receiver identifier right before a `.method(` — unwraps `?.`, `!`, and one balanced
216
+ * `( x as T )` / `( x satisfies T )` / `(<T>x)` / `(x)` wrapper (T may itself contain
217
+ * parentheses). `this.plane` yields `plane`.
218
+ */
219
+ function receiverBefore(before) {
220
+ const trimmed = before.replace(/\s*!?\s*\??\s*$/, '');
221
+ if (!trimmed.endsWith(')')) {
222
+ const match = TRAILING_IDENTIFIER_RE.exec(trimmed);
223
+ return match ? match[1] ?? null : null;
224
+ }
225
+ let depth = 0;
226
+ for (let index = trimmed.length - 1; index >= 0; index -= 1) {
227
+ const ch = trimmed[index];
228
+ if (ch === ')')
229
+ depth += 1;
230
+ else if (ch === '(') {
231
+ depth -= 1;
232
+ if (depth === 0) {
233
+ if (isCallBefore(trimmed.slice(0, index)))
234
+ return null;
235
+ const inner = trimmed.slice(index + 1, trimmed.length - 1);
236
+ const match = WRAPPED_IDENTIFIER_RE.exec(inner);
237
+ return match ? match[1] ?? null : null;
238
+ }
239
+ }
240
+ }
241
+ return null;
242
+ }
243
+ function planeReceiverContext(file, content, options) {
244
+ const resolved = arkOrderPlaneReceivers(file, content, options);
245
+ return { names: new Set(resolved.names), isArkOrderFile: resolved.isArkOrderFile };
246
+ }
247
+ function isPlaneReceiver(name, context) {
248
+ if (!name || name === ARKORDER_PLANE_FACTORY)
249
+ return false;
250
+ if (context.names.has(name))
251
+ return true;
252
+ return context.isArkOrderFile && CONVENTIONAL_PLANE_NAME_RE.test(name);
253
+ }
254
+ /**
255
+ * Direct evidence only: `.update|patch|set|mutate(` whose receiver is a plane (see
256
+ * arkOrderPlaneReceivers). EOSF5-001: Map / URLSearchParams / React `order.set` /
257
+ * `prisma.x.update` are not ξ mutation, even inside the plane-root file; nor is
258
+ * `clipPlane.set` / `controlPlane.update` in a file with no ArkOrder evidence.
259
+ */
260
+ export function extractArkOrderGenericUpdatesFromSource(file, content, options = {}) {
50
261
  const source = stripCommentsPreservingLines(content);
262
+ const context = planeReceiverContext(file, content, options);
51
263
  const facts = [];
52
264
  const re = /\.((?:update|patch|set|mutate))\s*\(/g;
53
265
  let match;
54
266
  while ((match = re.exec(source)) !== null) {
55
267
  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)) {
268
+ const before = source.slice(Math.max(0, match.index - 160), match.index);
269
+ if (!isPlaneReceiver(receiverBefore(before), context))
60
270
  continue;
61
- }
62
271
  facts.push({ file, line: lineAt(content, match.index), method });
63
272
  }
64
273
  return facts;
@@ -71,7 +280,7 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
71
280
  if (xiKeys.length === 0)
72
281
  return [];
73
282
  const source = stripCommentsPreservingLines(content);
74
- if (!sourceImportsPersistenceDriver(source) || !PERSISTENCE_WRITE_HINT_RE.test(source))
283
+ if (!sourceImportsPersistenceDriverText(source) || !sourceHasPersistenceWrite(source))
75
284
  return [];
76
285
  const facts = [];
77
286
  const seen = new Set();
@@ -91,11 +300,24 @@ export function extractArkOrderXiFieldWritesFromSource(file, content, xiKeys) {
91
300
  }
92
301
  return facts;
93
302
  }
94
- /** ingest() assigned into a release / ξ / current / pattern store. */
303
+ /**
304
+ * Holder names that read as a Release or ξ store: whole-word `xi` / `release` /
305
+ * `pattern` / `house` / `current`; an `xi` camelCase head (`xiNext`, `xiState`); or a
306
+ * `…Xi` / `…Release` / `…Pattern` camelCase tail (`nextXi`, `currentRelease`).
307
+ * Trade-off (ADR 0013, prefer false negatives): names that only *start* with
308
+ * release/current/pattern (`releaseState`, `currentResidual`, `patternResult`,
309
+ * `releaseDate`) are not evidence, so `this.releaseState = plane.ingest(e)` stays silent.
310
+ */
311
+ const INGEST_XI_HOLDER_SOURCE = '(?:xi(?:[A-Z][\\w$]*)?|release|pattern|house|current|[A-Za-z_$][\\w$]*?(?:Xi|Release|Pattern))(?![\\w$])';
312
+ /**
313
+ * ingest() assigned into a Release / ξ holder (INGEST_XI_HOLDER_SOURCE), as a binding
314
+ * or a property write such as `store.xi =` / `release.xi[k] =` / `this.nextXi =`.
315
+ * Comparisons (`===`) and arrows (`=>`) are not evidence.
316
+ */
95
317
  export function extractArkOrderIngestWritesXiFromSource(file, content) {
96
318
  const source = stripCommentsPreservingLines(content);
97
319
  const facts = [];
98
- const re = /(?:\b(?:xi|release|current|pattern|house)\w*|\.xi)\s*=\s*[^\n;]{0,160}?\bingest\s*\(/gi;
320
+ 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
321
  let match;
100
322
  while ((match = re.exec(source)) !== null) {
101
323
  facts.push({ file, line: lineAt(content, match.index) });
@@ -117,3 +339,83 @@ export function extractArkOrderReleaseKeyCountsFromSource(file, content) {
117
339
  }
118
340
  return facts;
119
341
  }
342
+ /** Keys of a flat object-literal body: `key: v`, `'key': v`, and shorthand `key`. */
343
+ function literalKeys(body) {
344
+ const keys = [];
345
+ for (const entry of body.split(',')) {
346
+ const match = /^\s*(['"]?)([A-Za-z_$][\w$]*)\1\s*(?::|$)/.exec(entry);
347
+ if (match?.[2])
348
+ keys.push(match[2]);
349
+ }
350
+ return keys;
351
+ }
352
+ /**
353
+ * Freshness keys in the ξ literal (first argument) of `plane.release({...})` /
354
+ * `plane.proposeRelease({...})`. σ (the second argument) may carry freshUntil — that
355
+ * is where freshness belongs. The receiver must be a plane (arkOrderPlaneReceivers), so
356
+ * a lock/lease `release({ ttl })` is never evidence. Shorthand `{ plan, ttl }` counts.
357
+ */
358
+ export function extractArkOrderXiTtlKeysFromSource(file, content, options = {}) {
359
+ const source = stripCommentsPreservingLines(content);
360
+ const context = planeReceiverContext(file, content, options);
361
+ const facts = [];
362
+ const re = /\.(?:release|proposeRelease)\s*\(\s*\{([^}]*)\}/g;
363
+ let match;
364
+ while ((match = re.exec(source)) !== null) {
365
+ const before = source.slice(Math.max(0, match.index - 160), match.index);
366
+ if (!isPlaneReceiver(receiverBefore(before), context))
367
+ continue;
368
+ for (const key of literalKeys(match[1] ?? '')) {
369
+ if (!XI_TTL_KEY_RE.test(key))
370
+ continue;
371
+ facts.push({ file, line: lineAt(content, match.index), key });
372
+ }
373
+ }
374
+ return facts;
375
+ }
376
+ function stringLiterals(body) {
377
+ const out = [];
378
+ const re = /(['"`])([^'"`\n\\]+)\1/g;
379
+ let match;
380
+ while ((match = re.exec(body)) !== null) {
381
+ if (match[2])
382
+ out.push(match[2]);
383
+ }
384
+ return out;
385
+ }
386
+ /**
387
+ * Direct evidence only: in a file that calls createOrderPlane, a literal
388
+ * `allowedKinds: [...]` entry that a literal `cannotObserve: [...]` denies. Computed
389
+ * or imported arrays stay silent (inference never blocks) — the runtime plane still
390
+ * throws ARKORDER_INFORMATION_BUDGET before such a Release is persisted.
391
+ */
392
+ export function extractArkOrderBudgetLeaksFromSource(file, content) {
393
+ const source = stripCommentsPreservingLines(content);
394
+ if (!/\bcreateOrderPlane\b/.test(source))
395
+ return [];
396
+ const denied = new Set();
397
+ const deniedRe = /\bcannotObserve\s*:\s*\[([^\]]*)\]/g;
398
+ let match;
399
+ while ((match = deniedRe.exec(source)) !== null) {
400
+ for (const kind of stringLiterals(match[1] ?? ''))
401
+ denied.add(kind);
402
+ }
403
+ if (denied.size === 0)
404
+ return [];
405
+ const facts = [];
406
+ const seen = new Set();
407
+ const allowedRe = /\ballowedKinds\s*:\s*\[([^\]]*)\]/g;
408
+ while ((match = allowedRe.exec(source)) !== null) {
409
+ const line = lineAt(content, match.index);
410
+ for (const kind of stringLiterals(match[1] ?? '')) {
411
+ if (!denied.has(kind))
412
+ continue;
413
+ const stamp = `${line}:${kind}`;
414
+ if (seen.has(stamp))
415
+ continue;
416
+ seen.add(stamp);
417
+ facts.push({ file, line, kind });
418
+ }
419
+ }
420
+ return facts;
421
+ }