arkgate 4.7.6 → 4.8.1

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 (67) hide show
  1. package/CHANGELOG.md +37 -3
  2. package/README.md +8 -9
  3. package/bin/lib/analysis-engine.mjs +6 -6
  4. package/bin/lib/architecture-scan.mjs +7 -2
  5. package/bin/lib/ark-order-error.mjs +18 -0
  6. package/bin/lib/ark-order-facts.mjs +53 -0
  7. package/bin/lib/ark-order-invariants.mjs +160 -0
  8. package/bin/lib/ark-order-sensors.mjs +118 -0
  9. package/bin/lib/ark-order-types.mjs +11 -0
  10. package/bin/lib/ark-run-sensors.mjs +13 -5
  11. package/bin/lib/config-contract.mjs +16 -72
  12. package/bin/lib/config-extras.mjs +151 -0
  13. package/bin/lib/diagnostic-catalog.mjs +7 -2
  14. package/bin/lib/extra-merge-teeth.mjs +8 -2
  15. package/bin/lib/install-migrate.mjs +4 -2
  16. package/bin/lib/invariant-coverage-io.mjs +61 -24
  17. package/bin/lib/invariant-coverage.mjs +4 -1
  18. package/bin/lib/managed-upgrade.mjs +1 -1
  19. package/bin/lib/mcp-adoption.mjs +9 -3
  20. package/bin/lib/policy-delta-io.mjs +8 -2
  21. package/bin/lib/remediation.mjs +48 -9
  22. package/bin/lib/resolved-candidate-facts.mjs +43 -0
  23. package/bin/lib/rules-under-contract.mjs +8 -2
  24. package/bin/lib/skill-install.mjs +17 -2
  25. package/bin/lib/start-preview.mjs +1 -0
  26. package/bin/lib/status-manifest.mjs +1 -1
  27. package/bin/lib/write-path-capabilities.mjs +2 -2
  28. package/dist/{configTypes-CgJimx9o.d.ts → configTypes-BdCe_gvv.d.ts} +22 -6
  29. package/dist/diagnosticCatalog-CSF4N3w8.d.ts +2313 -0
  30. package/dist/eslint/index.cjs +7 -6
  31. package/dist/eslint/index.d.ts +33 -2
  32. package/dist/eslint/index.js +7 -6
  33. package/dist/index.cjs +35 -35
  34. package/dist/index.d.ts +271 -2554
  35. package/dist/index.js +35 -35
  36. package/dist/nestjs/index.cjs +18 -0
  37. package/dist/nestjs/index.d.ts +24 -0
  38. package/dist/nestjs/index.js +18 -0
  39. package/dist/order/index.cjs +1 -0
  40. package/dist/order/index.d.ts +79 -0
  41. package/dist/order/index.js +1 -0
  42. package/dist/runtime/index.cjs +25 -0
  43. package/dist/runtime/index.d.ts +497 -0
  44. package/dist/runtime/index.js +25 -0
  45. package/dist/types-C9KApBzX.d.ts +1237 -0
  46. package/dist/types-DCSlrRnV.d.ts +181 -0
  47. package/docs/README.md +4 -4
  48. package/docs/agent-guide.md +1 -1
  49. package/docs/ai-gates.md +9 -2
  50. package/docs/configuration.md +13 -6
  51. package/docs/develop.md +4 -3
  52. package/docs/diagnostics.md +57 -3
  53. package/docs/package-surface.md +20 -16
  54. package/docs/product-voice.md +2 -1
  55. package/package.json +21 -2
  56. package/schemas/ark.config.schema.json +54 -2
  57. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  58. package/server.json +2 -2
  59. package/templates/agent-skills/README.md +1 -1
  60. package/templates/agent-skills/ark-adopt/SKILL.md +2 -2
  61. package/templates/agent-skills/ark-contract/SKILL.md +1 -1
  62. package/templates/agent-skills/ark-place/SKILL.md +15 -6
  63. package/templates/agent-skills/ark-runtime/SKILL.md +10 -15
  64. package/templates/skills/ark-adopt.md +2 -2
  65. package/templates/skills/ark-contract.md +1 -1
  66. package/templates/skills/ark-place.md +15 -6
  67. package/templates/skills/ark-runtime.md +10 -15
