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.
- package/CHANGELOG.md +125 -0
- package/README.md +16 -4
- package/bin/ark-check-runtime.mjs +75 -3
- package/bin/ark-mcp-runtime.mjs +94 -0
- package/bin/lib/adapter-contract.mjs +14 -1
- package/bin/lib/analysis-engine.mjs +8 -8
- package/bin/lib/architecture-scan.mjs +35 -2
- package/bin/lib/arkrule-file-hints.mjs +71 -0
- package/bin/lib/arkrules-contract.mjs +382 -0
- package/bin/lib/arkrules-sensors.mjs +411 -0
- package/bin/lib/config-contract.mjs +85 -6
- package/bin/lib/doctor-advisories.mjs +14 -1
- package/bin/lib/doctor-plan.mjs +21 -0
- package/bin/lib/effective-contract-load.mjs +116 -0
- package/bin/lib/field-install.mjs +104 -0
- package/bin/lib/graph-blind.mjs +20 -1
- package/bin/lib/html-report-advisories.mjs +12 -5
- package/bin/lib/install-migrate.mjs +20 -2
- package/bin/lib/invariant-coverage-io.mjs +157 -0
- package/bin/lib/invariant-coverage.mjs +127 -0
- package/bin/lib/managed-upgrade.mjs +1 -1
- package/bin/lib/policy-delta-io.mjs +33 -0
- package/bin/lib/presets.mjs +241 -1
- package/bin/lib/remediation.mjs +28 -0
- package/bin/lib/resolved-candidate-facts.mjs +14 -1
- package/bin/lib/rules-inventory.mjs +144 -0
- package/bin/lib/rules-under-contract.mjs +320 -0
- package/bin/lib/start-preview.mjs +24 -7
- package/bin/lib/upgrade-command.mjs +373 -16
- package/dist/{configTypes-DAPvBqK6.d.ts → configTypes-CC0FEXoF.d.ts} +16 -3
- package/dist/eslint/index.cjs +2 -2
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +2 -2
- package/dist/index.cjs +14 -7
- package/dist/index.d.ts +615 -20
- package/dist/index.js +13 -6
- package/docs/README.md +5 -3
- package/docs/agent-guide.md +7 -3
- package/docs/ai-gates.md +6 -1
- package/docs/brownfield-adoption.md +22 -0
- package/docs/configuration.md +53 -4
- package/docs/develop.md +8 -2
- package/docs/enthusiast/README.md +11 -0
- package/docs/package-surface.md +13 -10
- package/docs/product-voice.md +11 -2
- package/docs/use.md +11 -0
- package/package.json +4 -17
- package/schemas/ark.analysis-result.schema.json +9 -1
- package/schemas/ark.arkrules.schema.json +141 -0
- package/schemas/ark.config.schema.json +10 -2
- package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
- package/server.json +2 -2
- package/templates/arkrules/ApplicationOrchestration.json +14 -0
- package/templates/arkrules/DomainModel.json +32 -0
- package/templates/arkrules/PersistenceAdapters.json +14 -0
- package/templates/arkrules/PresentationAdapters.json +14 -0
- package/templates/skills/ark-adopt.md +28 -1
- package/templates/skills/ark-architect.md +23 -0
- package/templates/skills/ark-autopilot.md +27 -1
- package/templates/skills/ark-contract.md +27 -1
- package/templates/skills/ark-coverage.md +23 -0
- package/templates/skills/ark-explain.md +39 -3
- package/templates/skills/ark-explore.md +26 -1
- package/templates/skills/ark-fix.md +23 -0
- package/templates/skills/ark-loop.md +23 -0
- package/templates/skills/ark-place.md +26 -0
- package/templates/skills/ark-runtime.md +4 -0
- package/templates/skills/ark-think.md +24 -1
- package/templates/skills/ark-upgrade.md +80 -11
- package/compat/nestjs.cjs +0 -2
- package/compat/nestjs.d.ts +0 -2
- package/compat/nestjs.js +0 -1
- package/compat/runtime.cjs +0 -2
- package/compat/runtime.d.ts +0 -2
- 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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
91
|
-
maxBytes
|
|
92
|
-
ok:
|
|
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
|
-
|
|
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) {
|