arkgate 2.9.0 → 2.9.2
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 +62 -0
- package/README.md +8 -2
- package/bin/ark-check.mjs +19 -1
- package/bin/ark.mjs +131 -20
- package/bin/lib/agent-gates.mjs +66 -6
- package/bin/lib/field-install.mjs +334 -0
- 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/package.json +1 -1
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +57 -10
- package/templates/skills/ark-architect.md +33 -2
- package/templates/skills/ark-autopilot.md +76 -14
- package/templates/skills/ark-contract.md +36 -0
- package/templates/skills/ark-coverage.md +81 -27
- package/templates/skills/ark-explain.md +34 -1
- package/templates/skills/ark-explore.md +119 -0
- package/templates/skills/ark-fix.md +47 -2
- package/templates/skills/ark-loop.md +48 -2
- package/templates/skills/ark-place.md +36 -0
- package/templates/skills/ark-runtime.md +36 -0
- package/templates/skills/ark-think.md +63 -12
- package/templates/skills/ark-upgrade.md +33 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-install surfaces: baseline flag sync, package pin, false-green contract risk.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of agent-gates.mjs so gate install / MCP / skills stay scannable.
|
|
5
|
+
* Zero coupling to template emission — pure-ish FS helpers + package.json mutators.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
12
|
+
|
|
13
|
+
function arkPackageVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
fs.readFileSync(path.join(__packageRoot, 'package.json'), 'utf8')
|
|
17
|
+
);
|
|
18
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure a check command string includes `--baseline <file>`.
|
|
26
|
+
* Only touches strings that already invoke ark-check / arkgate-check.
|
|
27
|
+
*/
|
|
28
|
+
export function ensureBaselineFlagInCheckCommand(
|
|
29
|
+
command,
|
|
30
|
+
baselineRel = '.ark-baseline.json'
|
|
31
|
+
) {
|
|
32
|
+
if (typeof command !== 'string' || !command.trim()) {
|
|
33
|
+
return { command, changed: false };
|
|
34
|
+
}
|
|
35
|
+
if (/^\s*#/.test(command)) {
|
|
36
|
+
return { command, changed: false };
|
|
37
|
+
}
|
|
38
|
+
if (!/\b(ark-check|arkgate-check)\b/.test(command)) {
|
|
39
|
+
return { command, changed: false };
|
|
40
|
+
}
|
|
41
|
+
if (/(?:^|\s)--baseline(?:\s|=|$)/.test(command)) {
|
|
42
|
+
return { command, changed: false };
|
|
43
|
+
}
|
|
44
|
+
const rel = baselineRel?.trim() || '.ark-baseline.json';
|
|
45
|
+
return {
|
|
46
|
+
command: `${command.trimEnd()} --baseline ${rel}`,
|
|
47
|
+
changed: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* After a baseline file is written, patch existing package.json scripts and
|
|
53
|
+
* GitHub workflow lines that already run ark-check (no full --force reinstall).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} root
|
|
56
|
+
* @param {{ baselineRel?: string }} [opts]
|
|
57
|
+
*/
|
|
58
|
+
export function syncBaselineIntoCheckSurfaces(root, opts = {}) {
|
|
59
|
+
const baselineRel =
|
|
60
|
+
typeof opts.baselineRel === 'string' && opts.baselineRel.trim()
|
|
61
|
+
? opts.baselineRel.trim().replace(/^\.\/+/, '')
|
|
62
|
+
: '.ark-baseline.json';
|
|
63
|
+
const baselinePath = path.isAbsolute(baselineRel)
|
|
64
|
+
? baselineRel
|
|
65
|
+
: path.join(root, baselineRel);
|
|
66
|
+
if (!fs.existsSync(baselinePath)) {
|
|
67
|
+
return { changed: [], skipped: ['no-baseline-file'] };
|
|
68
|
+
}
|
|
69
|
+
const flagRel = path.isAbsolute(baselineRel)
|
|
70
|
+
? path.relative(root, baselineRel).split(path.sep).join('/') || '.ark-baseline.json'
|
|
71
|
+
: baselineRel.split(path.sep).join('/');
|
|
72
|
+
const changed = [];
|
|
73
|
+
const skipped = [];
|
|
74
|
+
|
|
75
|
+
const pkgPath = path.join(root, 'package.json');
|
|
76
|
+
if (fs.existsSync(pkgPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
79
|
+
const scripts =
|
|
80
|
+
pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : null;
|
|
81
|
+
if (scripts) {
|
|
82
|
+
let scriptChanged = false;
|
|
83
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
84
|
+
if (typeof value !== 'string') continue;
|
|
85
|
+
const { command, changed: c } = ensureBaselineFlagInCheckCommand(value, flagRel);
|
|
86
|
+
if (c) {
|
|
87
|
+
scripts[name] = command;
|
|
88
|
+
scriptChanged = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (scriptChanged) {
|
|
92
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify({ ...pkg, scripts }, null, 2)}\n`);
|
|
93
|
+
changed.push({ file: 'package.json', kind: 'scripts' });
|
|
94
|
+
} else {
|
|
95
|
+
skipped.push('package.json-no-ark-check-script-or-already-baselined');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
skipped.push('package.json-unreadable');
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
skipped.push('no-package-json');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const wfDir = path.join(root, '.github', 'workflows');
|
|
106
|
+
if (fs.existsSync(wfDir)) {
|
|
107
|
+
for (const file of fs.readdirSync(wfDir)) {
|
|
108
|
+
if (!/\.ya?ml$/i.test(file)) continue;
|
|
109
|
+
const abs = path.join(wfDir, file);
|
|
110
|
+
let text;
|
|
111
|
+
try {
|
|
112
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
113
|
+
} catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!/\b(ark-check|arkgate-check)\b/.test(text)) continue;
|
|
117
|
+
const lines = text.split('\n');
|
|
118
|
+
let fileChanged = false;
|
|
119
|
+
const nextLines = lines.map((line) => {
|
|
120
|
+
if (/^\s*#/.test(line)) return line;
|
|
121
|
+
if (!/\b(ark-check|arkgate-check)\b/.test(line)) return line;
|
|
122
|
+
if (/(?:^|\s)--baseline(?:\s|=|$)/.test(line)) return line;
|
|
123
|
+
const { command, changed: c } = ensureBaselineFlagInCheckCommand(line, flagRel);
|
|
124
|
+
if (c) {
|
|
125
|
+
fileChanged = true;
|
|
126
|
+
return command;
|
|
127
|
+
}
|
|
128
|
+
return line;
|
|
129
|
+
});
|
|
130
|
+
if (fileChanged) {
|
|
131
|
+
fs.writeFileSync(abs, nextLines.join('\n'));
|
|
132
|
+
changed.push({ file: path.join('.github', 'workflows', file), kind: 'workflow' });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
skipped.push('no-workflows-dir');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { changed, skipped };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pin `arkgate` in package.json devDependencies (no package manager network call).
|
|
144
|
+
*
|
|
145
|
+
* @returns {{ changed: boolean, reason: string, version?: string }}
|
|
146
|
+
*/
|
|
147
|
+
export function pinArkgateDevDependency(root, opts = {}) {
|
|
148
|
+
const pkgPath = path.join(root, 'package.json');
|
|
149
|
+
if (!fs.existsSync(pkgPath)) {
|
|
150
|
+
return { changed: false, reason: 'no-package-json' };
|
|
151
|
+
}
|
|
152
|
+
let pkg;
|
|
153
|
+
try {
|
|
154
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
155
|
+
} catch {
|
|
156
|
+
return { changed: false, reason: 'unreadable-package-json' };
|
|
157
|
+
}
|
|
158
|
+
const deps = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
|
|
159
|
+
const dev =
|
|
160
|
+
pkg.devDependencies && typeof pkg.devDependencies === 'object'
|
|
161
|
+
? { ...pkg.devDependencies }
|
|
162
|
+
: {};
|
|
163
|
+
if (typeof deps.arkgate === 'string' || typeof dev.arkgate === 'string') {
|
|
164
|
+
return {
|
|
165
|
+
changed: false,
|
|
166
|
+
reason: 'already-present',
|
|
167
|
+
version: deps.arkgate || dev.arkgate,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const shipped = arkPackageVersion();
|
|
171
|
+
const version =
|
|
172
|
+
typeof opts.version === 'string' && opts.version
|
|
173
|
+
? opts.version
|
|
174
|
+
: shipped
|
|
175
|
+
? `^${shipped}`
|
|
176
|
+
: 'latest';
|
|
177
|
+
dev.arkgate = version;
|
|
178
|
+
if (opts.write !== false) {
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
pkgPath,
|
|
181
|
+
`${JSON.stringify({ ...pkg, devDependencies: dev }, null, 2)}\n`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return { changed: true, reason: 'added', version };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Product-shaped I/O directory names under Application globs (not bare `db`/`infra`).
|
|
189
|
+
*/
|
|
190
|
+
export const IO_DIR_SEGMENTS = [
|
|
191
|
+
'airtable',
|
|
192
|
+
'supabase',
|
|
193
|
+
'prisma',
|
|
194
|
+
'drizzle',
|
|
195
|
+
'typeorm',
|
|
196
|
+
'sequelize',
|
|
197
|
+
'mongoose',
|
|
198
|
+
'knex',
|
|
199
|
+
'kysely',
|
|
200
|
+
'firebase',
|
|
201
|
+
'firestore',
|
|
202
|
+
'mongodb',
|
|
203
|
+
'persistence',
|
|
204
|
+
'repositories',
|
|
205
|
+
'repository',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const IO_SEGMENT_SET = new Set(IO_DIR_SEGMENTS);
|
|
209
|
+
|
|
210
|
+
/** Glob pattern → walk root directory (strip trailing wildcards). */
|
|
211
|
+
function walkBaseFromGlob(pattern) {
|
|
212
|
+
if (typeof pattern !== 'string') return null;
|
|
213
|
+
const base = pattern
|
|
214
|
+
.replace(/\/\*\*$/, '')
|
|
215
|
+
.replace(/\/\*$/, '')
|
|
216
|
+
.replace(/\*\*$/, '')
|
|
217
|
+
.replace(/\*$/, '');
|
|
218
|
+
if (!base || base.includes('*')) return null;
|
|
219
|
+
return base;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Collect rel paths for IO segments under absBase (depth 0–1), prefixed with baseRel. */
|
|
223
|
+
function collectIoDirs(absBase, baseRel, seen, out) {
|
|
224
|
+
let entries;
|
|
225
|
+
try {
|
|
226
|
+
entries = fs.readdirSync(absBase, { withFileTypes: true });
|
|
227
|
+
} catch {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (!entry.isDirectory()) continue;
|
|
232
|
+
const seg = entry.name.toLowerCase();
|
|
233
|
+
if (IO_SEGMENT_SET.has(seg)) {
|
|
234
|
+
const rel = path.join(baseRel, entry.name).split(path.sep).join('/');
|
|
235
|
+
if (!seen.has(rel)) {
|
|
236
|
+
seen.add(rel);
|
|
237
|
+
out.push(rel);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// one level deeper (e.g. lib/server/prisma)
|
|
241
|
+
try {
|
|
242
|
+
for (const child of fs.readdirSync(path.join(absBase, entry.name), {
|
|
243
|
+
withFileTypes: true,
|
|
244
|
+
})) {
|
|
245
|
+
if (!child.isDirectory()) continue;
|
|
246
|
+
if (!IO_SEGMENT_SET.has(child.name.toLowerCase())) continue;
|
|
247
|
+
const rel = path
|
|
248
|
+
.join(baseRel, entry.name, child.name)
|
|
249
|
+
.split(path.sep)
|
|
250
|
+
.join('/');
|
|
251
|
+
if (!seen.has(rel)) {
|
|
252
|
+
seen.add(rel);
|
|
253
|
+
out.push(rel);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch {
|
|
257
|
+
/* ignore */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Detect false-green: empty Domain/Persistence while Application globs still
|
|
264
|
+
* cover obvious I/O directories.
|
|
265
|
+
*
|
|
266
|
+
* @returns {null | { risk: true, ioPaths: string[], emptyCores: string[], message: string, fix: string }}
|
|
267
|
+
*/
|
|
268
|
+
export function detectContractFalseGreenRisk(root, config, coverage) {
|
|
269
|
+
if (!config || !Array.isArray(config.layers) || config.layers.length === 0) return null;
|
|
270
|
+
const emptyLayers = new Set(
|
|
271
|
+
Array.isArray(coverage?.emptyLayers) ? coverage.emptyLayers : []
|
|
272
|
+
);
|
|
273
|
+
if (Array.isArray(coverage?.layers)) {
|
|
274
|
+
for (const row of coverage.layers) {
|
|
275
|
+
if (row && typeof row.name === 'string' && (row.files ?? 0) === 0) {
|
|
276
|
+
emptyLayers.add(row.name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const emptyCores = [...emptyLayers].filter(
|
|
281
|
+
(name) =>
|
|
282
|
+
name === 'DomainModel' ||
|
|
283
|
+
name === 'PersistenceAdapters' ||
|
|
284
|
+
/^Domain/i.test(name) ||
|
|
285
|
+
/Persist|Infra|DataAccess/i.test(name)
|
|
286
|
+
);
|
|
287
|
+
if (emptyCores.length === 0) return null;
|
|
288
|
+
|
|
289
|
+
const appLayers = (config.layers || []).filter((layer) =>
|
|
290
|
+
/application|orchestr/i.test(layer?.name ?? '')
|
|
291
|
+
);
|
|
292
|
+
if (appLayers.length === 0) return null;
|
|
293
|
+
|
|
294
|
+
const ioPaths = [];
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const layer of appLayers) {
|
|
297
|
+
for (const pattern of layer.patterns || []) {
|
|
298
|
+
const base = walkBaseFromGlob(pattern);
|
|
299
|
+
if (!base) continue;
|
|
300
|
+
const absBase = path.join(root, base);
|
|
301
|
+
if (!fs.existsSync(absBase) || !fs.statSync(absBase).isDirectory()) continue;
|
|
302
|
+
collectIoDirs(absBase, base, seen, ioPaths);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (ioPaths.length === 0) return null;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
risk: true,
|
|
309
|
+
ioPaths,
|
|
310
|
+
emptyCores,
|
|
311
|
+
message:
|
|
312
|
+
`Contract may be a false green: empty core layer(s) [${emptyCores.join(', ')}] while ` +
|
|
313
|
+
`Application-class globs still cover I/O paths (${ioPaths.slice(0, 5).join(', ')}` +
|
|
314
|
+
`${ioPaths.length > 5 ? ', …' : ''}). A clean check can miss real coupling.`,
|
|
315
|
+
fix: 'Run /ark-adopt or /ark-contract — reclassify persistence/auth dirs out of Application before claiming ENFORCE.',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Stable adoption-gap id for false-green (doctor + start wrap-up). */
|
|
320
|
+
export const FALSE_GREEN_GAP_ID = 'contract-false-green-io-under-application';
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Build the adoption gap object (or null) for collectAdoptionGaps.
|
|
324
|
+
*/
|
|
325
|
+
export function falseGreenAdoptionGap(root, config, coverage) {
|
|
326
|
+
const risk = detectContractFalseGreenRisk(root, config, coverage);
|
|
327
|
+
if (!risk?.risk) return null;
|
|
328
|
+
return {
|
|
329
|
+
id: FALSE_GREEN_GAP_ID,
|
|
330
|
+
severity: 'warn',
|
|
331
|
+
message: risk.message,
|
|
332
|
+
fix: risk.fix,
|
|
333
|
+
};
|
|
334
|
+
}
|