@@ -0,0 +1,151 @@
1
+ /**
2
+ * GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Canonical algorithm: src/domain/configExtras.ts
5
+ * Regenerate: node scripts/generate-cli-pure.mjs
6
+ * Drift check: node scripts/generate-cli-pure.mjs --check
7
+ *
8
+ * Pure CLI helper (bin/lib/config-extras.mjs). Zero Node I/O.
9
+ */
10
+
11
+ const stringArraySchema = {
12
+ type: 'array',
13
+ items: { type: 'string', minLength: 1 },
14
+ uniqueItems: true,
15
+ };
16
+ export const ARK_RUN_SCHEMA_DEF = {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ mode: {
21
+ type: 'string',
22
+ enum: ['advisory', 'enforced'],
23
+ default: 'advisory',
24
+ },
25
+ compositionRoots: { ...stringArraySchema, default: [] },
26
+ kernelRoots: { ...stringArraySchema },
27
+ managedLayers: { ...stringArraySchema, default: [] },
28
+ requireDeclarations: { type: 'boolean', default: true },
29
+ ignoreDirectNewForErrors: { type: 'boolean', default: true },
30
+ },
31
+ };
32
+ export const ARK_ORDER_SCHEMA_DEF = {
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ properties: {
36
+ mode: {
37
+ type: 'string',
38
+ enum: ['advisory', 'enforced'],
39
+ default: 'advisory',
40
+ },
41
+ planeRoots: { ...stringArraySchema, default: [] },
42
+ managedLayers: { ...stringArraySchema, default: [] },
43
+ maxXiKeys: { type: 'integer', minimum: 1, default: 7 },
44
+ },
45
+ };
46
+ function isObject(value) {
47
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
48
+ }
49
+ function declaredLayerNames(config) {
50
+ const layerNames = new Set();
51
+ if (!Array.isArray(config.layers))
52
+ return layerNames;
53
+ for (const layer of config.layers) {
54
+ if (isObject(layer) && typeof layer.name === 'string' && layer.name.length > 0) {
55
+ layerNames.add(layer.name);
56
+ }
57
+ }
58
+ return layerNames;
59
+ }
60
+ export function defaultedArkRun(value) {
61
+ if (!isObject(value))
62
+ return value;
63
+ const out = {
64
+ mode: value.mode === undefined ? 'advisory' : value.mode,
65
+ compositionRoots: value.compositionRoots === undefined ? [] : value.compositionRoots,
66
+ managedLayers: value.managedLayers === undefined ? [] : value.managedLayers,
67
+ requireDeclarations: value.requireDeclarations === undefined ? true : value.requireDeclarations,
68
+ };
69
+ if (value.kernelRoots !== undefined)
70
+ out.kernelRoots = value.kernelRoots;
71
+ if (value.ignoreDirectNewForErrors !== undefined) {
72
+ out.ignoreDirectNewForErrors = value.ignoreDirectNewForErrors;
73
+ }
74
+ return { ...value, ...out };
75
+ }
76
+ export function defaultedArkOrder(value) {
77
+ if (!isObject(value))
78
+ return value;
79
+ const max = typeof value.maxXiKeys === 'number' && value.maxXiKeys > 0 ? value.maxXiKeys : 7;
80
+ return {
81
+ ...value,
82
+ mode: value.mode === undefined ? 'advisory' : value.mode,
83
+ planeRoots: value.planeRoots === undefined ? [] : value.planeRoots,
84
+ managedLayers: value.managedLayers === undefined ? [] : value.managedLayers,
85
+ maxXiKeys: max,
86
+ };
87
+ }
88
+ export function validateArkRunExtra(config, issues) {
89
+ const extra = config.arkRun;
90
+ if (extra === undefined || !isObject(extra))
91
+ return;
92
+ const layerNames = declaredLayerNames(config);
93
+ const managed = extra.managedLayers;
94
+ if (Array.isArray(managed)) {
95
+ managed.forEach((name, index) => {
96
+ if (typeof name === 'string' && name.length > 0 && !layerNames.has(name)) {
97
+ issues.push({
98
+ path: `$.arkRun.managedLayers[${index}]`,
99
+ message: `layer ${JSON.stringify(name)} is not declared in layers[]`,
100
+ });
101
+ }
102
+ });
103
+ }
104
+ if (extra.mode === 'enforced') {
105
+ const roots = extra.kernelRoots ?? extra.compositionRoots;
106
+ if (!Array.isArray(roots) || roots.length === 0) {
107
+ issues.push({
108
+ path: extra.kernelRoots !== undefined ? '$.arkRun.kernelRoots' : '$.arkRun.compositionRoots',
109
+ message: 'ARKRUN_MISSING_ROOT: enforced mode requires at least one kernel root',
110
+ });
111
+ }
112
+ if (!Array.isArray(managed) || managed.length === 0) {
113
+ issues.push({
114
+ path: '$.arkRun.managedLayers',
115
+ message: 'enforced mode requires at least one managed layer',
116
+ });
117
+ }
118
+ }
119
+ }
120
+ export function validateArkOrderExtra(config, issues) {
121
+ const extra = config.arkOrder;
122
+ if (extra === undefined || !isObject(extra))
123
+ return;
124
+ const layerNames = declaredLayerNames(config);
125
+ const managed = extra.managedLayers;
126
+ if (Array.isArray(managed)) {
127
+ managed.forEach((name, index) => {
128
+ if (typeof name === 'string' && name.length > 0 && !layerNames.has(name)) {
129
+ issues.push({
130
+ path: `$.arkOrder.managedLayers[${index}]`,
131
+ message: `layer ${JSON.stringify(name)} is not declared in layers[]`,
132
+ });
133
+ }
134
+ });
135
+ }
136
+ if (extra.mode === 'enforced') {
137
+ const roots = extra.planeRoots;
138
+ if (!Array.isArray(roots) || roots.length === 0) {
139
+ issues.push({
140
+ path: '$.arkOrder.planeRoots',
141
+ message: 'ARKORDER_MISSING_PLANE: enforced mode requires at least one plane root',
142
+ });
143
+ }
144
+ if (!Array.isArray(managed) || managed.length === 0) {
145
+ issues.push({
146
+ path: '$.arkOrder.managedLayers',
147
+ message: 'enforced mode requires at least one managed layer',
148
+ });
149
+ }
150
+ }
151
+ }
@@ -57,13 +57,18 @@ export const DIAGNOSTIC_CATALOG = Object.freeze([
57
57
  entry('ARKRULE_SCOPE_EMPTY', 'arkrules', 'ArkRule appliesTo matched zero files', 'An ArkRule’s appliesTo globs matched no governed files — the rule cannot observe what it claims to protect.', 'Fix appliesTo globs so they match governed files, or remove the rule. Enforced empty scope fails; advisory empty scope warns.', { oftenAdvisory: true }),
58
58
  entry('INVARIANT_UNCOVERED', 'arkrules', 'Invariant without coverage evidence', 'An ArkRules invariant is under contract but no covering test title or declared symbol evidence was found (or coverage is partial). Kind is never-had-tests (adopt residual) vs tests-disappeared (suite exists).', 'Add a test title or declared symbol covering the arkruleId, then preflight again. Treat never-had-tests as adopt residual; treat tests-disappeared as a regression. Missing test globs report partial — never fake green.'),
59
59
  // ── ArkRun (opt-in extra; RN05 dual-depth nextAction) ────────────────────
60
- entry('ARKRUN_MISSING_ROOT', 'arkrun', 'No kernel factory in composition roots', 'The ArkRun extra is on but no createArkKernel / createStrictArkKernel / createArkKernelFromConfig / createStrictArkKernelFromConfig factory was found in arkRun.compositionRoots, so agents can skip the kernel while the write gate stays green.', 'Import createStrictArkKernel from @arkgate/runtime (never a removed arkgate/runtime shim) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
61
- entry('ARKRUN_KERNEL_IN_DOMAIN', 'arkrun', 'Domain-role layer imports the kernel', 'A Domain-role layer imports @arkgate/runtime or kernel types. Domain stays kernel-free; composition roots and adapters own the factory.', 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again. Never mechanical-safe.'),
60
+ entry('ARKRUN_MISSING_ROOT', 'arkrun', 'No kernel factory in composition roots', 'The ArkRun extra is on but no createArkKernel / createStrictArkKernel / createArkKernelFromConfig / createStrictArkKernelFromConfig factory was found in arkRun.compositionRoots, so agents can skip the kernel while the write gate stays green.', 'Import createStrictArkKernel from arkgate/runtime (same npm package; @arkgate/runtime is deprecated) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
61
+ entry('ARKRUN_KERNEL_IN_DOMAIN', 'arkrun', 'Domain-role layer imports the kernel', 'A Domain-role layer imports arkgate/runtime, @arkgate/runtime, or kernel types. Domain stays kernel-free; composition roots and adapters own the factory.', 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from arkgate/runtime (same npm package; @arkgate/runtime is deprecated), then preflight again. Never mechanical-safe.'),
62
62
  entry('ARKRUN_DIRECT_NEW', 'arkrun', 'Managed type constructed with new', 'A managed non-Domain file constructs an admitted type with new outside an ArkRun composition-root factory, skipping kernel resolve/registration.', 'Resolve the type from the kernel instead of constructing it with new, then preflight again. Never mechanical-safe — rewiring construction is a design decision.'),
63
63
  entry('ARKRUN_UNDECLARED_EMIT', 'arkrun', 'Emit name not in raises/sends', 'A publisher / publish / raise / send call-site literal is not listed in the file’s raises or sends declaration.', 'Add the existing call-site name to raises or sends on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new emit stays judgment.'),
64
64
  entry('ARKRUN_UNDECLARED_HANDLE', 'arkrun', 'Handle name not in reactsTo', 'A subscribe / registerHandler call-site literal is not listed in the file’s reactsTo declaration.', 'Add the existing call-site name to reactsTo on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new handle stays judgment.'),
65
65
  entry('ARKRUN_UNDECLARED_DEPEND', 'arkrun', 'Depend name not in uses', 'A resolve / resolveSingleton call-site literal is not listed in the file’s uses declaration.', 'Add the existing call-site name to uses on the managed component, then preflight again. Mechanical-safe only when that literal already exists and the edit is the declaration list; inventing a new depend stays judgment.'),
66
66
  entry('ARKRUN_TRANSPORT_BYPASS', 'arkrun', 'Homemade broker or emitter import', 'A managed layer imports a closed broker/queue/emitter specifier (EventEmitter, queue clients, …) instead of the ArkRun kernel transport.', 'Send through the ArkRun kernel transport instead of importing that broker or emitter, then preflight again. Never mechanical-safe — homemade buses stay judgment.'),
67
+ entry('ARKORDER_MISSING_PLANE', 'arkorder', 'No createOrderPlane in plane roots', 'The ArkOrder extra is on but no createOrderPlane factory was found in arkOrder.planeRoots, so agents can skip the pattern plane while the write gate stays green.', 'Import createOrderPlane from arkgate/order and call it in a plane root listed in arkOrder.planeRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.'),
68
+ entry('ARKORDER_KERNEL_IN_DOMAIN', 'arkorder', 'Domain-role layer imports the order plane', 'A Domain-role layer imports arkgate/order. Domain stays plane-free; planeRoots own the factory.', 'Move the arkgate/order import out of the Domain-role layer into a plane root or adapter, then preflight again. Never mechanical-safe.'),
69
+ entry('ARKORDER_GENERIC_UPDATE', 'arkorder', 'Generic update of ξ', 'A call to update/patch/set on the order plane rewrites the slow pattern. Haken slaving forbids generic ξ mutation.', 'Use release() to freeze ξ or proposeRelease() for a pattern change with blast radius, then preflight again. Never mechanical-safe.'),
70
+ entry('ARKORDER_TOO_MANY_PARAMS', 'arkorder', 'Too many slow keys', 'ξ has more keys than arkOrder.maxXiKeys. Haken requires a few slow modes, not a dump of microstate.', 'Cut ξ to the slow keys that actually slave the rest, then preflight again. Never mechanical-safe.'),
71
+ entry('ARKORDER_INGEST_WRITES_XI', 'arkorder', 'ingest assigned into ξ', 'An ingest() result is written into a Release or ξ store. ingest may absorb or escalate; it never mints a pattern.', 'Keep ingest results as absorb/escalate only. Change ξ with proposeRelease + release. Never mechanical-safe.'),
67
72
  // ── atomic preflight / change set ────────────────────────────────────────
68
73
  entry('INVALID_CHANGE_PATH', 'preflight', 'Unsafe change path', 'A change set entry is not a safe, non-empty project-relative path (absolute, escape, empty, or NUL).', 'Use canonical project-relative paths only in the atomic change set, then preflight again.'),
69
74
  entry('DUPLICATE_CHANGE_PATH', 'preflight', 'Duplicate path in change set', 'The atomic change set lists more than one operation for the same path.', 'Collapse to one create/update/delete per path, then preflight again.'),
@@ -49,11 +49,17 @@ export function classifyResolvedLayerCoverage(files) {
49
49
  export function isArkRunRuleId(ruleId) {
50
50
  return typeof ruleId === 'string' && ruleId.startsWith('ARKRUN_');
51
51
  }
52
+ export function isArkOrderRuleId(ruleId) {
53
+ return typeof ruleId === 'string' && ruleId.startsWith('ARKORDER_');
54
+ }
52
55
  export function isExtraPlaneFinding(violation) {
53
56
  if (violation?.arkruleId != null)
54
57
  return true;
55
58
  const id = typeof violation?.ruleId === 'string' ? violation.ruleId : '';
56
- return id.startsWith('ARKRULE') || id.startsWith('arkrule') || id.startsWith('ARKRUN_');
59
+ return (id.startsWith('ARKRULE') ||
60
+ id.startsWith('arkrule') ||
61
+ id.startsWith('ARKRUN_') ||
62
+ id.startsWith('ARKORDER_'));
57
63
  }
58
64
  /**
59
65
  * Under the classification floor, demote enforced extra-plane findings in place
@@ -74,7 +80,7 @@ export function demoteExtraPlaneTeethUnderClassificationFloor(violations, classi
74
80
  return violations;
75
81
  }
76
82
  /** Stamp for extra-plane honesty: never one architecture score. */
77
- export const MERGE_PLANES_DUAL_STAMP = 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime); ArkRun = kernel usage + declarations (not a score). Extra planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth. Advisory ArkRun ≠ merge teeth.';
83
+ export const MERGE_PLANES_DUAL_STAMP = 'Structure = heuristics; invariants = catalog+coverage evidence (not business runtime); ArkRun = kernel usage + declarations (not a score); ArkOrder = pattern slaving (not a score). Extra planes never merge into one architecture score. Advisory ArkRules ≠ merge teeth. Advisory ArkRun ≠ merge teeth. Advisory ArkOrder ≠ merge teeth.';
78
84
  function countOrZero(value) {
79
85
  return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
80
86
  }
@@ -216,6 +216,8 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
216
216
  }
217
217
  if (selectedTools.has('antigravity')) {
218
218
  add('.agents/hooks.json', antigravityHooks(root));
219
+ // Official workspace MCP. Antigravity does not load repo-root `.mcp.json`.
220
+ add('.agents/mcp_config.json', mcpJson(root));
219
221
  // Still useful for Gemini CLI / legacy Gemini consumers sharing the tree.
220
222
  add('GEMINI.md', instructionRule(root));
221
223
  }
@@ -747,8 +749,8 @@ export function runInstallAgentGates(args) {
747
749
  if (tools.has('antigravity') && !args.compact) {
748
750
  console.log('');
749
751
  console.log(' Antigravity: PreToolUse deny is hard for listed write tools when hooks are trusted.');
750
- console.log(' - Install path: `.agents/hooks.json` (+ GEMINI.md for legacy Gemini consumers).');
751
- console.log(' - Trust project hooks in the host; pair with required CI --strict-merge.');
752
+ console.log(' - Install path: `.agents/hooks.json` + `.agents/mcp_config.json` (+ GEMINI.md for legacy Gemini consumers).');
753
+ console.log(' - Trust project hooks; refresh MCP (`/mcp` or restart); pair with required CI --strict-merge.');
752
754
  }
753
755
  if (tools.has('opencode') && !args.compact) {
754
756
  console.log('');
@@ -62,16 +62,42 @@ function matchSimpleGlob(glob, file) {
62
62
  return new RegExp(`^${out}$`).test(target);
63
63
  }
64
64
 
65
+ /**
66
+ * Declared invariant ids from an Effective catalog. Empty when the extra is off.
67
+ * @param {{ invariants?: Array<{ id?: unknown }> } | null | undefined} arkRules
68
+ * @returns {string[]}
69
+ */
70
+ export function invariantIdsFromCatalog(arkRules) {
71
+ return (arkRules?.invariants ?? [])
72
+ .map((inv) => inv?.id)
73
+ .filter((id) => typeof id === 'string' && id.length > 0);
74
+ }
75
+
65
76
  /**
66
77
  * @param {string} root
67
78
  * @param {{ files?: Array<{ path: string }> }} facts
68
- * @param {{ testGlobs?: string[] }} [opts]
69
- * @returns {{ fileContents: Record<string, string>, testFiles: string[], testGlobsMissing: boolean }}
79
+ * @param {{ testGlobs?: string[], invariantIds?: string[] }} [opts]
80
+ * @returns {{
81
+ * fileContents: Record<string, string>,
82
+ * testFiles: string[],
83
+ * testGlobsMissing: boolean,
84
+ * coverageBudgetExhausted: boolean,
85
+ * }}
70
86
  */
71
87
  export function loadInvariantCoverageInputs(root, facts, opts = {}) {
72
88
  const fileContents = {};
73
89
  const testFiles = [];
74
90
  const seen = new Set();
91
+ // Declared invariant ids. When present, a test file is RETAINED only if it
92
+ // mentions one: scanning is cheap (hundreds of small files), retaining is
93
+ // what costs memory. Without this the budget goes to whichever N tests the
94
+ // walk reaches first — an arbitrary order — so coverage is wrong on any repo
95
+ // with more test files than budget. Measured: 707 tests against a cap of 400.
96
+ const invariantIds = Array.isArray(opts.invariantIds)
97
+ ? opts.invariantIds.filter((id) => typeof id === 'string' && id.length > 0)
98
+ : [];
99
+ const mentionsInvariant = (content) =>
100
+ invariantIds.length === 0 || invariantIds.some((id) => content.includes(id));
75
101
  const testGlobs = Array.isArray(opts.testGlobs)
76
102
  ? opts.testGlobs.filter((g) => typeof g === 'string' && g.length > 0)
77
103
  : [];
@@ -93,40 +119,51 @@ export function loadInvariantCoverageInputs(root, facts, opts = {}) {
93
119
  const stat = fs.statSync(absolute);
94
120
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return;
95
121
  const content = fs.readFileSync(absolute, 'utf8');
122
+ const asTest = forceAsTest || isTestPath(rel);
123
+ // A test that names no invariant is evidence of nothing: scan it, drop
124
+ // it, and let it cost no budget.
125
+ if (asTest && !mentionsInvariant(content)) return;
96
126
  seen.add(rel);
97
127
  fileContents[rel] = content;
98
- if (forceAsTest || isTestPath(rel)) testFiles.push(rel);
128
+ if (asTest) testFiles.push(rel);
99
129
  } catch {
100
130
  // skip unreadable
101
131
  }
102
132
  };
103
133
 
104
- for (const file of facts?.files ?? []) {
105
- if (file?.path) pushFile(file.path);
134
+ // Tests FIRST, then production files.
135
+ //
136
+ // The order is load-bearing, not stylistic. `pushFile` stops at
137
+ // MAX_COVERAGE_FILES, and a real repo has far more production files than the
138
+ // budget — so walking facts first consumed the whole budget and the test walk
139
+ // pushed nothing. Coverage then reported `testGlobsMissing: true`, which the
140
+ // caller renders as "never-had-tests": a claim about the USER's repo that was
141
+ // actually about our own budget. Measured on a 4511-file project: every
142
+ // invariant reported uncovered while its test sat on disk with the invariant
143
+ // id in the describe title. Tests are tens of files, not thousands, so giving
144
+ // them the head of the budget costs the production scan nothing in practice.
145
+ const testWalkRoots = useCustomGlobs
146
+ ? ['.', 'tests', 'test', 'src', '__tests__', 'spec']
147
+ : ['tests', 'test', 'src', '__tests__'];
148
+ for (const dir of testWalkRoots) {
149
+ const absDir = path.join(root, dir === '.' ? '' : dir);
150
+ if (!fs.existsSync(absDir)) continue;
151
+ walkTestFiles(absDir, root, (rel) => {
152
+ if (isTestPath(rel)) pushFile(rel, true);
153
+ });
106
154
  }
107
155
 
108
- if (useCustomGlobs) {
109
- // Walk project roots and keep files matching custom globs.
110
- for (const dir of ['.', 'tests', 'test', 'src', '__tests__', 'spec']) {
111
- const absDir = path.join(root, dir === '.' ? '' : dir);
112
- if (!fs.existsSync(absDir)) continue;
113
- walkTestFiles(absDir, root, (rel) => {
114
- if (isTestPath(rel)) pushFile(rel, true);
115
- });
116
- }
117
- } else {
118
- // Walk common test roots when facts only cover production include globs.
119
- for (const dir of ['tests', 'test', 'src', '__tests__']) {
120
- const absDir = path.join(root, dir);
121
- if (!fs.existsSync(absDir)) continue;
122
- walkTestFiles(absDir, root, (rel) => {
123
- if (isTestPath(rel)) pushFile(rel, true);
124
- });
125
- }
156
+ for (const file of facts?.files ?? []) {
157
+ if (file?.path) pushFile(file.path);
126
158
  }
127
159
 
128
160
  const testGlobsMissing = testFiles.length === 0;
129
- return { fileContents, testFiles, testGlobsMissing };
161
+ return {
162
+ fileContents,
163
+ testFiles,
164
+ testGlobsMissing,
165
+ coverageBudgetExhausted: seen.size >= MAX_COVERAGE_FILES,
166
+ };
130
167
  }
131
168
 
132
169
  /**
@@ -38,6 +38,7 @@ export function evaluateInvariantCoverage(input) {
38
38
  }
39
39
  const testFiles = input.testFiles ?? [];
40
40
  const testGlobsMissing = input.testGlobsMissing === true || testFiles.length === 0;
41
+ const coverageBudgetExhausted = input.coverageBudgetExhausted === true;
41
42
  const coverage = [];
42
43
  const violations = [];
43
44
  for (const inv of invariants) {
@@ -83,7 +84,9 @@ export function evaluateInvariantCoverage(input) {
83
84
  violations.push({
84
85
  ruleId: 'INVARIANT_UNCOVERED',
85
86
  message: partial
86
- ? `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
87
+ ? coverageBudgetExhausted
88
+ ? `Invariant ${inv.id} coverage cannot be proven (coverage file budget exhausted); reporting partial, not covered.`
89
+ : `Invariant ${inv.id} coverage cannot be proven (test globs missing or empty); reporting partial, not covered (never-had-tests).`
87
90
  : kind === 'tests-disappeared'
88
91
  ? `Invariant ${inv.id} is not covered by a test title or declared symbol (tests-disappeared — suite exists).`
89
92
  : `Invariant ${inv.id} is not covered by a test title or declared symbol (never-had-tests).`,
@@ -129,7 +129,7 @@ const HOST_SIGNALS = {
129
129
  cursor: ['.cursor/mcp.json', '.cursor/hooks.json', '.cursor/rules/ark.mdc'],
130
130
  codex: ['.codex/hooks.json', '.codex/config.toml'],
131
131
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
132
- antigravity: ['.agents/hooks.json'],
132
+ antigravity: ['.agents/hooks.json', '.agents/mcp_config.json'],
133
133
  opencode: ['opencode.json', '.opencode/skills/ark-upgrade/SKILL.md'],
134
134
  windsurf: ['.windsurf/rules/ark.md', '.windsurf/workflows/ark-upgrade.md'],
135
135
  cline: ['.clinerules/ark.md', '.clinerules/workflows/ark-upgrade.md'],
@@ -36,6 +36,7 @@ export const COMMAND_GATE_JSON_FILES = [
36
36
  '.mcp.json',
37
37
  '.cursor/mcp.json',
38
38
  '.cursor/hooks.json',
39
+ '.agents/mcp_config.json',
39
40
  'opencode.json',
40
41
  ];
41
42
  // Primary CLI names (product) + one-major aliases. migrate-commands must strip ALL of these
@@ -208,10 +209,15 @@ export function collectAdoptionGaps(root, config, coverage) {
208
209
  host: 'antigravity',
209
210
  dir: '.agents',
210
211
  skill: (n) => path.join(root, '.agents', 'skills', n, 'SKILL.md'),
211
- extras: [['.agents/hooks.json', 'write-gate hook']],
212
+ extras: [
213
+ ['.agents/hooks.json', 'write-gate hook'],
214
+ ['.agents/mcp_config.json', 'workspace MCP'],
215
+ ],
212
216
  toolsFlag: 'antigravity',
213
- // Only when hooks.json is present — `.agents/skills` alone is Codex scope.
214
- presentIf: () => fs.existsSync(path.join(root, '.agents', 'hooks.json')),
217
+ // Hooks or official workspace MCP — `.agents/skills` alone is Codex scope.
218
+ presentIf: () =>
219
+ fs.existsSync(path.join(root, '.agents', 'hooks.json')) ||
220
+ fs.existsSync(path.join(root, '.agents', 'mcp_config.json')),
215
221
  },
216
222
  {
217
223
  host: 'opencode',
@@ -3,7 +3,10 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { analyzePolicyDelta } from './analysis-engine.mjs';
5
5
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
6
- import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
6
+ import {
7
+ invariantIdsFromCatalog,
8
+ loadInvariantCoverageInputs,
9
+ } from './invariant-coverage-io.mjs';
7
10
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
8
11
 
9
12
  function readJsonFile(filePath, label) {
@@ -175,12 +178,15 @@ export function analyzePolicyTransition({
175
178
 
176
179
  let candidateInvariantCoverage;
177
180
  if ((candidateArkRules?.invariants?.length ?? 0) > 0) {
178
- const coverageInputs = loadInvariantCoverageInputs(root, { files: [] });
181
+ const coverageInputs = loadInvariantCoverageInputs(root, { files: [] }, {
182
+ invariantIds: invariantIdsFromCatalog(candidateArkRules),
183
+ });
179
184
  const evaluated = evaluateInvariantCoverage({
180
185
  arkRules: candidateArkRules,
181
186
  fileContents: coverageInputs.fileContents,
182
187
  testFiles: coverageInputs.testFiles,
183
188
  testGlobsMissing: coverageInputs.testGlobsMissing,
189
+ coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
184
190
  });
185
191
  candidateInvariantCoverage = evaluated.coverage;
186
192
  }
@@ -100,6 +100,13 @@ const ARKRUN_JUDGMENT_RULE_IDS = new Set([
100
100
  'ARKRUN_DIRECT_NEW',
101
101
  'ARKRUN_TRANSPORT_BYPASS',
102
102
  ]);
103
+ const ARKORDER_JUDGMENT_RULE_IDS = new Set([
104
+ 'ARKORDER_MISSING_PLANE',
105
+ 'ARKORDER_KERNEL_IN_DOMAIN',
106
+ 'ARKORDER_GENERIC_UPDATE',
107
+ 'ARKORDER_TOO_MANY_PARAMS',
108
+ 'ARKORDER_INGEST_WRITES_XI',
109
+ ]);
103
110
  function arkRunCallSiteName(violation) {
104
111
  return typeof violation.target === 'string' && violation.target.trim().length > 0
105
112
  ? violation.target.trim()
@@ -119,12 +126,12 @@ function arkRunNextAction(violation) {
119
126
  switch (violation.ruleId) {
120
127
  case 'ARKRUN_MISSING_ROOT':
121
128
  return target
122
- ? `Import createStrictArkKernel from @arkgate/runtime and call it in composition root ${target} listed in arkRun.compositionRoots, then preflight again.`
123
- : 'Import createStrictArkKernel from @arkgate/runtime (never a removed arkgate/runtime shim) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.';
129
+ ? `Import createStrictArkKernel from arkgate/runtime and call it in composition root ${target} listed in arkRun.compositionRoots, then preflight again.`
130
+ : 'Import createStrictArkKernel from arkgate/runtime (same npm package; @arkgate/runtime is deprecated) and call it in a composition root listed in arkRun.compositionRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.';
124
131
  case 'ARKRUN_KERNEL_IN_DOMAIN':
125
132
  return target
126
- ? `Move the kernel import of ${target} out of ${fromLayer ?? 'the Domain-role layer'} into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again.`
127
- : 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from @arkgate/runtime, never a removed arkgate/runtime shim, then preflight again. Never mechanical-safe.';
133
+ ? `Move the kernel import of ${target} out of ${fromLayer ?? 'the Domain-role layer'} into a composition root or adapter. Import from arkgate/runtime, then preflight again.`
134
+ : 'Move the kernel import out of the Domain-role layer into a composition root or adapter. Import from arkgate/runtime (same npm package; @arkgate/runtime is deprecated), then preflight again. Never mechanical-safe.';
128
135
  case 'ARKRUN_DIRECT_NEW':
129
136
  return target
130
137
  ? `Resolve ${target} from the kernel instead of constructing it with new, then preflight again.`
@@ -154,12 +161,12 @@ function arkRunEnthusiastHint(violation) {
154
161
  switch (violation.ruleId) {
155
162
  case 'ARKRUN_MISSING_ROOT':
156
163
  return target
157
- ? `Call createStrictArkKernel from @arkgate/runtime in ${target} so the app actually uses the kernel.`
158
- : 'Call createStrictArkKernel from @arkgate/runtime in a listed composition root so the app actually uses the kernel.';
164
+ ? `Call createStrictArkKernel from arkgate/runtime in ${target} so the app actually uses the kernel.`
165
+ : 'Call createStrictArkKernel from arkgate/runtime in a listed composition root so the app actually uses the kernel.';
159
166
  case 'ARKRUN_KERNEL_IN_DOMAIN':
160
167
  return target
161
- ? `Domain stays kernel-free. Move the ${target} import to a composition root or adapter never a removed arkgate/runtime shim.`
162
- : 'Domain stays kernel-free. Move that @arkgate/runtime import to a composition root or adapter never a removed arkgate/runtime shim.';
168
+ ? `Domain stays kernel-free. Move the ${target} import to a composition root or adapter. Prefer arkgate/runtime.`
169
+ : 'Domain stays kernel-free. Move that kernel import to a composition root or adapter. Prefer arkgate/runtime; @arkgate/runtime is deprecated.';
163
170
  case 'ARKRUN_DIRECT_NEW':
164
171
  return target
165
172
  ? `Do not construct ${target} with new. Resolve it from the kernel instead.`
@@ -215,6 +222,18 @@ export function deterministicNextAction(violation) {
215
222
  case 'ARKRUN_UNDECLARED_DEPEND':
216
223
  case 'ARKRUN_TRANSPORT_BYPASS':
217
224
  return arkRunNextAction(violation);
225
+ case 'ARKORDER_MISSING_PLANE':
226
+ return typeof violation.target === 'string' && violation.target.length > 0
227
+ ? `Import createOrderPlane from arkgate/order and call it in plane root ${violation.target} listed in arkOrder.planeRoots, then preflight again.`
228
+ : 'Import createOrderPlane from arkgate/order and call it in a plane root listed in arkOrder.planeRoots, then preflight again. Never mechanical-safe — factory placement is a design decision.';
229
+ case 'ARKORDER_KERNEL_IN_DOMAIN':
230
+ return 'Move the arkgate/order import out of the Domain-role layer into a plane root or adapter, then preflight again. Never mechanical-safe.';
231
+ case 'ARKORDER_GENERIC_UPDATE':
232
+ return 'Use release() to freeze ξ or proposeRelease() for a pattern change with blast radius, then preflight again. Never mechanical-safe.';
233
+ case 'ARKORDER_TOO_MANY_PARAMS':
234
+ return 'Cut ξ to the slow keys that actually slave the rest, then preflight again. Never mechanical-safe.';
235
+ case 'ARKORDER_INGEST_WRITES_XI':
236
+ return 'Keep ingest results as absorb/escalate only. Change ξ with proposeRelease + release. Never mechanical-safe.';
218
237
  default:
219
238
  if (typeof violation.ruleId === 'string' && violation.ruleId.startsWith('ARKRULE_')) {
220
239
  return `Fix the ArkRule ${typeof violation.arkruleId === 'string' ? violation.arkruleId : violation.ruleId}, then preflight again.`;
@@ -340,7 +359,9 @@ export function classifyRemediation(violation) {
340
359
  };
341
360
  }
342
361
  if (typeof ruleId === 'string' &&
343
- (ARKRUN_JUDGMENT_RULE_IDS.has(ruleId) || ARKRUN_UNDECLARED_RULE_IDS.has(ruleId))) {
362
+ (ARKRUN_JUDGMENT_RULE_IDS.has(ruleId) ||
363
+ ARKRUN_UNDECLARED_RULE_IDS.has(ruleId) ||
364
+ ARKORDER_JUDGMENT_RULE_IDS.has(ruleId))) {
344
365
  return {
345
366
  class: 'judgment',
346
367
  confidence: 0.85,
@@ -470,6 +491,24 @@ export function enrichViolationWithFixClass(violation) {
470
491
  enriched.effort = ARKRUN_UNDECLARED_RULE_IDS.has(violation.ruleId ?? '') ? 'small' : 'medium';
471
492
  enriched.enthusiastHint = arkRunEnthusiastHint(violation);
472
493
  break;
494
+ case 'ARKORDER_MISSING_PLANE':
495
+ case 'ARKORDER_KERNEL_IN_DOMAIN':
496
+ case 'ARKORDER_GENERIC_UPDATE':
497
+ case 'ARKORDER_TOO_MANY_PARAMS':
498
+ case 'ARKORDER_INGEST_WRITES_XI':
499
+ enriched.fixClass = 'arkorder-usage';
500
+ enriched.effort = 'medium';
501
+ enriched.enthusiastHint =
502
+ violation.ruleId === 'ARKORDER_GENERIC_UPDATE'
503
+ ? 'Do not PATCH the slow pattern. Freeze ξ with release() or propose a pattern change that lists what dies.'
504
+ : violation.ruleId === 'ARKORDER_KERNEL_IN_DOMAIN'
505
+ ? 'Domain stays plane-free. Import arkgate/order only from a listed plane root.'
506
+ : violation.ruleId === 'ARKORDER_TOO_MANY_PARAMS'
507
+ ? 'Too many slow keys. Keep ξ small — the rest is derived noise.'
508
+ : violation.ruleId === 'ARKORDER_INGEST_WRITES_XI'
509
+ ? 'ingest can absorb or escalate. It never writes a new house.'
510
+ : 'Call createOrderPlane from arkgate/order in a listed plane root so the app actually freezes a pattern.';
511
+ break;
473
512
  default:
474
513
  enriched.fixClass = 'review-contract';
475
514
  enriched.effort = 'small';
@@ -40,6 +40,10 @@ import {
40
40
  extractArkRunKernelCallsFromSource,
41
41
  extractArkRunManagedNewsFromSource,
42
42
  } from './ark-run-facts.mjs';
43
+ import {
44
+ extractArkOrderGenericUpdatesFromSource,
45
+ extractArkOrderPlaneCallsFromSource,
46
+ } from './ark-order-facts.mjs';
43
47
  import {
44
48
  collectGovernedFiles,
45
49
  isGovernableSourceFile,
@@ -1008,7 +1012,11 @@ export function resolveCandidateFacts({
1008
1012
  const arkRunManagedNews = [];
1009
1013
  const arkRunCompositionRootHits = [];
1010
1014
  const arkRunDeclarations = [];
1015
+ const arkOrderPlaneCalls = [];
1016
+ const arkOrderGenericUpdates = [];
1017
+ const arkOrderRootHits = [];
1011
1018
  const compositionRootPatterns = [...(config.arkRun?.compositionRoots ?? [])];
1019
+ const planeRootPatterns = [...(config.arkOrder?.planeRoots ?? [])];
1012
1020
 
1013
1021
  for (const candidate of candidateFiles) {
1014
1022
  const sourceFile = ts.createSourceFile(
@@ -1112,6 +1120,20 @@ export function resolveCandidateFacts({
1112
1120
  } catch {
1113
1121
  // Never fail the resolver for ArkRun declaration extraction.
1114
1122
  }
1123
+ try {
1124
+ arkOrderPlaneCalls.push(
1125
+ ...extractArkOrderPlaneCallsFromSource(candidate.path, candidate.content)
1126
+ );
1127
+ } catch {
1128
+ // Never fail the resolver for ArkOrder factory extraction.
1129
+ }
1130
+ try {
1131
+ arkOrderGenericUpdates.push(
1132
+ ...extractArkOrderGenericUpdatesFromSource(candidate.path, candidate.content)
1133
+ );
1134
+ } catch {
1135
+ // Never fail the resolver for ArkOrder generic-update extraction.
1136
+ }
1115
1137
  }
1116
1138
  }
1117
1139
 
@@ -1131,6 +1153,24 @@ export function resolveCandidateFacts({
1131
1153
  }
1132
1154
  }
1133
1155
 
1156
+ if (planeRootPatterns.length > 0) {
1157
+ const factoryFiles = new Set(arkOrderPlaneCalls.map((call) => call.file));
1158
+ for (const candidate of candidateFiles) {
1159
+ for (const pattern of planeRootPatterns) {
1160
+ try {
1161
+ if (!globToRegExp(pattern).test(candidate.path)) continue;
1162
+ } catch {
1163
+ continue;
1164
+ }
1165
+ arkOrderRootHits.push({
1166
+ file: candidate.path,
1167
+ matchedRoot: pattern,
1168
+ hasPlaneFactory: factoryFiles.has(candidate.path),
1169
+ });
1170
+ }
1171
+ }
1172
+ }
1173
+
1134
1174
  if (compositionRootPatterns.length > 0) {
1135
1175
  const factoryFiles = new Set(
1136
1176
  arkRunKernelCalls.filter((call) => call.kind === 'factory').map((call) => call.file)
@@ -1234,5 +1274,8 @@ export function resolveCandidateFacts({
1234
1274
  arkRunManagedNews,
1235
1275
  arkRunCompositionRootHits,
1236
1276
  arkRunDeclarations,
1277
+ arkOrderPlaneCalls,
1278
+ arkOrderGenericUpdates,
1279
+ arkOrderRootHits,
1237
1280
  });
1238
1281
  }
@@ -6,7 +6,10 @@
6
6
  */
7
7
  import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
8
8
  import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
9
- import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
9
+ import {
10
+ invariantIdsFromCatalog,
11
+ loadInvariantCoverageInputs,
12
+ } from './invariant-coverage-io.mjs';
10
13
  import {
11
14
  EXTRA_MERGE_TEETH_GOVERNED_FLOOR,
12
15
  composeMergePlanesHonesty,
@@ -89,13 +92,16 @@ export function summarizeRulesUnderContract(root, config, facts, classification)
89
92
  const invariants = loaded.arkRules.invariants?.length ?? 0;
90
93
  const coverageInputs =
91
94
  invariants > 0
92
- ? loadInvariantCoverageInputs(root, facts ?? { files: [] })
95
+ ? loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
96
+ invariantIds: invariantIdsFromCatalog(loaded.arkRules),
97
+ })
93
98
  : { fileContents: {}, testFiles: [], testGlobsMissing: false };
94
99
  const coverage = evaluateInvariantCoverage({
95
100
  arkRules: loaded.arkRules,
96
101
  fileContents: coverageInputs.fileContents,
97
102
  testFiles: coverageInputs.testFiles,
98
103
  testGlobsMissing: coverageInputs.testGlobsMissing,
104
+ coverageBudgetExhausted: coverageInputs.coverageBudgetExhausted === true,
99
105
  });
100
106
  const covById = new Map(
101
107
  (coverage.coverage ?? []).map((row) => [row.invariantId, row])