arkgate 2.8.1 → 2.8.3
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 +57 -2
- package/README.md +4 -1
- package/bin/ark-check.mjs +8 -0
- package/bin/ark-shared.mjs +67 -5
- package/bin/ark.mjs +55 -9
- package/bin/lib/agent-gates.mjs +136 -23
- package/bin/lib/config-warnings.mjs +1 -1
- package/bin/lib/core-ratchet.mjs +152 -0
- package/bin/lib/doctor-plan.mjs +16 -0
- package/bin/lib/html-report.mjs +15 -0
- package/bin/lib/presets.mjs +86 -12
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/docs/brownfield-adoption.md +1 -1
- package/docs/package-surface.md +3 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/templates/skills/ark-autopilot.md +3 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-layer optionality ratchet — pure plan + CLI runner.
|
|
3
|
+
* Keeps ark-check.mjs orchestration-only (dispatch only).
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { arkCommand } from '../ark-shared.mjs';
|
|
8
|
+
import { computeCoverage } from './doctor-plan.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Core layers whose optionality matters once they match files (presets share these names).
|
|
12
|
+
* Used by doctor adoption gaps and `--ratchet-cores`.
|
|
13
|
+
*/
|
|
14
|
+
export const CORE_LAYER_NAMES = new Set([
|
|
15
|
+
'DomainModel',
|
|
16
|
+
'ApplicationOrchestration',
|
|
17
|
+
'PresentationAdapters',
|
|
18
|
+
'PersistenceAdapters',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Plan a ratchet of optional→required for core layers that already match files.
|
|
23
|
+
* Empty cores stay optional (avoids false ENFORCE theatre). Pure — does not write disk.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} config ark.config.json shape
|
|
26
|
+
* @param {{ name: string, files: number }[]} layerRows coverage layer rows
|
|
27
|
+
*/
|
|
28
|
+
export function planPopulatedCoreRatchet(config, layerRows = []) {
|
|
29
|
+
const countByName = new Map(
|
|
30
|
+
(Array.isArray(layerRows) ? layerRows : []).map((row) => [row.name, Number(row.files) || 0])
|
|
31
|
+
);
|
|
32
|
+
const ratcheted = [];
|
|
33
|
+
const alreadyStrict = [];
|
|
34
|
+
const stillOptionalEmpty = [];
|
|
35
|
+
const nextLayers = (config?.layers ?? []).map((layer) => {
|
|
36
|
+
if (!CORE_LAYER_NAMES.has(layer.name)) return layer;
|
|
37
|
+
const files = countByName.get(layer.name) ?? 0;
|
|
38
|
+
if (layer.optional !== true) {
|
|
39
|
+
if (files > 0) alreadyStrict.push({ layer: layer.name, files });
|
|
40
|
+
return layer;
|
|
41
|
+
}
|
|
42
|
+
if (files <= 0) {
|
|
43
|
+
stillOptionalEmpty.push(layer.name);
|
|
44
|
+
return layer;
|
|
45
|
+
}
|
|
46
|
+
ratcheted.push({ layer: layer.name, files });
|
|
47
|
+
return { ...layer, optional: false };
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
ratcheted,
|
|
51
|
+
alreadyStrict,
|
|
52
|
+
stillOptionalEmpty,
|
|
53
|
+
config: { ...(config ?? {}), layers: nextLayers },
|
|
54
|
+
changed: ratcheted.length > 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When the architecture is green (raw violations = 0, not baselined), ratchet populated
|
|
60
|
+
* core layers from optional→required so doctor can honestly report ENFORCE.
|
|
61
|
+
* Empty cores stay optional. Always writes when changes apply (like --update-baseline).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} root
|
|
64
|
+
* @param {object} config
|
|
65
|
+
* @param {string[]} files
|
|
66
|
+
* @param {object[]} rules
|
|
67
|
+
* @param {object[]} violations raw scan violations (baseline ignored — must be truly clean)
|
|
68
|
+
* @param {{ json?: boolean, config?: string }} args
|
|
69
|
+
* @param {{ displayPathFromRoot: (root: string, abs: string) => string }} deps
|
|
70
|
+
*/
|
|
71
|
+
export function runRatchetCores(root, config, files, rules, violations, args, deps) {
|
|
72
|
+
const displayPathFromRoot = deps.displayPathFromRoot;
|
|
73
|
+
const cov = computeCoverage(root, config, files, rules);
|
|
74
|
+
const activeCount = Array.isArray(violations) ? violations.length : 0;
|
|
75
|
+
const configPath = path.isAbsolute(args.config)
|
|
76
|
+
? args.config
|
|
77
|
+
: path.join(root, args.config || 'ark.config.json');
|
|
78
|
+
|
|
79
|
+
const refuse = (code, message, extra = {}) => {
|
|
80
|
+
if (args.json) {
|
|
81
|
+
console.log(JSON.stringify({ ok: false, error: message, ...extra }, null, 2));
|
|
82
|
+
} else {
|
|
83
|
+
console.error(message);
|
|
84
|
+
}
|
|
85
|
+
process.exitCode = code;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (activeCount > 0) {
|
|
89
|
+
refuse(
|
|
90
|
+
2,
|
|
91
|
+
`Refusing --ratchet-cores: ${activeCount} active architecture violation(s) (raw graph; baseline does not count). Resolve them first (ark-check --plan), then re-run.`,
|
|
92
|
+
{ activeViolations: activeCount, governed: cov.governed }
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (cov.totalFiles === 0 || (cov.governed?.percent ?? 0) < 50) {
|
|
97
|
+
refuse(
|
|
98
|
+
2,
|
|
99
|
+
`Refusing --ratchet-cores: governed coverage is too low (${cov.governed?.percent ?? 0}% of ${cov.totalFiles} files). Classify ungoverned code first.`,
|
|
100
|
+
{ governed: cov.governed }
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const plan = planPopulatedCoreRatchet(config, cov.layers);
|
|
106
|
+
if (!plan.changed) {
|
|
107
|
+
const payload = {
|
|
108
|
+
ok: true,
|
|
109
|
+
changed: false,
|
|
110
|
+
message: 'No optional core layers with files — nothing to ratchet.',
|
|
111
|
+
alreadyStrict: plan.alreadyStrict,
|
|
112
|
+
stillOptionalEmpty: plan.stillOptionalEmpty,
|
|
113
|
+
governed: cov.governed,
|
|
114
|
+
};
|
|
115
|
+
if (args.json) console.log(JSON.stringify(payload, null, 2));
|
|
116
|
+
else {
|
|
117
|
+
console.log(payload.message);
|
|
118
|
+
if (plan.stillOptionalEmpty.length > 0) {
|
|
119
|
+
console.log(`Still optional (empty patterns): ${plan.stillOptionalEmpty.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(configPath, `${JSON.stringify(plan.config, null, 2)}\n`);
|
|
126
|
+
const payload = {
|
|
127
|
+
ok: true,
|
|
128
|
+
changed: true,
|
|
129
|
+
configPath: displayPathFromRoot(root, configPath),
|
|
130
|
+
ratcheted: plan.ratcheted,
|
|
131
|
+
stillOptionalEmpty: plan.stillOptionalEmpty,
|
|
132
|
+
governed: cov.governed,
|
|
133
|
+
next: arkCommand(root, 'ark-check', '--doctor'),
|
|
134
|
+
};
|
|
135
|
+
if (args.json) {
|
|
136
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
137
|
+
} else {
|
|
138
|
+
console.log(
|
|
139
|
+
`Ratcheted ${plan.ratcheted.length} core layer(s) to optional: false (populated only):`
|
|
140
|
+
);
|
|
141
|
+
for (const row of plan.ratcheted) {
|
|
142
|
+
console.log(` ${row.layer} (${row.files} file(s))`);
|
|
143
|
+
}
|
|
144
|
+
if (plan.stillOptionalEmpty.length > 0) {
|
|
145
|
+
console.log(
|
|
146
|
+
`Left optional (empty patterns — avoid false ENFORCE): ${plan.stillOptionalEmpty.join(', ')}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
console.log(`Wrote ${displayPathFromRoot(root, configPath)}`);
|
|
150
|
+
console.log(`Confirm: ${payload.next}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -290,6 +290,15 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
290
290
|
governedPercent: cov.governed.percent,
|
|
291
291
|
planMet: activeCount === 0 && cov.governed.percent >= 50,
|
|
292
292
|
mature: cov.governed.totalFiles >= 150,
|
|
293
|
+
totalFiles: cov.governed.totalFiles,
|
|
294
|
+
emptyLayers: cov.emptyLayers,
|
|
295
|
+
coreOptionalWithFiles: adoption.coreOptional?.length ?? 0,
|
|
296
|
+
presentationShare: (() => {
|
|
297
|
+
const total = cov.governed.totalFiles || 0;
|
|
298
|
+
if (total <= 0) return null;
|
|
299
|
+
const p = cov.layers.find((r) => r.name === 'PresentationAdapters');
|
|
300
|
+
return p ? p.files / total : null;
|
|
301
|
+
})(),
|
|
293
302
|
}),
|
|
294
303
|
governed: cov.governed,
|
|
295
304
|
emptyLayers: cov.emptyLayers,
|
|
@@ -345,11 +354,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
345
354
|
console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
|
|
346
355
|
|
|
347
356
|
const emptyScope = cov.governed.totalFiles === 0;
|
|
357
|
+
const totalFiles = cov.governed.totalFiles || 0;
|
|
358
|
+
const presentationRow = cov.layers.find((r) => r.name === 'PresentationAdapters');
|
|
348
359
|
const mode = resolveOperatingMode({
|
|
349
360
|
governedPercent: emptyScope ? 0 : cov.governed.percent,
|
|
350
361
|
planMet:
|
|
351
362
|
activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
|
|
352
363
|
mature: cov.governed.totalFiles >= 150,
|
|
364
|
+
totalFiles: cov.governed.totalFiles,
|
|
365
|
+
emptyLayers: cov.emptyLayers,
|
|
366
|
+
coreOptionalWithFiles: adoption.coreOptional?.length ?? 0,
|
|
367
|
+
presentationShare:
|
|
368
|
+
totalFiles > 0 && presentationRow ? presentationRow.files / totalFiles : null,
|
|
353
369
|
});
|
|
354
370
|
console.log('');
|
|
355
371
|
console.log(color.bold('Operating mode'));
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -153,6 +153,17 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
153
153
|
const governedPercent = coverage?.governed?.percent ?? null;
|
|
154
154
|
const totalFiles = coverage?.governed?.totalFiles ?? 0;
|
|
155
155
|
const classifiedFiles = coverage?.governed?.classifiedFiles ?? 0;
|
|
156
|
+
const emptyLayers = (coverage?.layers ?? [])
|
|
157
|
+
.filter((r) => (r.files ?? 0) === 0)
|
|
158
|
+
.map((r) => r.name);
|
|
159
|
+
const presentationRow = (coverage?.layers ?? []).find(
|
|
160
|
+
(r) => r.name === 'PresentationAdapters'
|
|
161
|
+
);
|
|
162
|
+
const coreOptionalWithFiles = (config?.layers ?? []).filter((layer) => {
|
|
163
|
+
if (layer.optional !== true) return false;
|
|
164
|
+
const row = (coverage?.layers ?? []).find((r) => r.name === layer.name);
|
|
165
|
+
return (row?.files ?? 0) > 0;
|
|
166
|
+
}).length;
|
|
156
167
|
const mode = resolveOperatingMode({
|
|
157
168
|
governedPercent: totalFiles === 0 ? 0 : governedPercent,
|
|
158
169
|
planMet:
|
|
@@ -162,6 +173,10 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
|
|
|
162
173
|
(governedPercent == null || governedPercent >= 50),
|
|
163
174
|
mature: totalFiles >= 150,
|
|
164
175
|
totalFiles,
|
|
176
|
+
emptyLayers,
|
|
177
|
+
coreOptionalWithFiles,
|
|
178
|
+
presentationShare:
|
|
179
|
+
totalFiles > 0 && presentationRow ? presentationRow.files / totalFiles : null,
|
|
165
180
|
});
|
|
166
181
|
const modeLabel = { suggest: 'SUGGEST', adapt: 'ADAPT', enforce: 'ENFORCE' }[mode] || String(mode).toUpperCase();
|
|
167
182
|
const modeBlurb = {
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -267,22 +267,29 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
267
267
|
name: 'DomainModel',
|
|
268
268
|
description: 'Shared types and pure view-models (optional on UI-first trees).',
|
|
269
269
|
// Avoid bare **/types.ts — see monorepo DomainModel note (false Domain on core/**/types.ts).
|
|
270
|
-
patterns: [
|
|
270
|
+
patterns: [
|
|
271
|
+
'**/domain/**',
|
|
272
|
+
'**/cinematic/types.ts',
|
|
273
|
+
// Common pure types bag (not **/types.ts — that traps core/**/types.ts).
|
|
274
|
+
'src/lib/types.ts',
|
|
275
|
+
],
|
|
271
276
|
exclude: FRAMEWORK_INTERNAL_EXCLUDE,
|
|
272
277
|
forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
273
278
|
optional: true,
|
|
274
279
|
},
|
|
275
280
|
{
|
|
276
|
-
name: '
|
|
277
|
-
description: '
|
|
281
|
+
name: 'ApplicationOrchestration',
|
|
282
|
+
description: 'Server actions, features, and non-UI lib orchestration (when present).',
|
|
278
283
|
patterns: [
|
|
279
|
-
'
|
|
280
|
-
'
|
|
281
|
-
'
|
|
282
|
-
'
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
284
|
+
'src/features/**',
|
|
285
|
+
'src/server/**',
|
|
286
|
+
'src/services/**',
|
|
287
|
+
'src/use-cases/**',
|
|
288
|
+
'src/actions/**',
|
|
289
|
+
'src/lib/actions/**',
|
|
290
|
+
'src/lib/services/**',
|
|
291
|
+
'**/lib/actions/**',
|
|
292
|
+
'**/lib/services/**',
|
|
286
293
|
],
|
|
287
294
|
exclude: FRAMEWORK_INTERNAL_EXCLUDE,
|
|
288
295
|
optional: true,
|
|
@@ -290,7 +297,61 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
290
297
|
{
|
|
291
298
|
name: 'PersistenceAdapters',
|
|
292
299
|
description: 'Client data access and external API adapters (when present).',
|
|
293
|
-
|
|
300
|
+
// Prefer specific data-client bags over a presentation catch-all on **/lib/**
|
|
301
|
+
patterns: [
|
|
302
|
+
'**/infrastructure/**',
|
|
303
|
+
'**/adapters/**',
|
|
304
|
+
'**/repositories/**',
|
|
305
|
+
'**/persistence/**',
|
|
306
|
+
'src/db/**',
|
|
307
|
+
'src/data/**',
|
|
308
|
+
'src/lib/db/**',
|
|
309
|
+
'src/lib/prisma/**',
|
|
310
|
+
'src/lib/supabase/**',
|
|
311
|
+
'src/lib/airtable/**',
|
|
312
|
+
'src/lib/firebase/**',
|
|
313
|
+
'src/lib/firestore/**',
|
|
314
|
+
'src/lib/mongodb/**',
|
|
315
|
+
'src/lib/mongoose/**',
|
|
316
|
+
'src/lib/drizzle/**',
|
|
317
|
+
'src/lib/kysely/**',
|
|
318
|
+
'**/lib/supabase/**',
|
|
319
|
+
'**/lib/airtable/**',
|
|
320
|
+
'**/lib/prisma/**',
|
|
321
|
+
'**/lib/db/**',
|
|
322
|
+
],
|
|
323
|
+
exclude: FRAMEWORK_INTERNAL_EXCLUDE,
|
|
324
|
+
optional: true,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'PresentationAdapters',
|
|
328
|
+
description: 'UI, routes, hooks, components (not a whole-src bag).',
|
|
329
|
+
// No **/src/** or bare **/lib/** — those swallowed data clients and forced false ENFORCE.
|
|
330
|
+
patterns: [
|
|
331
|
+
'**/components/**',
|
|
332
|
+
'**/hooks/**',
|
|
333
|
+
'**/routes/**',
|
|
334
|
+
'**/app/**',
|
|
335
|
+
'**/pages/**',
|
|
336
|
+
'src/app/**',
|
|
337
|
+
'src/pages/**',
|
|
338
|
+
'src/components/**',
|
|
339
|
+
'src/hooks/**',
|
|
340
|
+
'src/ui/**',
|
|
341
|
+
'src/layouts/**',
|
|
342
|
+
'app/**',
|
|
343
|
+
'pages/**',
|
|
344
|
+
'components/**',
|
|
345
|
+
// Next middleware edge entry (classic + Next 16 proxy rename)
|
|
346
|
+
'src/middleware.ts',
|
|
347
|
+
'src/middleware.js',
|
|
348
|
+
'middleware.ts',
|
|
349
|
+
'middleware.js',
|
|
350
|
+
'src/proxy.ts',
|
|
351
|
+
'src/proxy.js',
|
|
352
|
+
'proxy.ts',
|
|
353
|
+
'proxy.js',
|
|
354
|
+
],
|
|
294
355
|
exclude: FRAMEWORK_INTERNAL_EXCLUDE,
|
|
295
356
|
optional: true,
|
|
296
357
|
},
|
|
@@ -298,7 +359,20 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
298
359
|
rules: [
|
|
299
360
|
{ from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
|
|
300
361
|
{ from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
|
|
301
|
-
{ from: '
|
|
362
|
+
{ from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
|
|
363
|
+
{ from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
|
|
364
|
+
// Next RSC often imports data clients from routes; deny is ideal but day-one
|
|
365
|
+
// ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
|
|
366
|
+
// Persistence → Presentation stays denied when that edge appears.
|
|
367
|
+
{
|
|
368
|
+
from: 'PresentationAdapters',
|
|
369
|
+
to: 'PersistenceAdapters',
|
|
370
|
+
allowed: true,
|
|
371
|
+
message:
|
|
372
|
+
'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.',
|
|
373
|
+
},
|
|
374
|
+
{ from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
|
|
375
|
+
{ from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
|
|
302
376
|
],
|
|
303
377
|
},
|
|
304
378
|
root
|