arkgate 3.0.5 → 3.2.0
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 +92 -1
- package/README.md +58 -21
- package/bin/ark-check.mjs +46 -4
- package/bin/ark-mcp.mjs +267 -26
- package/bin/ark.mjs +47 -0
- package/bin/lib/adapter-contract.mjs +27 -1
- package/bin/lib/analysis-engine.mjs +7 -1169
- package/bin/lib/ci-and-commands.mjs +4 -0
- package/bin/lib/contract-smells.mjs +514 -0
- package/bin/lib/doctor-plan.mjs +15 -4
- package/bin/lib/host-support-matrix.mjs +6 -2
- package/bin/lib/policy-delta-io.mjs +161 -0
- package/bin/lib/prepare-change.mjs +186 -0
- package/bin/lib/remediation.mjs +24 -0
- package/bin/lib/violations.mjs +2 -2
- package/bin/lib/write-path-capabilities.mjs +67 -1
- package/bin/lib/write-path-detect.mjs +4 -3
- package/dist/eslint/index.cjs +3 -977
- package/dist/eslint/index.js +3 -931
- package/dist/index.cjs +6 -1960
- package/dist/index.d.cts +152 -5
- package/dist/index.d.ts +152 -5
- package/dist/index.js +6 -1908
- package/docs/agent-guide.md +39 -5
- package/docs/ai-gates.md +17 -15
- package/docs/configuration.md +44 -0
- package/docs/demos/01-write-gate-self-correction.md +2 -2
- package/docs/enthusiast/README.md +5 -1
- package/docs/enthusiast/how-to-agent-gates.md +3 -5
- package/docs/enthusiast/how-to-policy-pack.md +4 -1
- package/docs/enthusiast/reference-archetypes.md +8 -1
- package/docs/enthusiast/reference-commands.md +8 -2
- package/docs/package-surface.md +12 -2
- package/docs/threat-model.md +10 -6
- package/package.json +7 -6
- package/schemas/ark.analysis-result.schema.json +5 -1
- package/schemas/ark.change-map.schema.json +77 -0
- package/server.json +3 -3
- package/docs/ark-check-example.json +0 -87
- package/docs/demos/03-copilot-autopilot.md +0 -93
- package/docs/migrate-from-ark-runtime-kernel.md +0 -174
- package/docs/production-hardening.md +0 -100
package/bin/ark-mcp.mjs
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* discarded if post-patch still invalid.
|
|
19
19
|
* - tool ark_prepare_write — W2: place + constrain + validate + autoPatch + judgmentBrief
|
|
20
20
|
* + contentHash (composes ark_place + write gate; not a second contract).
|
|
21
|
+
* - tool ark_prepare_change — atomically preflights a create/update/delete batch without writes.
|
|
22
|
+
* - tool ark_policy_delta — classifies a base/candidate ark.config.json transition and
|
|
23
|
+
* rejects weakening without an exact hash-bound acknowledgement.
|
|
21
24
|
* - tool ark_recommend — deterministic application-shape plan (same as
|
|
22
25
|
* ark-check --recommend --json)
|
|
23
26
|
*
|
|
@@ -61,6 +64,8 @@ import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
|
61
64
|
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
62
65
|
import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
|
|
63
66
|
import { loadGoldenPattern, attachGoldenToPlacement } from './lib/golden-pattern.mjs';
|
|
67
|
+
import { prepareChangeFromRoot } from './lib/prepare-change.mjs';
|
|
68
|
+
import { detectWritePathCapabilities } from './lib/write-path-detect.mjs';
|
|
64
69
|
|
|
65
70
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
66
71
|
|
|
@@ -201,17 +206,23 @@ function applyCodexUpdatePatch(current, lines) {
|
|
|
201
206
|
let source = current.split('\n');
|
|
202
207
|
let cursor = 0;
|
|
203
208
|
const hunks = [];
|
|
204
|
-
let hunk =
|
|
209
|
+
let hunk = null;
|
|
205
210
|
for (const line of lines) {
|
|
206
211
|
if (line.startsWith('@@')) {
|
|
207
|
-
if (hunk
|
|
208
|
-
hunk = [];
|
|
212
|
+
if (hunk) hunks.push(hunk);
|
|
213
|
+
hunk = { anchor: line.slice(2).trim(), entries: [] };
|
|
209
214
|
} else if (/^[ +\-]/.test(line)) {
|
|
210
|
-
hunk
|
|
215
|
+
if (!hunk) return null;
|
|
216
|
+
hunk.entries.push(line);
|
|
211
217
|
}
|
|
212
218
|
}
|
|
213
|
-
if (hunk
|
|
214
|
-
for (const entries of hunks) {
|
|
219
|
+
if (hunk) hunks.push(hunk);
|
|
220
|
+
for (const { anchor, entries } of hunks) {
|
|
221
|
+
if (anchor) {
|
|
222
|
+
const anchorAt = source.findIndex((line, index) => index >= cursor && line === anchor);
|
|
223
|
+
if (anchorAt < 0) return null;
|
|
224
|
+
cursor = anchorAt + 1;
|
|
225
|
+
}
|
|
215
226
|
const oldLines = entries.filter((line) => !line.startsWith('+')).map((line) => line.slice(1));
|
|
216
227
|
const newLines = entries.filter((line) => !line.startsWith('-')).map((line) => line.slice(1));
|
|
217
228
|
let found = -1;
|
|
@@ -229,36 +240,94 @@ function applyCodexUpdatePatch(current, lines) {
|
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
function codexPatchWrites(patch, root) {
|
|
232
|
-
if (typeof patch !== 'string'
|
|
243
|
+
if (typeof patch !== 'string') {
|
|
244
|
+
return { writes: [], complete: false };
|
|
245
|
+
}
|
|
233
246
|
const lines = patch.split('\n');
|
|
247
|
+
const begin = lines.indexOf('*** Begin Patch');
|
|
248
|
+
const end = lines.indexOf('*** End Patch', begin + 1);
|
|
249
|
+
if (begin < 0 || end <= begin) return { writes: [], complete: false };
|
|
234
250
|
const writes = [];
|
|
235
|
-
|
|
251
|
+
const seenPaths = new Set();
|
|
252
|
+
let complete = [
|
|
253
|
+
...lines.slice(0, begin),
|
|
254
|
+
...lines.slice(end + 1),
|
|
255
|
+
].every((line) => line.trim() === '');
|
|
256
|
+
let sawFileDirective = false;
|
|
257
|
+
for (let index = begin + 1; index < end; index += 1) {
|
|
236
258
|
const match = lines[index].match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
|
|
237
|
-
if (!match)
|
|
259
|
+
if (!match) {
|
|
260
|
+
if (lines[index].trim() !== '') complete = false;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
sawFileDirective = true;
|
|
238
264
|
const [, action, relativePath] = match;
|
|
239
265
|
const body = [];
|
|
240
|
-
for (index += 1; index <
|
|
266
|
+
for (index += 1; index < end && !lines[index].startsWith('*** '); index += 1) {
|
|
241
267
|
body.push(lines[index]);
|
|
242
268
|
}
|
|
243
269
|
index -= 1;
|
|
244
|
-
if (action === 'Delete') continue;
|
|
245
270
|
const filePath = path.resolve(root, relativePath);
|
|
271
|
+
const rel = path.relative(root, filePath);
|
|
272
|
+
if (
|
|
273
|
+
seenPaths.has(filePath) ||
|
|
274
|
+
rel.startsWith(`..${path.sep}`) ||
|
|
275
|
+
rel === '..' ||
|
|
276
|
+
path.isAbsolute(rel)
|
|
277
|
+
) {
|
|
278
|
+
complete = false;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
seenPaths.add(filePath);
|
|
282
|
+
if (action === 'Delete') {
|
|
283
|
+
if (body.some((line) => line.trim() !== '') || !fs.existsSync(filePath)) {
|
|
284
|
+
complete = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
writes.push({ path: relativePath, delete: true });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
246
290
|
let content;
|
|
247
291
|
if (action === 'Add') {
|
|
292
|
+
if (
|
|
293
|
+
body.length === 0 ||
|
|
294
|
+
fs.existsSync(filePath) ||
|
|
295
|
+
body.some((line) => !line.startsWith('+'))
|
|
296
|
+
) {
|
|
297
|
+
complete = false;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
248
300
|
content = body.filter((line) => line.startsWith('+')).map((line) => line.slice(1)).join('\n');
|
|
249
301
|
if (body.some((line) => line.startsWith('+'))) content += '\n';
|
|
250
302
|
} else {
|
|
303
|
+
if (
|
|
304
|
+
!body.some((line) => line.startsWith('@@')) ||
|
|
305
|
+
body.some((line) => !line.startsWith('@@') && !/^[ +\-]/.test(line))
|
|
306
|
+
) {
|
|
307
|
+
complete = false;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
251
310
|
let current;
|
|
252
311
|
try {
|
|
253
312
|
current = fs.readFileSync(filePath, 'utf8');
|
|
254
313
|
} catch {
|
|
314
|
+
complete = false;
|
|
255
315
|
continue;
|
|
256
316
|
}
|
|
257
317
|
content = applyCodexUpdatePatch(current, body);
|
|
318
|
+
if (content === null) complete = false;
|
|
258
319
|
}
|
|
259
|
-
if (typeof content === 'string') writes.push({ filePath, content });
|
|
320
|
+
if (typeof content === 'string') writes.push({ path: relativePath, filePath, content });
|
|
260
321
|
}
|
|
261
|
-
return writes;
|
|
322
|
+
return { writes, complete: complete && sawFileDirective };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hookEnforcement(root, host, operation, completePatch = false) {
|
|
326
|
+
return detectWritePathCapabilities(root, host, {
|
|
327
|
+
boundary: 'pre-tool',
|
|
328
|
+
operation,
|
|
329
|
+
completePatch,
|
|
330
|
+
}).enforcementLadder;
|
|
262
331
|
}
|
|
263
332
|
|
|
264
333
|
/**
|
|
@@ -308,22 +377,75 @@ function runHook(gate, config, args, ts) {
|
|
|
308
377
|
runHookPayload(payload, gate, config, args, ts);
|
|
309
378
|
}
|
|
310
379
|
|
|
311
|
-
function runHookPayload(payload, gate, config, args, ts) {
|
|
380
|
+
function runHookPayload(payload, gate, config, args, ts, attemptContext) {
|
|
312
381
|
const { toolName, toolInput, grokStyle } = normalizeHookPayload(payload);
|
|
313
382
|
if (toolName === 'ApplyPatch') {
|
|
314
383
|
const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
384
|
+
const parsedPatch = codexPatchWrites(patch, args.root);
|
|
385
|
+
// Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
|
|
386
|
+
// An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
|
|
387
|
+
if (!parsedPatch.complete) return;
|
|
388
|
+
const patchWrites = parsedPatch.writes;
|
|
389
|
+
const governedWrites = patchWrites.filter((change) => {
|
|
390
|
+
const relative = String(change.path).replace(/\\/g, '/');
|
|
391
|
+
if (!SOURCE_FILE.test(relative) || relative.endsWith('.d.ts')) return false;
|
|
392
|
+
return Boolean(inferLayer(path.resolve(args.root, relative), config, args.root));
|
|
393
|
+
});
|
|
394
|
+
const changes = governedWrites.map(({ path: relativePath, content, delete: deleted }) =>
|
|
395
|
+
deleted ? { path: relativePath, delete: true } : { path: relativePath, content }
|
|
396
|
+
);
|
|
397
|
+
if (changes.length === 0) return;
|
|
398
|
+
let result;
|
|
399
|
+
try {
|
|
400
|
+
result = prepareChangeFromRoot({
|
|
401
|
+
root: args.root,
|
|
322
402
|
config,
|
|
323
|
-
args
|
|
324
|
-
|
|
403
|
+
configSource: path.isAbsolute(args.config)
|
|
404
|
+
? args.config
|
|
405
|
+
: path.join(args.root, args.config),
|
|
406
|
+
changes,
|
|
407
|
+
});
|
|
408
|
+
} catch {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (result.valid) {
|
|
412
|
+
for (const write of governedWrites) {
|
|
413
|
+
if (typeof write.content !== 'string') continue;
|
|
414
|
+
runHookPayload(
|
|
415
|
+
{
|
|
416
|
+
tool_name: 'Write',
|
|
417
|
+
tool_input: { file_path: write.filePath, content: write.content },
|
|
418
|
+
},
|
|
419
|
+
gate,
|
|
420
|
+
config,
|
|
421
|
+
args,
|
|
422
|
+
ts,
|
|
423
|
+
{ host: 'codex', operation: 'apply_patch', completePatch: true }
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const message = [
|
|
429
|
+
`Ark architecture gate blocked this complete ${toolName} (${changes.length} governed file(s)):`,
|
|
430
|
+
...result.diagnostics.map(
|
|
431
|
+
(diagnostic) =>
|
|
432
|
+
`- [${diagnostic.ruleId}] ${diagnostic.message}\n Next action: ${diagnostic.nextAction}`
|
|
433
|
+
),
|
|
434
|
+
'No project file was written. Fix the complete patch and retry.',
|
|
435
|
+
].join('\n');
|
|
436
|
+
process.stderr.write(`${message}\n`);
|
|
437
|
+
if (args.hookRepair) {
|
|
438
|
+
process.stderr.write(
|
|
439
|
+
`ARK_REPAIR_JSON:${JSON.stringify({
|
|
440
|
+
...result,
|
|
441
|
+
mode: 'repair',
|
|
442
|
+
decision: 'deny',
|
|
443
|
+
enforcement: hookEnforcement(args.root, 'codex', 'apply_patch', true),
|
|
444
|
+
autoPatch: null,
|
|
445
|
+
})}\n`
|
|
325
446
|
);
|
|
326
447
|
}
|
|
448
|
+
process.exitCode = 2;
|
|
327
449
|
return;
|
|
328
450
|
}
|
|
329
451
|
const filePath = toolInput.file_path;
|
|
@@ -392,9 +514,9 @@ function runHookPayload(payload, gate, config, args, ts) {
|
|
|
392
514
|
violations: newViolations.map((violation) => ({ ...violation, file: normalizedRel })),
|
|
393
515
|
});
|
|
394
516
|
|
|
395
|
-
const lines =
|
|
396
|
-
(
|
|
397
|
-
`- [${
|
|
517
|
+
const lines = adapterResult.diagnostics.map(
|
|
518
|
+
(diagnostic) =>
|
|
519
|
+
`- [${diagnostic.ruleId}] ${diagnostic.message}${diagnostic.location.line ? ` (line ${diagnostic.location.line})` : ''}\n Next action: ${diagnostic.nextAction}`
|
|
398
520
|
);
|
|
399
521
|
// Surface the per-violation fix hints (the gate carries them in `suggestion`,
|
|
400
522
|
// but the hook was dropping them). Dedupe so two infra violations sharing one
|
|
@@ -436,6 +558,13 @@ function runHookPayload(payload, gate, config, args, ts) {
|
|
|
436
558
|
mode: 'repair',
|
|
437
559
|
decision: 'deny',
|
|
438
560
|
filePath: normalizedRel,
|
|
561
|
+
enforcement: hookEnforcement(
|
|
562
|
+
args.root,
|
|
563
|
+
attemptContext?.host ?? (grokStyle ? 'grok' : 'claude'),
|
|
564
|
+
attemptContext?.operation ??
|
|
565
|
+
(grokStyle ? (toolName === 'Edit' ? 'search_replace' : 'write') : toolName),
|
|
566
|
+
Boolean(attemptContext?.completePatch)
|
|
567
|
+
),
|
|
439
568
|
...(layer ? { layer } : {}),
|
|
440
569
|
...(autoPatch
|
|
441
570
|
? {
|
|
@@ -736,6 +865,34 @@ async function main() {
|
|
|
736
865
|
},
|
|
737
866
|
outputSchema: ARK_ANALYSIS_RESULT_SCHEMA,
|
|
738
867
|
},
|
|
868
|
+
{
|
|
869
|
+
name: 'ark_policy_delta',
|
|
870
|
+
description:
|
|
871
|
+
'Classify a complete ark.config.json transition as strengthening, neutral, ' +
|
|
872
|
+
'judgment-required, or weakening. Pass the previous baseConfig and optional ' +
|
|
873
|
+
'candidateConfig (defaults to this project contract). Weakening and judgment-required ' +
|
|
874
|
+
'results set isError unless acknowledgement exactly matches both policy hashes and all ' +
|
|
875
|
+
'blocking finding ids. Read-only; never edits the contract.',
|
|
876
|
+
inputSchema: {
|
|
877
|
+
type: 'object',
|
|
878
|
+
properties: {
|
|
879
|
+
baseConfig: {
|
|
880
|
+
type: 'object',
|
|
881
|
+
description: 'Previous complete ark.config.json object.',
|
|
882
|
+
},
|
|
883
|
+
candidateConfig: {
|
|
884
|
+
type: 'object',
|
|
885
|
+
description: 'Candidate complete config; defaults to the current project contract.',
|
|
886
|
+
},
|
|
887
|
+
acknowledgement: {
|
|
888
|
+
type: 'object',
|
|
889
|
+
description:
|
|
890
|
+
'Optional schemaVersion/basePolicyHash/candidatePolicyHash/findingIds/reason object.',
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
required: ['baseConfig'],
|
|
894
|
+
},
|
|
895
|
+
},
|
|
739
896
|
{
|
|
740
897
|
name: 'ark_coverage',
|
|
741
898
|
description:
|
|
@@ -797,6 +954,38 @@ async function main() {
|
|
|
797
954
|
required: ['source'],
|
|
798
955
|
},
|
|
799
956
|
},
|
|
957
|
+
{
|
|
958
|
+
name: 'ark_prepare_change',
|
|
959
|
+
description:
|
|
960
|
+
'Validate one complete governed-source create/update/delete batch as an atomic in-memory candidate. ' +
|
|
961
|
+
'Catches cross-file forbidden edges and cycles before any host write, and returns ' +
|
|
962
|
+
'per-file content hashes plus base/candidate tree and policy hashes. Never writes files.',
|
|
963
|
+
inputSchema: {
|
|
964
|
+
type: 'object',
|
|
965
|
+
properties: {
|
|
966
|
+
changes: {
|
|
967
|
+
type: 'array',
|
|
968
|
+
description:
|
|
969
|
+
'Full candidate batch. Each item is {path, content} for create/update or {path, delete:true}.',
|
|
970
|
+
items: {
|
|
971
|
+
type: 'object',
|
|
972
|
+
properties: {
|
|
973
|
+
path: { type: 'string' },
|
|
974
|
+
content: { type: 'string' },
|
|
975
|
+
delete: { type: 'boolean' },
|
|
976
|
+
},
|
|
977
|
+
required: ['path'],
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
changeMap: {
|
|
981
|
+
type: 'object',
|
|
982
|
+
description:
|
|
983
|
+
'Optional strict schema 1.0 architecture change map. Omit it to use ordinary atomic preflight.',
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
required: ['changes'],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
800
989
|
{
|
|
801
990
|
name: 'ark_recommend',
|
|
802
991
|
description:
|
|
@@ -993,6 +1182,33 @@ async function main() {
|
|
|
993
1182
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], isError: false };
|
|
994
1183
|
}
|
|
995
1184
|
|
|
1185
|
+
function runPolicyDeltaTool(params) {
|
|
1186
|
+
const baseConfig = params?.arguments?.baseConfig;
|
|
1187
|
+
if (!baseConfig || typeof baseConfig !== 'object' || Array.isArray(baseConfig)) {
|
|
1188
|
+
return {
|
|
1189
|
+
content: [{ type: 'text', text: 'ark_policy_delta requires baseConfig (object).' }],
|
|
1190
|
+
isError: true,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
const result = ark.analyzePolicyDelta({
|
|
1195
|
+
baseConfig,
|
|
1196
|
+
candidateConfig: params?.arguments?.candidateConfig ?? config,
|
|
1197
|
+
acknowledgement: params?.arguments?.acknowledgement,
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1201
|
+
structuredContent: result,
|
|
1202
|
+
isError: !result.valid,
|
|
1203
|
+
};
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
return {
|
|
1206
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
1207
|
+
isError: true,
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
996
1212
|
function runRecommendTool() {
|
|
997
1213
|
const { data, raw } = runArkCheckJson(['--recommend']);
|
|
998
1214
|
if (!data) {
|
|
@@ -1153,6 +1369,29 @@ async function main() {
|
|
|
1153
1369
|
};
|
|
1154
1370
|
}
|
|
1155
1371
|
|
|
1372
|
+
function runPrepareChange(params) {
|
|
1373
|
+
try {
|
|
1374
|
+
const result = prepareChangeFromRoot({
|
|
1375
|
+
root: args.root,
|
|
1376
|
+
config,
|
|
1377
|
+
configSource: configPath,
|
|
1378
|
+
changes: params?.arguments?.changes,
|
|
1379
|
+
changeMap: params?.arguments?.changeMap,
|
|
1380
|
+
changeMapSource: 'ark_prepare_change.changeMap',
|
|
1381
|
+
});
|
|
1382
|
+
return {
|
|
1383
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1384
|
+
structuredContent: result,
|
|
1385
|
+
isError: !result.valid,
|
|
1386
|
+
};
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
return {
|
|
1389
|
+
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
|
|
1390
|
+
isError: true,
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1156
1395
|
function runSuggestIncludeTool() {
|
|
1157
1396
|
try {
|
|
1158
1397
|
const workspaces = detectWorkspaces(args.root);
|
|
@@ -1193,9 +1432,11 @@ async function main() {
|
|
|
1193
1432
|
const TOOL_HANDLERS = {
|
|
1194
1433
|
validate_code: runValidate,
|
|
1195
1434
|
ark_check: runCheckTool,
|
|
1435
|
+
ark_policy_delta: runPolicyDeltaTool,
|
|
1196
1436
|
ark_coverage: runCoverageTool,
|
|
1197
1437
|
ark_place: runPlace,
|
|
1198
1438
|
ark_prepare_write: runPrepareWrite,
|
|
1439
|
+
ark_prepare_change: runPrepareChange,
|
|
1199
1440
|
ark_recommend: runRecommendTool,
|
|
1200
1441
|
ark_suggest_include: runSuggestIncludeTool,
|
|
1201
1442
|
};
|
package/bin/ark.mjs
CHANGED
|
@@ -22,6 +22,13 @@ import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install
|
|
|
22
22
|
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
23
23
|
import { applyStartPreview, planStart, renderStartPreview } from './lib/start-preview.mjs';
|
|
24
24
|
import { detectActiveAgentHost } from './lib/skill-install.mjs';
|
|
25
|
+
import { loadArkConfigContract } from './lib/config-contract.mjs';
|
|
26
|
+
import {
|
|
27
|
+
prepareChangeFromRoot,
|
|
28
|
+
readChangeMapFile,
|
|
29
|
+
readChangeSetFile,
|
|
30
|
+
renderChangePreflight,
|
|
31
|
+
} from './lib/prepare-change.mjs';
|
|
25
32
|
|
|
26
33
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
27
34
|
const arkCheck = path.join(here, 'ark-check.mjs');
|
|
@@ -56,6 +63,7 @@ function parseArgs(argv) {
|
|
|
56
63
|
const args = {
|
|
57
64
|
command: undefined,
|
|
58
65
|
root: process.cwd(),
|
|
66
|
+
config: 'ark.config.json',
|
|
59
67
|
yes: false,
|
|
60
68
|
force: false,
|
|
61
69
|
strict: true,
|
|
@@ -101,6 +109,9 @@ function parseArgs(argv) {
|
|
|
101
109
|
else if (arg === '--skip-package-manager') args.skipPackageManager = true;
|
|
102
110
|
else if (arg === '--remove-host') args.removeHost = requireValue(arg, i++).trim().toLowerCase();
|
|
103
111
|
else if (arg === '--preset') args.preset = requireValue(arg, i++);
|
|
112
|
+
else if (arg === '--config') args.config = requireValue(arg, i++);
|
|
113
|
+
else if (arg === '--changes') args.changes = requireValue(arg, i++);
|
|
114
|
+
else if (arg === '--change-map') args.changeMap = requireValue(arg, i++);
|
|
104
115
|
else if (arg === '--archetype') args.archetype = requireValue(arg, i++);
|
|
105
116
|
else if (arg === '--tools') args.tools = requireValue(arg, i++);
|
|
106
117
|
else if (arg === '--require-write-hook') {
|
|
@@ -121,6 +132,7 @@ function usage() {
|
|
|
121
132
|
ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture]
|
|
122
133
|
[--archetype <playbook-id>] [--tools <list>] [--require-write-hook <host>] [--yes] [--force] [--no-strict]
|
|
123
134
|
ark upgrade [--root <project>] [--no-install] [--no-strict]
|
|
135
|
+
ark preflight --changes <change-set.json> [--change-map <map.json>] [--root <project>] [--config ark.config.json] [--json]
|
|
124
136
|
|
|
125
137
|
Commands:
|
|
126
138
|
start New here? Analyze and preview the complete setup. Read-only unless --apply.
|
|
@@ -128,6 +140,7 @@ Commands:
|
|
|
128
140
|
upgrade One command to update Ark: bump the package to @latest, refresh gate
|
|
129
141
|
templates + /ark-* skills (and Codex home prompts), migrate command
|
|
130
142
|
runners to this project's package manager, then run the strict check.
|
|
143
|
+
preflight Validate one atomic create/update/delete set without writing project files.
|
|
131
144
|
(alias: ark update)
|
|
132
145
|
|
|
133
146
|
Options:
|
|
@@ -775,6 +788,13 @@ async function main() {
|
|
|
775
788
|
console.error('--require-write-hook is supported by ark start and ark init.');
|
|
776
789
|
return 2;
|
|
777
790
|
}
|
|
791
|
+
if (
|
|
792
|
+
args.command !== 'preflight' &&
|
|
793
|
+
(args.changes || args.changeMap || args.config !== 'ark.config.json')
|
|
794
|
+
) {
|
|
795
|
+
console.error('--changes, --change-map, and --config are supported by ark preflight.');
|
|
796
|
+
return 2;
|
|
797
|
+
}
|
|
778
798
|
const enforcement = validateHardWriteRequest({
|
|
779
799
|
root: args.root,
|
|
780
800
|
host: args.requireWriteHook,
|
|
@@ -817,6 +837,33 @@ async function main() {
|
|
|
817
837
|
}
|
|
818
838
|
}
|
|
819
839
|
|
|
840
|
+
if (args.command === 'preflight') {
|
|
841
|
+
try {
|
|
842
|
+
if (!args.changes) throw new Error('ark preflight requires --changes <change-set.json>.');
|
|
843
|
+
const configPath = path.isAbsolute(args.config)
|
|
844
|
+
? args.config
|
|
845
|
+
: path.join(args.root, args.config);
|
|
846
|
+
const config = loadArkConfigContract(
|
|
847
|
+
JSON.parse(fs.readFileSync(configPath, 'utf8')),
|
|
848
|
+
configPath
|
|
849
|
+
).config;
|
|
850
|
+
const changeMap = args.changeMap ? readChangeMapFile(args.root, args.changeMap) : undefined;
|
|
851
|
+
const result = prepareChangeFromRoot({
|
|
852
|
+
root: args.root,
|
|
853
|
+
config,
|
|
854
|
+
configSource: configPath,
|
|
855
|
+
changes: readChangeSetFile(args.root, args.changes),
|
|
856
|
+
...(changeMap ? { changeMap: changeMap.input, changeMapSource: changeMap.source } : {}),
|
|
857
|
+
});
|
|
858
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
859
|
+
else renderChangePreflight(result);
|
|
860
|
+
return result.valid ? 0 : 1;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
863
|
+
return 2;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
820
867
|
console.error(`Unknown command: ${args.command}`);
|
|
821
868
|
console.error(usage());
|
|
822
869
|
return 2;
|
|
@@ -8,13 +8,37 @@
|
|
|
8
8
|
* Pure CLI helper (bin/lib/adapter-contract.mjs). Zero Node I/O.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export const ARK_ANALYSIS_RESULT_SCHEMA_VERSION = '1.
|
|
11
|
+
export const ARK_ANALYSIS_RESULT_SCHEMA_VERSION = '1.1';
|
|
12
12
|
function text(value) {
|
|
13
13
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
14
14
|
}
|
|
15
15
|
function positiveInteger(value, fallback) {
|
|
16
16
|
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
17
17
|
}
|
|
18
|
+
function nextActionForDiagnostic(ruleId, evidence, violation) {
|
|
19
|
+
if (ruleId === 'LAYER_IMPORT_VIOLATION') {
|
|
20
|
+
if (evidence.typeOnly ||
|
|
21
|
+
violation.targetTypeOnlyExports === true ||
|
|
22
|
+
violation.namedBindingsTypeOnly === true) {
|
|
23
|
+
return 'Move the referenced type to a mutually allowed layer, use `import type`, then preflight again.';
|
|
24
|
+
}
|
|
25
|
+
if (violation.peerIsolation === true) {
|
|
26
|
+
return 'Extract the shared dependency to a shared layer, then preflight again.';
|
|
27
|
+
}
|
|
28
|
+
return `Define a port in ${evidence.fromLayer ?? 'the source layer'}, inject the ${evidence.toLayer ?? 'outer-layer'} implementation, then preflight again.`;
|
|
29
|
+
}
|
|
30
|
+
if (ruleId === 'FORBIDDEN_GLOBAL') {
|
|
31
|
+
return `Inject ${evidence.target ?? 'the capability'} through a port, then preflight again.`;
|
|
32
|
+
}
|
|
33
|
+
if (ruleId === 'CIRCULAR_DEPENDENCY') {
|
|
34
|
+
return 'Extract the shared dependency into a third module, then preflight again.';
|
|
35
|
+
}
|
|
36
|
+
if (ruleId === 'RAW_EVENT_PUBLISH')
|
|
37
|
+
return 'Publish through a registered intent creator, then run Ark again.';
|
|
38
|
+
if (ruleId === 'PUBLISH_MISSING_SOURCE')
|
|
39
|
+
return 'Add metadata.source to the publish call, then run Ark again.';
|
|
40
|
+
return `Resolve ${ruleId} without weakening ark.config.json, then run Ark again.`;
|
|
41
|
+
}
|
|
18
42
|
export function toAdapterDiagnostic(violation, fallbackSeverity = 'error') {
|
|
19
43
|
const ruleId = text(violation.ruleId) ?? text(violation.code) ?? 'ARK_UNKNOWN';
|
|
20
44
|
const severity = violation.severity === 'warning' ? 'warning' : fallbackSeverity;
|
|
@@ -34,6 +58,7 @@ export function toAdapterDiagnostic(violation, fallbackSeverity = 'error') {
|
|
|
34
58
|
column: positiveInteger(violation.column, 1),
|
|
35
59
|
},
|
|
36
60
|
evidence,
|
|
61
|
+
nextAction: text(violation.nextAction) ?? nextActionForDiagnostic(ruleId, evidence, violation),
|
|
37
62
|
};
|
|
38
63
|
}
|
|
39
64
|
export function createAdapterResult(input) {
|
|
@@ -86,6 +111,7 @@ export const ARK_ANALYSIS_RESULT_SCHEMA = {
|
|
|
86
111
|
typeOnly: { type: 'boolean' },
|
|
87
112
|
},
|
|
88
113
|
},
|
|
114
|
+
nextAction: { type: 'string', minLength: 1 },
|
|
89
115
|
},
|
|
90
116
|
},
|
|
91
117
|
},
|