arkgate 3.9.2 → 4.0.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 (75) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +16 -4
  3. package/bin/ark-check-runtime.mjs +75 -3
  4. package/bin/ark-mcp-runtime.mjs +94 -0
  5. package/bin/lib/adapter-contract.mjs +14 -1
  6. package/bin/lib/analysis-engine.mjs +8 -8
  7. package/bin/lib/architecture-scan.mjs +35 -2
  8. package/bin/lib/arkrule-file-hints.mjs +71 -0
  9. package/bin/lib/arkrules-contract.mjs +382 -0
  10. package/bin/lib/arkrules-sensors.mjs +411 -0
  11. package/bin/lib/config-contract.mjs +85 -6
  12. package/bin/lib/doctor-advisories.mjs +14 -1
  13. package/bin/lib/doctor-plan.mjs +21 -0
  14. package/bin/lib/effective-contract-load.mjs +116 -0
  15. package/bin/lib/field-install.mjs +104 -0
  16. package/bin/lib/graph-blind.mjs +20 -1
  17. package/bin/lib/html-report-advisories.mjs +12 -5
  18. package/bin/lib/install-migrate.mjs +20 -2
  19. package/bin/lib/invariant-coverage-io.mjs +157 -0
  20. package/bin/lib/invariant-coverage.mjs +127 -0
  21. package/bin/lib/managed-upgrade.mjs +1 -1
  22. package/bin/lib/policy-delta-io.mjs +33 -0
  23. package/bin/lib/presets.mjs +241 -1
  24. package/bin/lib/remediation.mjs +28 -0
  25. package/bin/lib/resolved-candidate-facts.mjs +14 -1
  26. package/bin/lib/rules-inventory.mjs +144 -0
  27. package/bin/lib/rules-under-contract.mjs +320 -0
  28. package/bin/lib/start-preview.mjs +24 -7
  29. package/bin/lib/upgrade-command.mjs +373 -16
  30. package/dist/{configTypes-DAPvBqK6.d.ts → configTypes-CC0FEXoF.d.ts} +16 -3
  31. package/dist/eslint/index.cjs +2 -2
  32. package/dist/eslint/index.d.ts +1 -1
  33. package/dist/eslint/index.js +2 -2
  34. package/dist/index.cjs +14 -7
  35. package/dist/index.d.ts +615 -20
  36. package/dist/index.js +13 -6
  37. package/docs/README.md +5 -3
  38. package/docs/agent-guide.md +7 -3
  39. package/docs/ai-gates.md +6 -1
  40. package/docs/brownfield-adoption.md +22 -0
  41. package/docs/configuration.md +53 -4
  42. package/docs/develop.md +8 -2
  43. package/docs/enthusiast/README.md +11 -0
  44. package/docs/package-surface.md +13 -10
  45. package/docs/product-voice.md +11 -2
  46. package/docs/use.md +11 -0
  47. package/package.json +4 -17
  48. package/schemas/ark.analysis-result.schema.json +9 -1
  49. package/schemas/ark.arkrules.schema.json +141 -0
  50. package/schemas/ark.config.schema.json +10 -2
  51. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  52. package/server.json +2 -2
  53. package/templates/arkrules/ApplicationOrchestration.json +14 -0
  54. package/templates/arkrules/DomainModel.json +32 -0
  55. package/templates/arkrules/PersistenceAdapters.json +14 -0
  56. package/templates/arkrules/PresentationAdapters.json +14 -0
  57. package/templates/skills/ark-adopt.md +28 -1
  58. package/templates/skills/ark-architect.md +23 -0
  59. package/templates/skills/ark-autopilot.md +27 -1
  60. package/templates/skills/ark-contract.md +27 -1
  61. package/templates/skills/ark-coverage.md +23 -0
  62. package/templates/skills/ark-explain.md +39 -3
  63. package/templates/skills/ark-explore.md +26 -1
  64. package/templates/skills/ark-fix.md +23 -0
  65. package/templates/skills/ark-loop.md +23 -0
  66. package/templates/skills/ark-place.md +26 -0
  67. package/templates/skills/ark-runtime.md +4 -0
  68. package/templates/skills/ark-think.md +24 -1
  69. package/templates/skills/ark-upgrade.md +80 -11
  70. package/compat/nestjs.cjs +0 -2
  71. package/compat/nestjs.d.ts +0 -2
  72. package/compat/nestjs.js +0 -1
  73. package/compat/runtime.cjs +0 -2
  74. package/compat/runtime.d.ts +0 -2
  75. package/compat/runtime.js +0 -1
