arkgate 2.9.2 → 2.11.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 +87 -0
- package/README.md +12 -2
- package/SECURITY.md +3 -4
- package/bin/ark-check.mjs +41 -16
- package/bin/ark-mcp.mjs +335 -111
- package/bin/ark.mjs +35 -5
- package/bin/lib/agent-gates.mjs +161 -8
- package/bin/lib/architecture-scan.mjs +23 -1
- package/bin/lib/auto-patch.mjs +264 -0
- package/bin/lib/baseline-key.mjs +17 -0
- package/bin/lib/config-warnings.mjs +22 -0
- package/bin/lib/core-layers.mjs +7 -0
- package/bin/lib/core-ratchet.mjs +3 -7
- package/bin/lib/doctor-plan.mjs +83 -5
- package/bin/lib/port-proof.mjs +309 -0
- package/bin/lib/prepare-write.mjs +130 -0
- package/bin/lib/remediation.mjs +21 -0
- package/bin/lib/safety-diagnostics.mjs +263 -0
- package/bin/lib/scan-files.mjs +51 -6
- package/bin/lib/violations.mjs +3 -3
- package/dist/index.cjs +115 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +115 -11
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +18 -5
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +18 -5
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +115 -11
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +115 -11
- package/dist/runtime/index.js.map +1 -1
- package/dist/{types-D6Q8WHes.d.cts → types-BZ17b9i5.d.cts} +5 -1
- package/dist/{types-D6Q8WHes.d.ts → types-BZ17b9i5.d.ts} +5 -1
- package/docs/agent-guide.md +15 -1
- package/docs/ai-gates.md +63 -5
- package/docs/enthusiast/how-to-agent-gates.md +8 -0
- package/docs/enthusiast/reference-commands.md +1 -1
- package/docs/package-surface.md +2 -2
- package/docs/production-hardening.md +5 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/templates/skills/ark-explain.md +1 -1
- package/templates/skills/ark-loop.md +2 -1
package/bin/ark-mcp.mjs
CHANGED
|
@@ -12,18 +12,27 @@
|
|
|
12
12
|
* - resource ark://manifest — the architectural contract (layers + rules, or a project
|
|
13
13
|
* manifest file when --manifest is provided)
|
|
14
14
|
* - tool validate_code — runs Ark's AI code gate on a source snippet; returns
|
|
15
|
-
* { valid, violations } and sets isError when invalid
|
|
15
|
+
* { valid, violations, autoPatch? } and sets isError when invalid.
|
|
16
|
+
* autoPatch (W1) is a gate-revalidated rewrite for mechanical-safe
|
|
17
|
+
* import-type kinds only (not W6 port-proof — signature change is judgment);
|
|
18
|
+
* discarded if post-patch still invalid.
|
|
19
|
+
* - tool ark_prepare_write — W2: place + constrain + validate + autoPatch + judgmentBrief
|
|
20
|
+
* + contentHash (composes ark_place + write gate; not a second contract).
|
|
16
21
|
* - tool ark_recommend — deterministic application-shape plan (same as
|
|
17
22
|
* ark-check --recommend --json)
|
|
18
23
|
*
|
|
19
24
|
* Usage: ark-mcp [--root <dir>] [--config ark.config.json] [--manifest <manifest.json>]
|
|
20
|
-
* ark-mcp --hook [--root <dir>] [--config ark.config.json]
|
|
25
|
+
* ark-mcp --hook [--hook-repair] [--root <dir>] [--config ark.config.json]
|
|
21
26
|
*
|
|
22
27
|
* --hook runs one-shot instead of serving: it reads a Claude Code PreToolUse payload from
|
|
23
28
|
* stdin, validates the file content a Write/Edit/MultiEdit is about to produce, and exits
|
|
24
29
|
* 2 with the violations on stderr when the write must be blocked (0 otherwise). This is
|
|
25
30
|
* the copy-paste integration for agent runtimes whose hooks run shell commands.
|
|
26
31
|
*
|
|
32
|
+
* --hook-repair (W4, also ARK_HOOK_REPAIR=1): on deny, emit machine-readable
|
|
33
|
+
* ARK_REPAIR_JSON / ARK_AUTOPATCH_JSON on stderr (and autoPatch in Grok deny JSON).
|
|
34
|
+
* Never silently writes the file — default and repair mode both hard-block.
|
|
35
|
+
*
|
|
27
36
|
* --session-context runs one-shot and prints a compact contract summary (layers, rule
|
|
28
37
|
* count, forbidden globals, baseline state, check command) to stdout. Bind it to a
|
|
29
38
|
* SessionStart hook so the agent has the architecture in context from the first token,
|
|
@@ -39,6 +48,7 @@ import {
|
|
|
39
48
|
DEFAULT_LAYER_DIRECTORIES,
|
|
40
49
|
DEFAULT_RULES,
|
|
41
50
|
arkCommand,
|
|
51
|
+
globToRegExp,
|
|
42
52
|
layerForFile,
|
|
43
53
|
shouldShowNewHereNudge,
|
|
44
54
|
detectWorkspaces,
|
|
@@ -46,9 +56,22 @@ import {
|
|
|
46
56
|
resolveIncludeRoots,
|
|
47
57
|
} from './ark-shared.mjs';
|
|
48
58
|
import { createImportTargetResolver } from './lib/import-resolve.mjs';
|
|
59
|
+
import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
|
|
60
|
+
import { composePrepareWrite } from './lib/prepare-write.mjs';
|
|
49
61
|
|
|
50
62
|
const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* W4 — opt-in hook repair payload.
|
|
66
|
+
* True when CLI `--hook-repair` or env ARK_HOOK_REPAIR is 1/true/yes.
|
|
67
|
+
* Default remains hard block with prose violations only (no machine-readable patch).
|
|
68
|
+
*/
|
|
69
|
+
function envTruthy(name) {
|
|
70
|
+
const v = process.env[name];
|
|
71
|
+
if (v == null || v === '') return false;
|
|
72
|
+
return /^(1|true|yes|on)$/i.test(String(v).trim());
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
function parseArgs(argv) {
|
|
53
76
|
const args = {
|
|
54
77
|
root: process.cwd(),
|
|
@@ -56,18 +79,27 @@ function parseArgs(argv) {
|
|
|
56
79
|
configExplicit: false,
|
|
57
80
|
manifest: undefined,
|
|
58
81
|
hook: false,
|
|
82
|
+
/** When true with --hook: emit ARK_REPAIR_JSON / ARK_AUTOPATCH_JSON (never silent write). */
|
|
83
|
+
hookRepair: false,
|
|
59
84
|
sessionContext: false,
|
|
60
85
|
};
|
|
61
86
|
for (let i = 2; i < argv.length; i += 1) {
|
|
62
87
|
const a = argv[i];
|
|
63
88
|
if (a === '--hook') args.hook = true;
|
|
64
|
-
else if (a === '--
|
|
89
|
+
else if (a === '--hook-repair') {
|
|
90
|
+
args.hook = true;
|
|
91
|
+
args.hookRepair = true;
|
|
92
|
+
} else if (a === '--session-context') args.sessionContext = true;
|
|
65
93
|
else if (a === '--root') args.root = path.resolve(argv[++i]);
|
|
66
94
|
else if (a === '--config') {
|
|
67
95
|
args.config = argv[++i];
|
|
68
96
|
args.configExplicit = true;
|
|
69
97
|
} else if (a === '--manifest') args.manifest = argv[++i];
|
|
70
98
|
}
|
|
99
|
+
// Env can enable repair without rewriting host templates (ARK_HOOK_REPAIR=1).
|
|
100
|
+
if (envTruthy('ARK_HOOK_REPAIR')) {
|
|
101
|
+
args.hookRepair = true;
|
|
102
|
+
}
|
|
71
103
|
return args;
|
|
72
104
|
}
|
|
73
105
|
|
|
@@ -191,7 +223,7 @@ function proposedSource(toolName, toolInput) {
|
|
|
191
223
|
* decision JSON on stdout. Gate plumbing problems (no stdin, malformed JSON, non-file
|
|
192
224
|
* tools, non-source files) never block the agent.
|
|
193
225
|
*/
|
|
194
|
-
function runHook(gate, config, args) {
|
|
226
|
+
function runHook(gate, config, args, ts) {
|
|
195
227
|
let payload;
|
|
196
228
|
try {
|
|
197
229
|
payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
@@ -213,27 +245,52 @@ function runHook(gate, config, args) {
|
|
|
213
245
|
if (typeof source !== 'string') return;
|
|
214
246
|
|
|
215
247
|
const layer = inferLayer(filePath, config, args.root);
|
|
216
|
-
const
|
|
248
|
+
const validateOnce = (src) => gate.validate(src, { layer, filePath });
|
|
249
|
+
// W1: one validation pass (+ optional autoPatch). Original write still blocked when
|
|
250
|
+
// invalid; hosts must apply autoPatch explicitly (never silent write).
|
|
251
|
+
const result = ts
|
|
252
|
+
? validateWithAutoPatch({
|
|
253
|
+
source,
|
|
254
|
+
filePath,
|
|
255
|
+
root: args.root,
|
|
256
|
+
ts,
|
|
257
|
+
validate: validateOnce,
|
|
258
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
259
|
+
})
|
|
260
|
+
: (() => {
|
|
261
|
+
const once = validateOnce(source);
|
|
262
|
+
return {
|
|
263
|
+
valid: Boolean(once.valid),
|
|
264
|
+
violations: once.violations ?? [],
|
|
265
|
+
autoPatch: null,
|
|
266
|
+
};
|
|
267
|
+
})();
|
|
217
268
|
if (result.valid) return;
|
|
218
269
|
|
|
219
270
|
// Ratchet semantics (same philosophy as ark-check --baseline): an edit is blocked only
|
|
220
271
|
// when it ADDS violations relative to the file's current on-disk state. Otherwise a
|
|
221
272
|
// pre-existing violation — frozen in a baseline or predating Ark adoption — would make
|
|
222
|
-
// every subsequent edit to that file un-writable while CI passes.
|
|
223
|
-
// numbers (edits shift them)
|
|
273
|
+
// every subsequent edit to that file un-writable while CI passes. Same-file keys ignore
|
|
274
|
+
// line numbers (edits shift them); simpler than full baselineKey (no file/layer fields
|
|
275
|
+
// needed — this file is fixed).
|
|
224
276
|
const violationKey = (violation) => `${violation.ruleId}|${violation.target ?? violation.message}`;
|
|
225
|
-
let
|
|
277
|
+
let existingCounts = new Map();
|
|
226
278
|
try {
|
|
227
279
|
const current = fs.readFileSync(filePath, 'utf8');
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
280
|
+
for (const violation of gate.validate(current, { layer, filePath }).violations) {
|
|
281
|
+
const key = violationKey(violation);
|
|
282
|
+
existingCounts.set(key, (existingCounts.get(key) ?? 0) + 1);
|
|
283
|
+
}
|
|
231
284
|
} catch {
|
|
232
285
|
// New file: nothing pre-exists, every violation is new.
|
|
233
286
|
}
|
|
234
|
-
const newViolations = result.violations.filter(
|
|
235
|
-
|
|
236
|
-
|
|
287
|
+
const newViolations = (result.violations ?? []).filter((violation) => {
|
|
288
|
+
const key = violationKey(violation);
|
|
289
|
+
const remaining = existingCounts.get(key) ?? 0;
|
|
290
|
+
if (remaining === 0) return true;
|
|
291
|
+
existingCounts.set(key, remaining - 1);
|
|
292
|
+
return false;
|
|
293
|
+
});
|
|
237
294
|
if (newViolations.length === 0) return;
|
|
238
295
|
|
|
239
296
|
const lines = newViolations.map(
|
|
@@ -246,16 +303,68 @@ function runHook(gate, config, args) {
|
|
|
246
303
|
const suggestions = [
|
|
247
304
|
...new Set(newViolations.map((violation) => violation.suggestion).filter(Boolean)),
|
|
248
305
|
];
|
|
306
|
+
const autoPatch = result.autoPatch;
|
|
307
|
+
// W4: structured repair payload is opt-in (--hook-repair / ARK_HOOK_REPAIR).
|
|
308
|
+
// Default remains hard block with prose only — hosts that cannot re-inject stay clean.
|
|
309
|
+
const repair = Boolean(args.hookRepair);
|
|
249
310
|
const message = [
|
|
250
311
|
`Ark architecture gate blocked this write to ${rel}${layer ? ` (layer: ${layer})` : ''}:`,
|
|
251
312
|
...lines,
|
|
252
313
|
...(suggestions.length > 0 ? ['Fix:', ...suggestions.map((s) => ` ${s}`)] : []),
|
|
314
|
+
...(autoPatch && repair
|
|
315
|
+
? [
|
|
316
|
+
`autoPatch available (${autoPatch.remediationKind}, confidence ${autoPatch.confidence}): ` +
|
|
317
|
+
'apply the patched source from ARK_AUTOPATCH_JSON / ARK_REPAIR_JSON on stderr' +
|
|
318
|
+
(grokStyle ? ' (or autoPatch in the deny JSON on stdout)' : '') +
|
|
319
|
+
' instead of re-drafting. Gate still denies this write (never silent apply).',
|
|
320
|
+
]
|
|
321
|
+
: []),
|
|
322
|
+
...(autoPatch && !repair
|
|
323
|
+
? [
|
|
324
|
+
`Mechanical-safe autoPatch is available (${autoPatch.remediationKind}). ` +
|
|
325
|
+
'Enable repair payload with ARK_HOOK_REPAIR=1 or --hook-repair to receive ' +
|
|
326
|
+
'machine-readable source (still hard-blocks; host re-injects).',
|
|
327
|
+
]
|
|
328
|
+
: []),
|
|
253
329
|
'Fix the violations and retry. The architecture contract is available as the ark://manifest MCP resource.',
|
|
254
330
|
].join('\n');
|
|
255
331
|
process.stderr.write(message + '\n');
|
|
332
|
+
|
|
333
|
+
if (repair) {
|
|
334
|
+
// Structured envelope for any host that can re-inject. Never writes the file.
|
|
335
|
+
const repairPayload = {
|
|
336
|
+
mode: 'repair',
|
|
337
|
+
decision: 'deny',
|
|
338
|
+
filePath: rel.split(path.sep).join('/'),
|
|
339
|
+
...(layer ? { layer } : {}),
|
|
340
|
+
...(autoPatch
|
|
341
|
+
? {
|
|
342
|
+
autoPatch: {
|
|
343
|
+
source: autoPatch.source,
|
|
344
|
+
remediationKind: autoPatch.remediationKind,
|
|
345
|
+
confidence: autoPatch.confidence,
|
|
346
|
+
valid: autoPatch.valid,
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
: { autoPatch: null }),
|
|
350
|
+
};
|
|
351
|
+
process.stderr.write(`ARK_REPAIR_JSON:${JSON.stringify(repairPayload)}\n`);
|
|
352
|
+
if (autoPatch) {
|
|
353
|
+
process.stderr.write(`ARK_AUTOPATCH_JSON:${JSON.stringify(autoPatch)}\n`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
256
357
|
// Grok Build honors { decision: "deny" } on stdout (exit 2 alone is also deny).
|
|
358
|
+
// autoPatch in stdout only when repair mode is on (same opt-in as stderr).
|
|
257
359
|
if (grokStyle) {
|
|
258
|
-
process.stdout.write(
|
|
360
|
+
process.stdout.write(
|
|
361
|
+
JSON.stringify({
|
|
362
|
+
decision: 'deny',
|
|
363
|
+
reason: message,
|
|
364
|
+
...(repair && autoPatch ? { autoPatch } : {}),
|
|
365
|
+
...(repair ? { repair: true } : {}),
|
|
366
|
+
}) + '\n'
|
|
367
|
+
);
|
|
259
368
|
}
|
|
260
369
|
process.exitCode = 2;
|
|
261
370
|
}
|
|
@@ -265,8 +374,14 @@ function runArkCheckJsonFromRoot(root, config, extraArgs, manifest) {
|
|
|
265
374
|
const result = spawnSync(
|
|
266
375
|
process.execPath,
|
|
267
376
|
[arkCheckBin, '--root', root, '--config', config, ...manifestArgs, '--json', ...extraArgs],
|
|
268
|
-
{ encoding: 'utf8' }
|
|
377
|
+
{ encoding: 'utf8', timeout: 120_000, maxBuffer: 20 * 1024 * 1024 }
|
|
269
378
|
);
|
|
379
|
+
if (result.error) {
|
|
380
|
+
return {
|
|
381
|
+
data: null,
|
|
382
|
+
raw: `ark-check failed to execute: ${result.error.message}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
270
385
|
const stdout = result.stdout ?? '';
|
|
271
386
|
try {
|
|
272
387
|
return { data: JSON.parse(stdout), raw: stdout };
|
|
@@ -441,10 +556,22 @@ async function main() {
|
|
|
441
556
|
name: layer.name,
|
|
442
557
|
patterns: layer.patterns,
|
|
443
558
|
})),
|
|
559
|
+
allowNonLiteralDynamicImport: (filePath) => {
|
|
560
|
+
if (!filePath || !Array.isArray(config.dynamicImportAllowlist)) return false;
|
|
561
|
+
const rel = path.relative(args.root, path.resolve(args.root, filePath)).split(path.sep).join('/');
|
|
562
|
+
return config.dynamicImportAllowlist.some((pattern) => {
|
|
563
|
+
if (typeof pattern !== 'string') return false;
|
|
564
|
+
try {
|
|
565
|
+
return globToRegExp(pattern).test(rel);
|
|
566
|
+
} catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
},
|
|
444
571
|
});
|
|
445
572
|
|
|
446
573
|
if (args.hook) {
|
|
447
|
-
runHook(gate, config, args);
|
|
574
|
+
runHook(gate, config, args, ts);
|
|
448
575
|
return;
|
|
449
576
|
}
|
|
450
577
|
|
|
@@ -463,7 +590,9 @@ async function main() {
|
|
|
463
590
|
"Validate a source snippet about to be written against Ark's architecture " +
|
|
464
591
|
'(forbidden infra imports, unknown intents, and layer-reference violations). ' +
|
|
465
592
|
'Bind to PreToolUse on Write/Edit to block architecturally-invalid generated code. ' +
|
|
466
|
-
'Returns { valid, violations }
|
|
593
|
+
'Returns { valid, violations, autoPatch? }. autoPatch (when present) is a ' +
|
|
594
|
+
'mechanical-safe rewrite of the source (import type conversion) that re-validates green; ' +
|
|
595
|
+
'hosts may apply it instead of re-drafting. isError is true when valid is false.',
|
|
467
596
|
inputSchema: {
|
|
468
597
|
type: 'object',
|
|
469
598
|
properties: {
|
|
@@ -518,7 +647,8 @@ async function main() {
|
|
|
518
647
|
description:
|
|
519
648
|
'Place a file in the architecture: pass filePath (preferred) and/or description. ' +
|
|
520
649
|
'Returns layer, mayImport / mustNotImport, forbiddenGlobals. Call BEFORE writing a new file. ' +
|
|
521
|
-
'If only description is given, returns a conventional path proposal under a governed layer.'
|
|
650
|
+
'If only description is given, returns a conventional path proposal under a governed layer. ' +
|
|
651
|
+
'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
|
|
522
652
|
inputSchema: {
|
|
523
653
|
type: 'object',
|
|
524
654
|
properties: {
|
|
@@ -534,6 +664,34 @@ async function main() {
|
|
|
534
664
|
},
|
|
535
665
|
},
|
|
536
666
|
},
|
|
667
|
+
{
|
|
668
|
+
name: 'ark_prepare_write',
|
|
669
|
+
description:
|
|
670
|
+
'Prepare a write against the architecture contract: place (filePath and/or description) + ' +
|
|
671
|
+
'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
|
|
672
|
+
'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
|
|
673
|
+
'Composes ark_place + write-gate — call BEFORE Write/Edit when you have the snippet. ' +
|
|
674
|
+
'Returns { filePath, layer, valid, violations?, autoPatch?, judgmentBrief?, contentHash, ... }.',
|
|
675
|
+
inputSchema: {
|
|
676
|
+
type: 'object',
|
|
677
|
+
properties: {
|
|
678
|
+
source: { type: 'string', description: 'Full source text about to be written.' },
|
|
679
|
+
filePath: {
|
|
680
|
+
type: 'string',
|
|
681
|
+
description: 'Target path (preferred). Used for layer inference and autoPatch resolution.',
|
|
682
|
+
},
|
|
683
|
+
description: {
|
|
684
|
+
type: 'string',
|
|
685
|
+
description: 'When filePath omitted: propose a conventional path from this description.',
|
|
686
|
+
},
|
|
687
|
+
layer: {
|
|
688
|
+
type: 'string',
|
|
689
|
+
description: 'Optional explicit layer override (otherwise inferred from filePath).',
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
required: ['source'],
|
|
693
|
+
},
|
|
694
|
+
},
|
|
537
695
|
{
|
|
538
696
|
name: 'ark_recommend',
|
|
539
697
|
description:
|
|
@@ -572,7 +730,10 @@ async function main() {
|
|
|
572
730
|
// DomainModel there would tell the agent to create a second layer for the same
|
|
573
731
|
// prefix, making longest-prefix resolution ambiguous.
|
|
574
732
|
function suggestedLayers() {
|
|
575
|
-
const activeNames = new Set(
|
|
733
|
+
const activeNames = new Set([
|
|
734
|
+
...configLayers.map((layer) => layer.name),
|
|
735
|
+
...profile.layers.map((layer) => layer.name),
|
|
736
|
+
]);
|
|
576
737
|
const claimedPrefixes = new Set(
|
|
577
738
|
profile.layers.flatMap((layer) =>
|
|
578
739
|
(layer.prefixes ?? []).map((p) => (p.endsWith('.') ? p : `${p}.`))
|
|
@@ -598,13 +759,30 @@ async function main() {
|
|
|
598
759
|
);
|
|
599
760
|
}
|
|
600
761
|
const suggestions = suggestedLayers();
|
|
762
|
+
const contractLayers = usedProjectConfig
|
|
763
|
+
? configLayers.map((layer) => ({
|
|
764
|
+
...layer,
|
|
765
|
+
prefixes: Array.isArray(layer.intentPrefixes) ? layer.intentPrefixes : [],
|
|
766
|
+
}))
|
|
767
|
+
: profile.layers;
|
|
601
768
|
return JSON.stringify(
|
|
602
769
|
{
|
|
603
770
|
source: profile === ark.elevenLayerProfile ? 'strictDefaultElevenLayerProfile' : 'project',
|
|
604
771
|
name: profile.name,
|
|
605
|
-
|
|
772
|
+
// File placement contract: every configured layer, including layers that do not
|
|
773
|
+
// own intent prefixes (e.g. Tooling / FrameworkAdapters).
|
|
774
|
+
layers: contractLayers,
|
|
775
|
+
// Runtime/intent resolution profile kept explicit so consumers never have to infer
|
|
776
|
+
// why a prefix-less file layer is absent from intent resolution.
|
|
777
|
+
intentLayers: profile.layers,
|
|
606
778
|
rules: profile.rules,
|
|
607
779
|
...(Object.keys(forbiddenGlobals).length > 0 ? { forbiddenGlobals } : {}),
|
|
780
|
+
...(Array.isArray(config.dynamicImportAllowlist)
|
|
781
|
+
? { dynamicImportAllowlist: config.dynamicImportAllowlist }
|
|
782
|
+
: {}),
|
|
783
|
+
...(config.safety && typeof config.safety === 'object'
|
|
784
|
+
? { safety: config.safety }
|
|
785
|
+
: {}),
|
|
608
786
|
...(suggestions.length > 0
|
|
609
787
|
? {
|
|
610
788
|
suggestedLayers: suggestions,
|
|
@@ -626,13 +804,38 @@ async function main() {
|
|
|
626
804
|
if (typeof source !== 'string') {
|
|
627
805
|
return { content: [{ type: 'text', text: 'Missing required "source" argument.' }], isError: true };
|
|
628
806
|
}
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
807
|
+
const filePath = params.arguments.filePath;
|
|
808
|
+
const layer = params.arguments.layer ?? inferLayer(filePath, config, args.root);
|
|
809
|
+
const validateOnce = (src) =>
|
|
810
|
+
gate.validate(src, {
|
|
811
|
+
layer,
|
|
812
|
+
filePath,
|
|
813
|
+
});
|
|
814
|
+
// W1: attempt mechanical-safe single-file autoPatch (import type), re-validate or discard.
|
|
815
|
+
const result = validateWithAutoPatch({
|
|
816
|
+
source,
|
|
817
|
+
filePath,
|
|
818
|
+
root: args.root,
|
|
819
|
+
ts,
|
|
820
|
+
validate: validateOnce,
|
|
821
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
633
822
|
});
|
|
634
823
|
return {
|
|
635
|
-
content: [
|
|
824
|
+
content: [
|
|
825
|
+
{
|
|
826
|
+
type: 'text',
|
|
827
|
+
text: JSON.stringify(
|
|
828
|
+
{
|
|
829
|
+
valid: result.valid,
|
|
830
|
+
violations: result.violations,
|
|
831
|
+
...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
|
|
832
|
+
layer,
|
|
833
|
+
},
|
|
834
|
+
null,
|
|
835
|
+
2
|
|
836
|
+
),
|
|
837
|
+
},
|
|
838
|
+
],
|
|
636
839
|
isError: !result.valid,
|
|
637
840
|
};
|
|
638
841
|
}
|
|
@@ -691,11 +894,8 @@ async function main() {
|
|
|
691
894
|
// Deterministic placement guidance (in-process; no TS resolver needed): which layer a
|
|
692
895
|
// path falls in, and — from the same rules ark-check enforces (default allow, explicit
|
|
693
896
|
// `allowed:false` denies) — which layers it may and must not import.
|
|
694
|
-
function
|
|
695
|
-
const filePath = params?.arguments?.filePath;
|
|
696
|
-
const description = params?.arguments?.description;
|
|
897
|
+
function placeResult(filePath, description) {
|
|
697
898
|
if ((typeof filePath !== 'string' || !filePath) && typeof description === 'string' && description.trim()) {
|
|
698
|
-
// Description-only: propose a governed path under PresentationAdapters (UI default).
|
|
699
899
|
const slug = description
|
|
700
900
|
.trim()
|
|
701
901
|
.toLowerCase()
|
|
@@ -705,73 +905,39 @@ async function main() {
|
|
|
705
905
|
const proposedPath = `src/components/${slug}.tsx`;
|
|
706
906
|
const layerName = inferLayer(proposedPath, config, args.root) || 'PresentationAdapters';
|
|
707
907
|
return {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
layer: layerName,
|
|
717
|
-
governed: Boolean(inferLayer(proposedPath, config, args.root)),
|
|
718
|
-
note:
|
|
719
|
-
'filePath was omitted — proposed a conventional path from description. ' +
|
|
720
|
-
'Pass filePath explicitly for authoritative placement. Then validate_code the snippet.',
|
|
721
|
-
},
|
|
722
|
-
null,
|
|
723
|
-
2
|
|
724
|
-
),
|
|
725
|
-
},
|
|
726
|
-
],
|
|
727
|
-
isError: false,
|
|
908
|
+
filePath: proposedPath,
|
|
909
|
+
proposed: true,
|
|
910
|
+
description: description.trim(),
|
|
911
|
+
layer: layerName,
|
|
912
|
+
governed: Boolean(inferLayer(proposedPath, config, args.root)),
|
|
913
|
+
note:
|
|
914
|
+
'filePath was omitted — proposed a conventional path from description. ' +
|
|
915
|
+
'Pass filePath explicitly for authoritative placement.',
|
|
728
916
|
};
|
|
729
917
|
}
|
|
730
918
|
if (typeof filePath !== 'string' || !filePath) {
|
|
731
919
|
return {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
text:
|
|
736
|
-
'ark_place needs filePath and/or description. ' +
|
|
737
|
-
'Example: { "filePath": "src/components/Foo.tsx" } or { "description": "caption overlay UI component" }.',
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
isError: true,
|
|
920
|
+
error:
|
|
921
|
+
'Needs filePath and/or description. ' +
|
|
922
|
+
'Example: { "filePath": "src/components/Foo.tsx" } or { "description": "caption overlay UI component" }.',
|
|
741
923
|
};
|
|
742
924
|
}
|
|
743
925
|
const layerName = inferLayer(filePath, config, args.root);
|
|
744
926
|
if (!layerName) {
|
|
745
|
-
// Two distinct reasons the path matched no layer: either this project declares no
|
|
746
|
-
// path-based layers at all (the gate still enforces the default 11-layer profile by
|
|
747
|
-
// intent-name PREFIX — placement just can't be inferred from the path), or it does
|
|
748
|
-
// declare layers and this path falls outside all of them (genuinely ungoverned).
|
|
749
927
|
const noLayers = configLayers.length === 0;
|
|
750
928
|
return {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
'for the layers and validate the actual snippet with validate_code.'
|
|
764
|
-
: 'No layer pattern matches this path — code here is UNGOVERNED (no import ' +
|
|
765
|
-
'rules enforced). Place it under a directory a layer in ark.config.json ' +
|
|
766
|
-
'matches, or add a layer. See suggestedLayers for conventional homes.',
|
|
767
|
-
suggestedLayers: suggestedLayers(),
|
|
768
|
-
},
|
|
769
|
-
null,
|
|
770
|
-
2
|
|
771
|
-
),
|
|
772
|
-
},
|
|
773
|
-
],
|
|
774
|
-
isError: false,
|
|
929
|
+
filePath,
|
|
930
|
+
layer: null,
|
|
931
|
+
governed: noLayers,
|
|
932
|
+
message: noLayers
|
|
933
|
+
? 'This project declares no path-based layers in ark.config.json, so a ' +
|
|
934
|
+
'layer cannot be inferred from the path. The gate still enforces the ' +
|
|
935
|
+
'default 11-layer profile by intent-name prefix — read ark://manifest ' +
|
|
936
|
+
'for the layers and validate the actual snippet with validate_code.'
|
|
937
|
+
: 'No layer pattern matches this path — code here is UNGOVERNED (no import ' +
|
|
938
|
+
'rules enforced). Place it under a directory a layer in ark.config.json ' +
|
|
939
|
+
'matches, or add a layer. See suggestedLayers for conventional homes.',
|
|
940
|
+
suggestedLayers: suggestedLayers(),
|
|
775
941
|
};
|
|
776
942
|
}
|
|
777
943
|
const layerMeta = configLayers.find((layer) => layer.name === layerName);
|
|
@@ -782,34 +948,91 @@ async function main() {
|
|
|
782
948
|
);
|
|
783
949
|
const mayImport = otherNames.filter((name) => !mustNotImport.includes(name));
|
|
784
950
|
return {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
],
|
|
951
|
+
filePath,
|
|
952
|
+
layer: layerName,
|
|
953
|
+
governed: true,
|
|
954
|
+
description: layerMeta?.description,
|
|
955
|
+
forbiddenGlobals: layerMeta?.forbiddenGlobals ?? [],
|
|
956
|
+
...(layerMeta?.mayImportInfrastructure ? { mayImportInfrastructure: true } : {}),
|
|
957
|
+
mayImport,
|
|
958
|
+
mustNotImport,
|
|
959
|
+
note:
|
|
960
|
+
'mayImport = layers with no explicit deny (default is allow). Respect ' +
|
|
961
|
+
'forbiddenGlobals, then verify the actual snippet with validate_code or ark_prepare_write.',
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function runPlace(params) {
|
|
966
|
+
const placement = placeResult(params?.arguments?.filePath, params?.arguments?.description);
|
|
967
|
+
if (placement.error) {
|
|
968
|
+
return {
|
|
969
|
+
content: [{ type: 'text', text: `ark_place: ${placement.error}` }],
|
|
970
|
+
isError: true,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
content: [{ type: 'text', text: JSON.stringify(placement, null, 2) }],
|
|
809
975
|
isError: false,
|
|
810
976
|
};
|
|
811
977
|
}
|
|
812
978
|
|
|
979
|
+
/**
|
|
980
|
+
* W2: place + constrain + validate + autoPatch + judgmentBrief + contentHash.
|
|
981
|
+
* Composes ark_place + write-boundary gate — not a second contract.
|
|
982
|
+
*/
|
|
983
|
+
function runPrepareWrite(params) {
|
|
984
|
+
const source = params?.arguments?.source;
|
|
985
|
+
const filePath = params?.arguments?.filePath;
|
|
986
|
+
const description = params?.arguments?.description;
|
|
987
|
+
if (typeof source !== 'string') {
|
|
988
|
+
return {
|
|
989
|
+
content: [
|
|
990
|
+
{
|
|
991
|
+
type: 'text',
|
|
992
|
+
text: 'ark_prepare_write requires "source" (string). Optional: filePath, description.',
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
isError: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const placement = placeResult(filePath, description);
|
|
999
|
+
if (placement.error) {
|
|
1000
|
+
return {
|
|
1001
|
+
content: [{ type: 'text', text: `ark_prepare_write: ${placement.error}` }],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
const layer =
|
|
1006
|
+
placement.layer ||
|
|
1007
|
+
params?.arguments?.layer ||
|
|
1008
|
+
inferLayer(placement.filePath, config, args.root);
|
|
1009
|
+
const validateOnce = (src) =>
|
|
1010
|
+
gate.validate(src, {
|
|
1011
|
+
layer,
|
|
1012
|
+
filePath: placement.filePath,
|
|
1013
|
+
});
|
|
1014
|
+
const result = composePrepareWrite({
|
|
1015
|
+
source,
|
|
1016
|
+
placement: { ...placement, layer },
|
|
1017
|
+
root: args.root,
|
|
1018
|
+
ts,
|
|
1019
|
+
validate: validateOnce,
|
|
1020
|
+
resolveTargetAbs: resolveImportFileAbs,
|
|
1021
|
+
});
|
|
1022
|
+
if (!result.ok) {
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{ type: 'text', text: result.error || 'prepare_write failed' }],
|
|
1025
|
+
isError: true,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1030
|
+
// Align with validate_code / --hook: proposed source still invalid → isError.
|
|
1031
|
+
// autoPatch is additive recovery guidance in the body, never soft-success.
|
|
1032
|
+
isError: !result.valid,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
813
1036
|
function runSuggestIncludeTool() {
|
|
814
1037
|
try {
|
|
815
1038
|
const workspaces = detectWorkspaces(args.root);
|
|
@@ -852,6 +1075,7 @@ async function main() {
|
|
|
852
1075
|
ark_check: runCheckTool,
|
|
853
1076
|
ark_coverage: runCoverageTool,
|
|
854
1077
|
ark_place: runPlace,
|
|
1078
|
+
ark_prepare_write: runPrepareWrite,
|
|
855
1079
|
ark_recommend: runRecommendTool,
|
|
856
1080
|
ark_suggest_include: runSuggestIncludeTool,
|
|
857
1081
|
};
|