ark-runtime-kernel 1.0.0 → 1.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/README.md +133 -219
- package/bin/ark-check.mjs +316 -20
- package/bin/ark-mcp.mjs +0 -0
- package/bin/ark-postinstall.mjs +12 -0
- package/bin/ark.mjs +129 -0
- package/dist/index.cjs +26 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -958
- package/dist/index.d.ts +5 -958
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +2301 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +22 -0
- package/dist/nestjs/index.d.ts +22 -0
- package/dist/nestjs/index.js +2298 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/types-7K_KQCgS.d.cts +991 -0
- package/dist/types-7K_KQCgS.d.ts +991 -0
- package/docs/agent-guide.md +344 -0
- package/docs/ai-gates.md +169 -0
- package/docs/ark-check-example.json +87 -0
- package/docs/assets/ark-write-gate.svg +28 -0
- package/docs/blog/how-i-stopped-claude-from-breaking-my-architecture.md +85 -0
- package/docs/production-hardening.md +59 -0
- package/package.json +46 -8
- package/server.json +31 -0
package/bin/ark-check.mjs
CHANGED
|
@@ -22,14 +22,24 @@ function parseArgs(argv) {
|
|
|
22
22
|
json: false,
|
|
23
23
|
strictConfig: false,
|
|
24
24
|
init: false,
|
|
25
|
+
installAgentGates: false,
|
|
25
26
|
force: false,
|
|
27
|
+
baseline: undefined,
|
|
28
|
+
updateBaseline: false,
|
|
26
29
|
};
|
|
27
30
|
for (let i = 2; i < argv.length; i += 1) {
|
|
28
31
|
const arg = argv[i];
|
|
29
32
|
if (arg === '--json') args.json = true;
|
|
30
33
|
else if (arg === '--strict-config') args.strictConfig = true;
|
|
31
34
|
else if (arg === '--init') args.init = true;
|
|
35
|
+
else if (arg === '--install-agent-gates') args.installAgentGates = true;
|
|
32
36
|
else if (arg === '--force') args.force = true;
|
|
37
|
+
else if (arg === '--baseline' || arg === '--update-baseline') {
|
|
38
|
+
if (arg === '--update-baseline') args.updateBaseline = true;
|
|
39
|
+
// optional path value: consume the next arg only when it isn't another flag
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
|
|
42
|
+
}
|
|
33
43
|
else if (arg === '--root') args.root = path.resolve(argv[++i]);
|
|
34
44
|
else if (arg === '--config') args.config = argv[++i];
|
|
35
45
|
else if (arg === '--manifest') args.manifest = argv[++i];
|
|
@@ -42,10 +52,16 @@ function parseArgs(argv) {
|
|
|
42
52
|
|
|
43
53
|
function usage() {
|
|
44
54
|
return [
|
|
45
|
-
'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json]',
|
|
55
|
+
'Usage: ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--json] [--baseline [file]]',
|
|
46
56
|
' ark-check --init [--force]',
|
|
57
|
+
' ark-check --install-agent-gates [--force]',
|
|
58
|
+
' ark-check --update-baseline [file] freeze current violations (default .ark-baseline.json)',
|
|
47
59
|
' ark-check --print-config eleven-layer',
|
|
48
60
|
'',
|
|
61
|
+
'Adopting Ark in an existing codebase? Run --update-baseline once to freeze existing',
|
|
62
|
+
'violations, commit the baseline file, and gate CI with --baseline: only NEW violations',
|
|
63
|
+
'fail the check, so the ratchet only moves toward zero.',
|
|
64
|
+
'',
|
|
49
65
|
'--init scans the project for the built-in layer directory conventions (src/domain,',
|
|
50
66
|
'src/application, src/adapters/persistence, ...) and writes an ark.config.json covering',
|
|
51
67
|
'only the layers that actually exist, with the default rules filtered to those layers.',
|
|
@@ -69,6 +85,9 @@ function usage() {
|
|
|
69
85
|
'',
|
|
70
86
|
'Generate a starter 11-layer config:',
|
|
71
87
|
' ark-check --print-config eleven-layer > ark.config.json',
|
|
88
|
+
'',
|
|
89
|
+
'Install agent + CI enforcement templates:',
|
|
90
|
+
' ark-check --install-agent-gates',
|
|
72
91
|
].join('\n');
|
|
73
92
|
}
|
|
74
93
|
|
|
@@ -192,6 +211,170 @@ function runInit(args) {
|
|
|
192
211
|
console.log(' (bind its validate_code tool to your agent\'s pre-write hook — see README)');
|
|
193
212
|
}
|
|
194
213
|
|
|
214
|
+
function ensureDirForFile(file) {
|
|
215
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeTemplate(root, relativePath, content, force) {
|
|
219
|
+
const fullPath = path.join(root, relativePath);
|
|
220
|
+
if (fs.existsSync(fullPath) && !force) {
|
|
221
|
+
return { relativePath, status: 'skipped' };
|
|
222
|
+
}
|
|
223
|
+
ensureDirForFile(fullPath);
|
|
224
|
+
fs.writeFileSync(fullPath, content);
|
|
225
|
+
return { relativePath, status: fs.existsSync(fullPath) ? 'written' : 'written' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function packageManager(root) {
|
|
229
|
+
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) {
|
|
230
|
+
return {
|
|
231
|
+
cache: 'pnpm',
|
|
232
|
+
setup: ['corepack enable'],
|
|
233
|
+
install: 'pnpm install --frozen-lockfile',
|
|
234
|
+
run: 'pnpm exec ark-check --root . --config ark.config.json --strict-config',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (fs.existsSync(path.join(root, 'yarn.lock'))) {
|
|
238
|
+
return {
|
|
239
|
+
cache: 'yarn',
|
|
240
|
+
setup: ['corepack enable'],
|
|
241
|
+
install: 'yarn install --frozen-lockfile',
|
|
242
|
+
run: 'yarn ark-check --root . --config ark.config.json --strict-config',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
cache: 'npm',
|
|
247
|
+
setup: [],
|
|
248
|
+
install: fs.existsSync(path.join(root, 'package-lock.json')) ? 'npm ci' : 'npm install',
|
|
249
|
+
run: 'npx ark-check --root . --config ark.config.json --strict-config',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function agentInstructions() {
|
|
254
|
+
return `# Ark Enforcement
|
|
255
|
+
|
|
256
|
+
Before editing TypeScript or JavaScript source files:
|
|
257
|
+
|
|
258
|
+
1. Read the Ark contract from \`ark://manifest\` when the MCP server is available.
|
|
259
|
+
2. Keep source files inside the layer boundaries declared in \`ark.config.json\`.
|
|
260
|
+
3. Do not bypass Ark publishers, event contracts, or source metadata for runtime mutations.
|
|
261
|
+
4. After edits, run \`npx ark-check --root . --config ark.config.json --strict-config\`.
|
|
262
|
+
5. If Ark reports violations, fix the architecture instead of weakening the gate.
|
|
263
|
+
|
|
264
|
+
The project is only considered Ark-enforced when the write gate, CI gate, and runtime path all pass.
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function mcpJson() {
|
|
269
|
+
return `${JSON.stringify({
|
|
270
|
+
mcpServers: {
|
|
271
|
+
ark: {
|
|
272
|
+
type: 'stdio',
|
|
273
|
+
command: 'npx',
|
|
274
|
+
args: ['ark-mcp', '--root', '.', '--config', 'ark.config.json'],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}, null, 2)}\n`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function codexTomlSnippet() {
|
|
281
|
+
return `[mcp_servers.ark]
|
|
282
|
+
command = "npx"
|
|
283
|
+
args = ["ark-mcp", "--root", ".", "--config", "ark.config.json"]
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cursorRule() {
|
|
288
|
+
return `---
|
|
289
|
+
description: Ark architecture contract
|
|
290
|
+
alwaysApply: true
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
Before writing or editing TypeScript or JavaScript source files, read the
|
|
294
|
+
\`ark://manifest\` resource from the \`ark\` MCP server when available.
|
|
295
|
+
|
|
296
|
+
Validate the full post-edit file content with the \`validate_code\` tool before
|
|
297
|
+
writing whenever your runtime supports it. After edits, run:
|
|
298
|
+
|
|
299
|
+
\`\`\`bash
|
|
300
|
+
npx ark-check --root . --config ark.config.json --strict-config
|
|
301
|
+
\`\`\`
|
|
302
|
+
|
|
303
|
+
If Ark reports violations, fix the architecture instead of bypassing the gate.
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function githubWorkflow(pm) {
|
|
308
|
+
const setupSteps = pm.setup.map((command) => ` - run: ${command}`).join('\n');
|
|
309
|
+
return `name: Ark architecture gate
|
|
310
|
+
|
|
311
|
+
on:
|
|
312
|
+
pull_request:
|
|
313
|
+
push:
|
|
314
|
+
branches: [main, master]
|
|
315
|
+
|
|
316
|
+
jobs:
|
|
317
|
+
ark-check:
|
|
318
|
+
runs-on: ubuntu-latest
|
|
319
|
+
steps:
|
|
320
|
+
- uses: actions/checkout@v4
|
|
321
|
+
- uses: actions/setup-node@v4
|
|
322
|
+
with:
|
|
323
|
+
node-version: 20
|
|
324
|
+
cache: ${pm.cache}
|
|
325
|
+
${setupSteps ? `${setupSteps}\n` : ''} - run: ${pm.install}
|
|
326
|
+
- run: ${pm.run}
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function claudeSettings() {
|
|
331
|
+
return `${JSON.stringify({
|
|
332
|
+
hooks: {
|
|
333
|
+
PreToolUse: [
|
|
334
|
+
{
|
|
335
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
336
|
+
hooks: [
|
|
337
|
+
{
|
|
338
|
+
type: 'command',
|
|
339
|
+
command:
|
|
340
|
+
'npx ark-mcp --hook --root "$CLAUDE_PROJECT_DIR" --config ark.config.json',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
}, null, 2)}\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function runInstallAgentGates(args) {
|
|
350
|
+
const root = args.root;
|
|
351
|
+
const pm = packageManager(root);
|
|
352
|
+
const templates = [
|
|
353
|
+
['AGENTS.md', agentInstructions()],
|
|
354
|
+
['.mcp.json', mcpJson()],
|
|
355
|
+
['.cursor/mcp.json', mcpJson()],
|
|
356
|
+
['.cursor/rules/ark.mdc', cursorRule()],
|
|
357
|
+
['.claude/settings.json', claudeSettings()],
|
|
358
|
+
['.github/workflows/ark-check.yml', githubWorkflow(pm)],
|
|
359
|
+
['docs/ark-codex-config.toml', codexTomlSnippet()],
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const results = templates.map(([relativePath, content]) =>
|
|
363
|
+
writeTemplate(root, relativePath, content, args.force)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
console.log('Ark agent gate templates:');
|
|
367
|
+
for (const result of results) {
|
|
368
|
+
const marker = result.status === 'written' ? 'wrote' : 'skipped';
|
|
369
|
+
console.log(` ${marker.padEnd(7)} ${result.relativePath}`);
|
|
370
|
+
}
|
|
371
|
+
console.log('');
|
|
372
|
+
console.log('Next steps:');
|
|
373
|
+
console.log(' 1. Review the generated files and commit the ones that match your tools.');
|
|
374
|
+
console.log(' 2. Run: npx ark-check --root . --config ark.config.json --strict-config');
|
|
375
|
+
console.log(' 3. Wire Codex manually from docs/ark-codex-config.toml if your host uses ~/.codex/config.toml.');
|
|
376
|
+
}
|
|
377
|
+
|
|
195
378
|
function readManifest(root, manifestPath) {
|
|
196
379
|
if (!manifestPath) return undefined;
|
|
197
380
|
const fullPath = path.isAbsolute(manifestPath)
|
|
@@ -564,6 +747,71 @@ function publishHasSource(ts, node) {
|
|
|
564
747
|
);
|
|
565
748
|
}
|
|
566
749
|
|
|
750
|
+
// ponytail: baseline keys exclude the line number so unrelated edits that shift lines
|
|
751
|
+
// don't resurrect frozen violations; the trade-off is that N identical violations in one
|
|
752
|
+
// file collapse to one key.
|
|
753
|
+
function baselineKey(violation) {
|
|
754
|
+
return [
|
|
755
|
+
violation.ruleId,
|
|
756
|
+
violation.file,
|
|
757
|
+
violation.fromLayer ?? '',
|
|
758
|
+
violation.toLayer ?? '',
|
|
759
|
+
violation.target ?? '',
|
|
760
|
+
].join('|');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function readBaseline(root, baselinePath) {
|
|
764
|
+
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
|
765
|
+
if (!fs.existsSync(fullPath)) return { keys: new Set(), fullPath, exists: false };
|
|
766
|
+
const raw = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
767
|
+
return { keys: new Set(raw.violations ?? []), fullPath, exists: true };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function writeBaseline(root, baselinePath, violations) {
|
|
771
|
+
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
|
772
|
+
const keys = [...new Set(violations.map(baselineKey))].sort();
|
|
773
|
+
fs.writeFileSync(
|
|
774
|
+
fullPath,
|
|
775
|
+
`${JSON.stringify({ version: 1, note: 'Frozen ark-check violations. Only NEW violations fail --baseline runs. Regenerate with: ark-check --update-baseline', violations: keys }, null, 2)}\n`
|
|
776
|
+
);
|
|
777
|
+
return { fullPath, count: keys.length };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const useColor = process.stderr.isTTY && !process.env.NO_COLOR;
|
|
781
|
+
const color = {
|
|
782
|
+
red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s),
|
|
783
|
+
yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s),
|
|
784
|
+
green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s),
|
|
785
|
+
dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
|
|
786
|
+
bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const FIX_HINTS = {
|
|
790
|
+
LAYER_IMPORT_VIOLATION:
|
|
791
|
+
'Depend on a port/interface owned by an inner layer instead, or move this code to a layer allowed to make this import.',
|
|
792
|
+
LAYER_INTENT_REFERENCE_VIOLATION:
|
|
793
|
+
'Reference intents through a layer that owns them (e.g. subscribe from an adapter, not from the domain).',
|
|
794
|
+
RAW_EVENT_PUBLISH:
|
|
795
|
+
'Define the intent with ark.registry.define(...) and publish through the returned creator.',
|
|
796
|
+
PUBLISH_MISSING_SOURCE:
|
|
797
|
+
'Add metadata.source (the publishing intent name) to the publish call.',
|
|
798
|
+
PUBLISH_SOURCE_LAYER_MISMATCH:
|
|
799
|
+
'Use a source intent that belongs to the same layer as the publishing file, or move the file.',
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
function printViolation(violation) {
|
|
803
|
+
const location = `${violation.file}:${violation.line}`;
|
|
804
|
+
console.error(`${color.red('✖')} ${color.bold(violation.ruleId)} ${location}`);
|
|
805
|
+
if (violation.fromLayer && violation.toLayer) {
|
|
806
|
+
const target = violation.target ? ` ${color.dim(`(${violation.target})`)}` : '';
|
|
807
|
+
console.error(` ${violation.fromLayer} → ${violation.toLayer}${target}`);
|
|
808
|
+
}
|
|
809
|
+
console.error(` ${violation.message}`);
|
|
810
|
+
const hint = FIX_HINTS[violation.ruleId];
|
|
811
|
+
if (hint) console.error(` ${color.dim(`fix: ${hint}`)}`);
|
|
812
|
+
console.error('');
|
|
813
|
+
}
|
|
814
|
+
|
|
567
815
|
function moduleSpecifierFromCall(ts, node) {
|
|
568
816
|
if (!ts.isCallExpression(node)) return undefined;
|
|
569
817
|
|
|
@@ -592,6 +840,10 @@ async function main() {
|
|
|
592
840
|
runInit(args);
|
|
593
841
|
return;
|
|
594
842
|
}
|
|
843
|
+
if (args.installAgentGates) {
|
|
844
|
+
runInstallAgentGates(args);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
595
847
|
if (args.printConfig) {
|
|
596
848
|
if (args.printConfig !== 'eleven-layer') {
|
|
597
849
|
console.error(`Unknown config profile: ${args.printConfig}`);
|
|
@@ -733,38 +985,82 @@ async function main() {
|
|
|
733
985
|
visit(sourceFile);
|
|
734
986
|
}
|
|
735
987
|
|
|
988
|
+
if (args.updateBaseline) {
|
|
989
|
+
const { fullPath, count } = writeBaseline(root, args.baseline, violations);
|
|
990
|
+
console.log(`Wrote ${fullPath} with ${count} frozen violation key(s).`);
|
|
991
|
+
console.log('Commit it and gate CI with: ark-check --baseline (only NEW violations fail).');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let suppressed = [];
|
|
996
|
+
let activeViolations = violations;
|
|
997
|
+
let staleBaselineKeys = 0;
|
|
998
|
+
if (args.baseline) {
|
|
999
|
+
const baseline = readBaseline(root, args.baseline);
|
|
1000
|
+
if (baseline.exists) {
|
|
1001
|
+
suppressed = violations.filter((violation) => baseline.keys.has(baselineKey(violation)));
|
|
1002
|
+
activeViolations = violations.filter(
|
|
1003
|
+
(violation) => !baseline.keys.has(baselineKey(violation))
|
|
1004
|
+
);
|
|
1005
|
+
const currentKeys = new Set(violations.map(baselineKey));
|
|
1006
|
+
staleBaselineKeys = [...baseline.keys].filter((key) => !currentKeys.has(key)).length;
|
|
1007
|
+
} else {
|
|
1008
|
+
warnings.push(
|
|
1009
|
+
configWarning(
|
|
1010
|
+
'BASELINE_NOT_FOUND',
|
|
1011
|
+
`Baseline file not found: ${baseline.fullPath}. Generate it with: ark-check --update-baseline`
|
|
1012
|
+
)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const ok = activeViolations.length === 0 && (!args.strictConfig || warnings.length === 0);
|
|
1018
|
+
|
|
736
1019
|
if (args.json) {
|
|
737
1020
|
console.log(JSON.stringify({
|
|
738
|
-
ok
|
|
739
|
-
violations,
|
|
1021
|
+
ok,
|
|
1022
|
+
violations: activeViolations,
|
|
1023
|
+
suppressedViolations: suppressed.length,
|
|
1024
|
+
staleBaselineKeys,
|
|
740
1025
|
warnings,
|
|
741
1026
|
}, null, 2));
|
|
742
|
-
} else
|
|
1027
|
+
} else {
|
|
743
1028
|
for (const warning of warnings) {
|
|
744
|
-
console.error(
|
|
1029
|
+
console.error(`${color.yellow('warning')} ${warning.ruleId} ${warning.message}`);
|
|
745
1030
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
} else if (args.strictConfig) {
|
|
749
|
-
console.error(`Ark check failed with ${warnings.length} config warning(s).`);
|
|
750
|
-
} else {
|
|
751
|
-
console.log(`Ark check passed with ${warnings.length} config warning(s).`);
|
|
1031
|
+
for (const violation of activeViolations) {
|
|
1032
|
+
printViolation(violation);
|
|
752
1033
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1034
|
+
|
|
1035
|
+
const baselineNote =
|
|
1036
|
+
suppressed.length > 0 ? ` (${suppressed.length} suppressed by baseline)` : '';
|
|
1037
|
+
if (staleBaselineKeys > 0) {
|
|
1038
|
+
console.error(
|
|
1039
|
+
color.dim(
|
|
1040
|
+
`${staleBaselineKeys} baseline entr(y/ies) no longer occur — tighten the ratchet with: ark-check --update-baseline`
|
|
1041
|
+
)
|
|
1042
|
+
);
|
|
756
1043
|
}
|
|
757
|
-
|
|
1044
|
+
if (activeViolations.length === 0) {
|
|
1045
|
+
if (warnings.length === 0) {
|
|
1046
|
+
console.log(`${color.green('✔')} Ark check passed.${baselineNote}`);
|
|
1047
|
+
} else if (args.strictConfig) {
|
|
1048
|
+
console.error(
|
|
1049
|
+
`${color.red('✖')} Ark check failed with ${warnings.length} config warning(s).${baselineNote}`
|
|
1050
|
+
);
|
|
1051
|
+
} else {
|
|
1052
|
+
console.log(
|
|
1053
|
+
`${color.green('✔')} Ark check passed with ${warnings.length} config warning(s).${baselineNote}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
758
1057
|
console.error(
|
|
759
|
-
`${
|
|
1058
|
+
`${color.red('✖')} ${activeViolations.length} violation(s).${baselineNote}`
|
|
760
1059
|
);
|
|
761
1060
|
}
|
|
762
1061
|
}
|
|
763
1062
|
|
|
764
|
-
process.exitCode =
|
|
765
|
-
violations.length === 0 && (!args.strictConfig || warnings.length === 0)
|
|
766
|
-
? 0
|
|
767
|
-
: 1;
|
|
1063
|
+
process.exitCode = ok ? 0 : 1;
|
|
768
1064
|
}
|
|
769
1065
|
|
|
770
1066
|
main().catch((error) => {
|
package/bin/ark-mcp.mjs
CHANGED
|
File without changes
|
package/bin/ark.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
|
|
8
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const arkCheck = path.join(here, 'ark-check.mjs');
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = {
|
|
13
|
+
command: argv[2],
|
|
14
|
+
root: process.cwd(),
|
|
15
|
+
yes: false,
|
|
16
|
+
force: false,
|
|
17
|
+
strict: true,
|
|
18
|
+
help: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let i = 3; i < argv.length; i += 1) {
|
|
22
|
+
const arg = argv[i];
|
|
23
|
+
if (arg === '--root') args.root = path.resolve(argv[++i]);
|
|
24
|
+
else if (arg === '--yes' || arg === '-y') args.yes = true;
|
|
25
|
+
else if (arg === '--force') args.force = true;
|
|
26
|
+
else if (arg === '--no-strict') args.strict = false;
|
|
27
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function usage() {
|
|
34
|
+
return `Usage:
|
|
35
|
+
ark init [--root <project>] [--yes] [--force] [--no-strict]
|
|
36
|
+
|
|
37
|
+
Commands:
|
|
38
|
+
init Configure Ark project enforcement with explicit prompts.
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
|
|
42
|
+
--force Allow generated files to overwrite existing files.
|
|
43
|
+
--no-strict Skip the final strict ark-check run.
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function runArkCheck(args, options = {}) {
|
|
48
|
+
const result = spawnSync(process.execPath, [arkCheck, ...args], {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
stdio: options.stdio ?? 'inherit',
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
});
|
|
53
|
+
return result.status ?? 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function askYesNo(rl, question, defaultYes = true) {
|
|
57
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
|
|
58
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
59
|
+
if (!answer) return defaultYes;
|
|
60
|
+
return answer === 'y' || answer === 'yes';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function init(args) {
|
|
64
|
+
const root = args.root;
|
|
65
|
+
const configPath = path.join(root, 'ark.config.json');
|
|
66
|
+
const rl = args.yes
|
|
67
|
+
? null
|
|
68
|
+
: readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
let shouldInit = !fs.existsSync(configPath);
|
|
72
|
+
if (fs.existsSync(configPath)) {
|
|
73
|
+
shouldInit = args.force
|
|
74
|
+
? true
|
|
75
|
+
: args.yes
|
|
76
|
+
? false
|
|
77
|
+
: await askYesNo(rl, 'ark.config.json already exists. Regenerate it?', false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (shouldInit) {
|
|
81
|
+
const initArgs = ['--root', root, '--init'];
|
|
82
|
+
if (args.force) initArgs.push('--force');
|
|
83
|
+
const status = runArkCheck(initArgs, { cwd: root });
|
|
84
|
+
if (status !== 0) return status;
|
|
85
|
+
} else {
|
|
86
|
+
console.log('Skipped ark.config.json generation.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const installGates = args.yes || await askYesNo(rl, 'Configure agent and CI gate templates?', true);
|
|
90
|
+
if (installGates) {
|
|
91
|
+
const gateArgs = ['--root', root, '--install-agent-gates'];
|
|
92
|
+
if (args.force) gateArgs.push('--force');
|
|
93
|
+
const status = runArkCheck(gateArgs, { cwd: root });
|
|
94
|
+
if (status !== 0) return status;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const runStrict =
|
|
98
|
+
args.strict && (args.yes || await askYesNo(rl, 'Run strict architecture check now?', true));
|
|
99
|
+
if (runStrict) {
|
|
100
|
+
return runArkCheck(
|
|
101
|
+
['--root', root, '--config', 'ark.config.json', '--strict-config'],
|
|
102
|
+
{ cwd: root }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('Ark init complete. Run `npx ark-check --root . --config ark.config.json --strict-config` before merging.');
|
|
107
|
+
return 0;
|
|
108
|
+
} finally {
|
|
109
|
+
rl?.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const args = parseArgs(process.argv);
|
|
115
|
+
if (args.help || !args.command) {
|
|
116
|
+
console.log(usage());
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args.command === 'init') {
|
|
121
|
+
return init(args);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.error(`Unknown command: ${args.command}`);
|
|
125
|
+
console.error(usage());
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
process.exitCode = await main();
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/version.ts
|
|
4
|
-
var version = "1.
|
|
4
|
+
var version = "1.2.0";
|
|
5
5
|
|
|
6
6
|
// src/kernel/intent/IntentRegistry.ts
|
|
7
7
|
var IntentRegistry = class {
|
|
@@ -1105,6 +1105,12 @@ function createEventBus(options) {
|
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
1107
1107
|
// src/kernel/event-contracts/EventContractRegistry.ts
|
|
1108
|
+
function formatStandardSchemaPath(path) {
|
|
1109
|
+
if (!path || path.length === 0) return void 0;
|
|
1110
|
+
return path.map(
|
|
1111
|
+
(segment) => typeof segment === "object" && segment !== null && "key" in segment ? String(segment.key) : String(segment)
|
|
1112
|
+
).join(".");
|
|
1113
|
+
}
|
|
1108
1114
|
function actualType(value) {
|
|
1109
1115
|
if (Array.isArray(value)) return "array";
|
|
1110
1116
|
if (value === null) return "object";
|
|
@@ -1249,6 +1255,25 @@ var EventContractRegistryImpl = class {
|
|
|
1249
1255
|
}
|
|
1250
1256
|
}
|
|
1251
1257
|
}
|
|
1258
|
+
if (contract.standardSchema) {
|
|
1259
|
+
const result = contract.standardSchema["~standard"].validate(event.payload);
|
|
1260
|
+
if (result instanceof Promise) {
|
|
1261
|
+
issues.push({
|
|
1262
|
+
intent: event.intent,
|
|
1263
|
+
version: contract.version,
|
|
1264
|
+
message: "Standard Schema validator returned a Promise; event contract validation is synchronous."
|
|
1265
|
+
});
|
|
1266
|
+
} else if (result.issues) {
|
|
1267
|
+
for (const issue of result.issues) {
|
|
1268
|
+
issues.push({
|
|
1269
|
+
intent: event.intent,
|
|
1270
|
+
version: contract.version,
|
|
1271
|
+
field: formatStandardSchemaPath(issue.path),
|
|
1272
|
+
message: issue.message
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1252
1277
|
return { ok: issues.length === 0, contract, issues };
|
|
1253
1278
|
}
|
|
1254
1279
|
clear() {
|