@@ -0,0 +1,320 @@
1
+ /**
2
+ * AR12 — doctor/HTML "Rules under contract" (ArkRules plane — counts, never a score).
3
+ * Uses real file I/O for coverage evidence (never empty-fileContents stub).
4
+ * Summary includes per-layer + structure/invariant detail so showcase HTML /ark-explain
5
+ * can teach what is under contract, not only aggregate numbers.
6
+ */
7
+ import { loadEffectiveArkRulesFromDisk } from './effective-contract-load.mjs';
8
+ import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
9
+ import { loadInvariantCoverageInputs } from './invariant-coverage-io.mjs';
10
+
11
+ /**
12
+ * Cap long catalogs in doctor JSON (and HTML, which consumes the same summary).
13
+ * Covered is a sample; structure/uncovered are truncated with *Truncated counters.
14
+ */
15
+ const COVERED_SAMPLE_MAX = 24;
16
+ const STRUCTURE_CATALOG_MAX = 40;
17
+ const UNCOVERED_CATALOG_MAX = 30;
18
+
19
+ /**
20
+ * @param {string} root
21
+ * @param {Record<string, unknown>} config
22
+ * @param {{ files?: Array<{ path: string }> }} [facts] optional facts for path set
23
+ */
24
+ export function summarizeRulesUnderContract(root, config, facts) {
25
+ if (!config?.arkRules || Object.keys(config.arkRules).length === 0) {
26
+ return {
27
+ active: false,
28
+ structureRules: 0,
29
+ invariants: 0,
30
+ coveredInvariants: 0,
31
+ uncoveredInvariants: 0,
32
+ notAScore: true,
33
+ note: 'No arkRules map — intra-layer ArkRules are opt-in.',
34
+ };
35
+ }
36
+ try {
37
+ const loaded = loadEffectiveArkRulesFromDisk(root, config);
38
+ if (loaded.errors?.length) {
39
+ return {
40
+ active: true,
41
+ loadErrors: loaded.errors,
42
+ notAScore: true,
43
+ note: 'ArkRules references failed to load (fail closed on full check).',
44
+ };
45
+ }
46
+ const structureRules = loaded.arkRules.structure?.length ?? 0;
47
+ const invariants = loaded.arkRules.invariants?.length ?? 0;
48
+ const coverageInputs =
49
+ invariants > 0
50
+ ? loadInvariantCoverageInputs(root, facts ?? { files: [] })
51
+ : { fileContents: {}, testFiles: [], testGlobsMissing: false };
52
+ const coverage = evaluateInvariantCoverage({
53
+ arkRules: loaded.arkRules,
54
+ fileContents: coverageInputs.fileContents,
55
+ testFiles: coverageInputs.testFiles,
56
+ testGlobsMissing: coverageInputs.testGlobsMissing,
57
+ });
58
+ const covById = new Map(
59
+ (coverage.coverage ?? []).map((row) => [row.invariantId, row])
60
+ );
61
+ const byLayer = loaded.arkRules.byLayer ?? {};
62
+ const layers = Object.keys(byLayer)
63
+ .sort((a, b) => a.localeCompare(b))
64
+ .map((name) => {
65
+ const part = byLayer[name] ?? {};
66
+ const layerInvariants = part.invariants ?? [];
67
+ let covered = 0;
68
+ for (const inv of layerInvariants) {
69
+ if (covById.get(inv.id)?.covered) covered += 1;
70
+ }
71
+ return {
72
+ name,
73
+ sourceFile: part.sourceFile ?? null,
74
+ structureRules: (part.structure ?? []).length,
75
+ invariants: layerInvariants.length,
76
+ coveredInvariants: covered,
77
+ uncoveredInvariants: layerInvariants.length - covered,
78
+ };
79
+ });
80
+
81
+ const structureAll = (loaded.arkRules.structure ?? []).map((entry) => ({
82
+ id: entry.id,
83
+ sensor: entry.sensor,
84
+ mode: entry.mode ?? 'advisory',
85
+ layer: entry.provenance?.layer ?? null,
86
+ description: entry.description ?? null,
87
+ sourceFile: entry.provenance?.sourceFile ?? null,
88
+ }));
89
+ const structureTruncated = Math.max(0, structureAll.length - STRUCTURE_CATALOG_MAX);
90
+ const structure = structureAll.slice(0, STRUCTURE_CATALOG_MAX);
91
+
92
+ const uncoveredAll = (coverage.coverage ?? [])
93
+ .filter((row) => !row.covered)
94
+ .map((row) => ({
95
+ id: row.invariantId,
96
+ layer: row.layer ?? null,
97
+ mode: row.mode ?? null,
98
+ description: row.description ?? null,
99
+ sourceFile: row.sourceFile ?? null,
100
+ }));
101
+ const uncoveredTruncated = Math.max(0, uncoveredAll.length - UNCOVERED_CATALOG_MAX);
102
+ const uncovered = uncoveredAll.slice(0, UNCOVERED_CATALOG_MAX);
103
+
104
+ const coveredAll = (coverage.coverage ?? [])
105
+ .filter((row) => row.covered)
106
+ .map((row) => ({
107
+ id: row.invariantId,
108
+ layer: row.layer ?? null,
109
+ mode: row.mode ?? null,
110
+ description: row.description ?? null,
111
+ }));
112
+ const coveredTruncated = Math.max(0, coveredAll.length - COVERED_SAMPLE_MAX);
113
+ const coveredSample = coveredAll.slice(0, COVERED_SAMPLE_MAX);
114
+
115
+ return {
116
+ active: true,
117
+ structureRules,
118
+ invariants,
119
+ coveredInvariants: coverage.coverage.filter((c) => c.covered).length,
120
+ uncoveredInvariants: coverage.coverage.filter((c) => !c.covered).length,
121
+ partialCoverage: coverage.partial,
122
+ testFilesScanned: coverageInputs.testFiles.length,
123
+ layers,
124
+ structure,
125
+ structureTruncated,
126
+ uncovered,
127
+ uncoveredTruncated,
128
+ coveredSample,
129
+ coveredTruncated,
130
+ notAScore: true,
131
+ note: 'ArkRules plane (intra-layer) — counts and catalog, never a score. Green with uncovered residual must say so.',
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ active: true,
136
+ notAScore: true,
137
+ note: error instanceof Error ? error.message : String(error),
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Showcase HTML for the ArkRules plane (used by html-report-advisories).
144
+ * @param {ReturnType<typeof summarizeRulesUnderContract>|null|undefined} section
145
+ * @param {(v: unknown) => string} esc
146
+ */
147
+ export function formatRulesUnderContractHtml(section, esc) {
148
+ if (!section || typeof section !== 'object') return '';
149
+ const escape = typeof esc === 'function' ? esc : (v) => String(v);
150
+ const note = section.note ? `<p class="muted">${escape(section.note)}</p>` : '';
151
+
152
+ if (section.active === false) {
153
+ return `
154
+ <section class="section card" data-advisory="rulesUnderContract">
155
+ <h2>Rules under contract <span class="muted">(ArkRules opt-in)</span></h2>
156
+ <p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
157
+ Intra-layer plane (structure sensors + domain invariants as data). Separate from inter-layer import edges.
158
+ </p>
159
+ ${note}
160
+ </section>`;
161
+ }
162
+
163
+ if (Array.isArray(section.loadErrors) && section.loadErrors.length) {
164
+ const errs = section.loadErrors
165
+ .slice(0, 8)
166
+ .map((e) => `<li><code>${escape(e.path ?? '')}</code> — ${escape(e.message ?? e)}</li>`)
167
+ .join('');
168
+ return `
169
+ <section class="section card" data-advisory="rulesUnderContract">
170
+ <h2>Rules under contract <span class="muted">(load errors)</span></h2>
171
+ ${note}
172
+ <ul class="senior-list">${errs}</ul>
173
+ </section>`;
174
+ }
175
+
176
+ const layers = Array.isArray(section.layers) ? section.layers : [];
177
+ const structure = Array.isArray(section.structure) ? section.structure : [];
178
+ const uncovered = Array.isArray(section.uncovered) ? section.uncovered : [];
179
+ const coveredSample = Array.isArray(section.coveredSample) ? section.coveredSample : [];
180
+ const coveredTruncated = Number(section.coveredTruncated) || 0;
181
+ const structureTruncated = Number(section.structureTruncated) || 0;
182
+ const uncoveredTruncated = Number(section.uncoveredTruncated) || 0;
183
+
184
+ const layerRows = layers
185
+ .map((row) => {
186
+ const cov =
187
+ row.invariants > 0
188
+ ? `${row.coveredInvariants}/${row.invariants} inv covered`
189
+ : 'no invariants';
190
+ return `<tr>
191
+ <td class="ln">${escape(row.name)}${
192
+ row.sourceFile ? `<div class="tags"><span class="tag"><code>${escape(row.sourceFile)}</code></span></div>` : ''
193
+ }</td>
194
+ <td class="num">${Number(row.structureRules) || 0}</td>
195
+ <td class="num">${Number(row.invariants) || 0}</td>
196
+ <td>${escape(cov)}${
197
+ row.uncoveredInvariants > 0
198
+ ? ` <span class="tag warn">${row.uncoveredInvariants} uncovered</span>`
199
+ : ''
200
+ }</td>
201
+ </tr>`;
202
+ })
203
+ .join('\n');
204
+
205
+ const layerTable = layers.length
206
+ ? `<table class="layers" style="margin-top:.55rem">
207
+ <thead><tr><th>Layer</th><th>Structure</th><th>Invariants</th><th>Coverage</th></tr></thead>
208
+ <tbody>${layerRows}</tbody>
209
+ </table>`
210
+ : '';
211
+
212
+ // Doctor JSON already caps catalogs; slice again only if a caller passed untruncated arrays.
213
+ const structureShown = structure.slice(0, STRUCTURE_CATALOG_MAX);
214
+ const structureOverflow =
215
+ structureTruncated > 0
216
+ ? structureTruncated
217
+ : Math.max(0, structure.length - STRUCTURE_CATALOG_MAX);
218
+ const structureItems = structureShown
219
+ .map((s) => {
220
+ const mode = s.mode === 'enforced' ? 'enforced' : s.mode === 'advisory' ? 'advisory' : String(s.mode ?? '');
221
+ const modeTag =
222
+ mode === 'enforced'
223
+ ? '<span class="tag">enforced</span>'
224
+ : `<span class="tag warn">${escape(mode || 'mode?')}</span>`;
225
+ return `<li>
226
+ <code>${escape(s.id)}</code>
227
+ ${modeTag}
228
+ <span class="dim">· ${escape(s.layer || '?')} · sensor <code>${escape(s.sensor || '')}</code></span>
229
+ ${s.description ? `<div class="msg">${escape(s.description)}</div>` : ''}
230
+ </li>`;
231
+ })
232
+ .join('\n');
233
+ const structureMore =
234
+ structureOverflow > 0
235
+ ? `<p class="muted">…(+${structureOverflow} more structure rule(s) in arkrules/*)</p>`
236
+ : '';
237
+
238
+ const uncoveredShown = uncovered.slice(0, UNCOVERED_CATALOG_MAX);
239
+ const uncoveredOverflow =
240
+ uncoveredTruncated > 0
241
+ ? uncoveredTruncated
242
+ : Math.max(0, uncovered.length - UNCOVERED_CATALOG_MAX);
243
+ const uncoveredItems = uncoveredShown
244
+ .map(
245
+ (u) => `<li>
246
+ <code>${escape(u.id)}</code>
247
+ <span class="tag warn">uncovered</span>
248
+ <span class="dim">· ${escape(u.layer || '?')}</span>
249
+ ${u.description ? `<div class="msg">${escape(u.description)}</div>` : ''}
250
+ </li>`
251
+ )
252
+ .join('\n');
253
+ const uncoveredMore =
254
+ uncoveredOverflow > 0
255
+ ? `<p class="muted">…(+${uncoveredOverflow} more uncovered)</p>`
256
+ : '';
257
+ const uncoveredBlock =
258
+ // Aggregate total (not the truncated array length) decides "all covered".
259
+ Number(section.uncoveredInvariants) === 0 && uncovered.length === 0
260
+ ? `<p class="clean-body" style="margin-top:.55rem">All catalogued invariants have coverage evidence (test/symbol scan) — residual inventory may still suggest new candidates via <code>--rules-inventory</code>.</p>`
261
+ : `<h3 style="margin-top:.9rem;font-size:.95rem">Uncovered invariants</h3>
262
+ <ul class="senior-list">${uncoveredItems}</ul>${uncoveredMore}`;
263
+
264
+ const coveredItems = coveredSample
265
+ .map(
266
+ (c) => `<li>
267
+ <code>${escape(c.id)}</code>
268
+ <span class="tag">covered</span>
269
+ <span class="dim">· ${escape(c.layer || '?')}</span>
270
+ ${c.description ? `<div class="msg">${escape(c.description)}</div>` : ''}
271
+ </li>`
272
+ )
273
+ .join('\n');
274
+ const coveredBlock =
275
+ coveredSample.length === 0
276
+ ? ''
277
+ : `<h3 style="margin-top:.9rem;font-size:.95rem">Covered invariants${
278
+ coveredTruncated > 0 ? ` <span class="dim">(sample of ${coveredSample.length})</span>` : ''
279
+ }</h3>
280
+ <ul class="senior-list">${coveredItems}</ul>
281
+ ${
282
+ coveredTruncated > 0
283
+ ? `<p class="muted">…(+${coveredTruncated} more covered — full catalog in <code>arkrules/*</code>)</p>`
284
+ : ''
285
+ }`;
286
+
287
+ return `
288
+ <section class="section card" data-advisory="rulesUnderContract">
289
+ <h2>Rules under contract <span class="muted">(ArkRules — not a score)</span></h2>
290
+ <p class="dim" style="margin:.15rem 0 .55rem;font-size:.88rem">
291
+ <b>[ArkRules]</b> Intra-layer plane — separate from <b>[Layer]</b> import edges above.
292
+ <b>Structure</b> = module-shape heuristics (not proof of Domain extraction).
293
+ <b>Invariants</b> = named policies + coverage evidence (symbol/test), not a business runtime
294
+ and not a fitness score.
295
+ </p>
296
+ <div class="kpis" style="margin-bottom:.55rem">
297
+ <div class="kpi"><b>${Number(section.structureRules) || 0}</b><span>Structure rules</span></div>
298
+ <div class="kpi"><b>${Number(section.invariants) || 0}</b><span>Invariants</span></div>
299
+ <div class="kpi"><b>${Number(section.coveredInvariants) || 0}</b><span>Covered</span></div>
300
+ <div class="kpi"><b>${Number(section.uncoveredInvariants) || 0}</b><span>Uncovered</span></div>
301
+ </div>
302
+ ${layers.length ? `<p class="dim" style="margin:0 0 .35rem;font-size:.86rem">${layers.length} layer(s) with an <code>arkRules</code> map entry · tests scanned: ${Number(section.testFilesScanned) || 0}</p>` : ''}
303
+ ${layerTable}
304
+ ${
305
+ structure.length
306
+ ? `<h3 style="margin-top:.9rem;font-size:.95rem">Structure sensors</h3>
307
+ <p class="muted" style="margin:.15rem 0 .4rem;font-size:.84rem">Heuristics of module shape. Enforced fails the check; it does not prove extraction to Domain.</p>
308
+ <ul class="senior-list">${structureItems}</ul>${structureMore}`
309
+ : '<p class="muted" style="margin-top:.55rem">No structure sensors in loaded ArkRules files.</p>'
310
+ }
311
+ ${uncoveredBlock}
312
+ ${coveredBlock}
313
+ ${
314
+ coveredSample.length || uncovered.length
315
+ ? `<p class="muted" style="margin-top:.65rem;font-size:.84rem">Covered = catalog evidence found (symbol and/or test title). Not a claim that business semantics are fully proven end-to-end.</p>`
316
+ : ''
317
+ }
318
+ ${note}
319
+ </section>`;
320
+ }
@@ -76,20 +76,31 @@ function change(pathname, before, after) {
76
76
  };
77
77
  }
78
78
 
79
- function setupBudget(changes) {
79
+ /**
80
+ * Compact start onboarding budget. Exported for pure unit tests.
81
+ * Gate surface (non-arkrules) ≤ maxFiles; arkrules count toward bytes only.
82
+ */
83
+ export function setupBudget(changes) {
80
84
  const generatedChanges = changes.filter((item) => item.path !== 'package.json');
85
+ // Compact gate surface stays ≤8 files (MCP + one host + CI + AGENTS + config).
86
+ // ArkRules starters (arkrules/*.json) are opt-in contract content and count against
87
+ // the byte budget only — AR08 4.0 emit was failing field start at 10/8 otherwise.
88
+ const gateChanges = generatedChanges.filter((item) => !item.path.startsWith('arkrules/'));
89
+ const arkrulesFiles = generatedChanges.length - gateChanges.length;
81
90
  const bytes = generatedChanges.reduce(
82
91
  (total, item) => total + (item.afterBase64 ? Buffer.from(item.afterBase64, 'base64').length : 0),
83
92
  0
84
93
  );
85
- // Compact start includes shared MCP + one host registration + CI + AGENTS + config.
86
- // Budget raised from 5→8 so .mcp.json always fits (field: grok compact hit the old ceiling).
94
+ const maxFiles = 8;
95
+ const maxBytes = 32 * 1024;
87
96
  return {
88
97
  files: generatedChanges.length,
98
+ gateFiles: gateChanges.length,
99
+ arkrulesFiles,
89
100
  bytes,
90
- maxFiles: 8,
91
- maxBytes: 32 * 1024,
92
- ok: generatedChanges.length <= 8 && bytes < 32 * 1024,
101
+ maxFiles,
102
+ maxBytes,
103
+ ok: gateChanges.length <= maxFiles && bytes < maxBytes,
93
104
  };
94
105
  }
95
106
 
@@ -117,7 +128,13 @@ export function renderStartPreview(preview) {
117
128
  console.log(`Your project looks like: ${preview.analysis.label} (${preview.analysis.archetype}, confidence ${preview.analysis.confidence}).`);
118
129
  }
119
130
  console.log(`Projected governed coverage: ${preview.projectedCoverage.percent ?? 'unknown'}% (${preview.projectedCoverage.classifiedFiles}/${preview.projectedCoverage.totalFiles} files)`);
120
- console.log(`Compact setup budget: ${preview.setupBudget.files}/${preview.setupBudget.maxFiles} files, ${preview.setupBudget.bytes}/${preview.setupBudget.maxBytes} bytes${preview.setupBudget.ok ? '' : ' (exceeded)'}.`);
131
+ const budget = preview.setupBudget;
132
+ const arkrulesNote =
133
+ budget.arkrulesFiles > 0 ? ` (+${budget.arkrulesFiles} arkrules)` : '';
134
+ const gateCount = budget.gateFiles ?? budget.files;
135
+ console.log(
136
+ `Compact setup budget: ${gateCount}/${budget.maxFiles} gate files${arkrulesNote}, ${budget.bytes}/${budget.maxBytes} bytes${budget.ok ? '' : ' (exceeded)'}.`
137
+ );
121
138
  console.log('Files to create/edit/delete:');
122
139
  if (preview.changes.length === 0) console.log(' (none)');
123
140
  for (const change of preview.changes) {