@aikdna/kdna-cli 0.21.0 → 0.22.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 +31 -96
- package/fixtures/v1-minimal/checksums.json +6 -0
- package/fixtures/v1-minimal/kdna.json +42 -0
- package/fixtures/v1-minimal/mimetype +1 -0
- package/fixtures/v1-minimal/payload.kdnab +31 -0
- package/package.json +10 -8
- package/schema/checksums.schema.json +43 -0
- package/schema/load-contract.schema.json +41 -0
- package/schema/manifest.schema.json +198 -0
- package/schema/payload-profile-v1.schema.json +70 -0
- package/src/agent.js +14 -65
- package/src/cli.js +72 -4
- package/src/cmds/anti-monolithic.js +192 -0
- package/src/cmds/domain.js +104 -0
- package/src/v1-cli.js +715 -0
- package/src/verify.js +18 -4
package/src/agent.js
CHANGED
|
@@ -16,17 +16,15 @@
|
|
|
16
16
|
* not treat as a fit decision; many false positives expected)
|
|
17
17
|
* The agent makes the final call using its own language understanding.
|
|
18
18
|
*
|
|
19
|
-
* kdna load <name|file.kdna> [--as=prompt
|
|
20
|
-
* Read the domain's
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* --as=json: raw JSON of Core + Patterns
|
|
25
|
-
* --as=raw: concatenated raw file contents
|
|
19
|
+
* kdna load <name|file.kdna> [--as=prompt]
|
|
20
|
+
* Read the domain's judgment and emit context suitable for agent
|
|
21
|
+
* system-prompt injection (axioms one-liners + stances +
|
|
22
|
+
* banned-terms + misunderstandings + self-checks).
|
|
23
|
+
* For raw inspection use: kdna dev decode <file.kdna> --reveal
|
|
26
24
|
*
|
|
27
25
|
* These commands are the supported interface between the kdna-loader
|
|
28
|
-
* skill and the KDNA file format. The skill should not
|
|
29
|
-
* directly.
|
|
26
|
+
* skill and the KDNA file format. The skill should not read KDNA
|
|
27
|
+
* internals directly.
|
|
30
28
|
*/
|
|
31
29
|
|
|
32
30
|
const fs = require('fs');
|
|
@@ -309,7 +307,7 @@ function cmdMatch(taskText, args = []) {
|
|
|
309
307
|
}
|
|
310
308
|
console.log('');
|
|
311
309
|
console.log(
|
|
312
|
-
'To
|
|
310
|
+
'To load a domain: kdna load <name|file.kdna>',
|
|
313
311
|
);
|
|
314
312
|
}
|
|
315
313
|
}
|
|
@@ -407,61 +405,12 @@ function cmdLoad(input, args = []) {
|
|
|
407
405
|
const core = container.core || {};
|
|
408
406
|
const pat = container.patterns || {};
|
|
409
407
|
|
|
410
|
-
// JSON format
|
|
411
|
-
if (format === 'json') {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
core,
|
|
417
|
-
patterns: pat,
|
|
418
|
-
trust: {
|
|
419
|
-
signature: isPlaceholder ? 'unsigned' : 'present',
|
|
420
|
-
risk_level: riskLevel,
|
|
421
|
-
deprecated: manifest.status === 'deprecated',
|
|
422
|
-
yanked: false,
|
|
423
|
-
warnings: loadWarnings,
|
|
424
|
-
asset_digest: asset.asset_digest || null,
|
|
425
|
-
content_digest: asset.content_digest || null,
|
|
426
|
-
license_id: licenseActivation?.license_id || null,
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
null,
|
|
430
|
-
2,
|
|
431
|
-
) + '\n',
|
|
432
|
-
);
|
|
433
|
-
recordTrace({
|
|
434
|
-
timestamp: new Date().toISOString(),
|
|
435
|
-
agent: detectAgent(),
|
|
436
|
-
domain: label,
|
|
437
|
-
format: 'json',
|
|
438
|
-
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
439
|
-
});
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Raw format
|
|
444
|
-
if (format === 'raw') {
|
|
445
|
-
for (const f of ['KDNA_Core.json', 'KDNA_Patterns.json']) {
|
|
446
|
-
const encrypted = encryptedEntries.includes(f);
|
|
447
|
-
const buf = encrypted
|
|
448
|
-
? Buffer.from(
|
|
449
|
-
JSON.stringify(container[f === 'KDNA_Core.json' ? 'core' : 'patterns'], null, 2),
|
|
450
|
-
)
|
|
451
|
-
: readContainerEntry(asset.asset_path, f);
|
|
452
|
-
if (buf) {
|
|
453
|
-
process.stdout.write(`\n=== ${f} ===\n`);
|
|
454
|
-
process.stdout.write(buf.toString('utf8'));
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
recordTrace({
|
|
458
|
-
timestamp: new Date().toISOString(),
|
|
459
|
-
agent: detectAgent(),
|
|
460
|
-
domain: label,
|
|
461
|
-
format: 'raw',
|
|
462
|
-
asset: traceAssetFields(asset, manifest, licenseActivation),
|
|
463
|
-
});
|
|
464
|
-
return;
|
|
408
|
+
// JSON format — removed from agent runtime
|
|
409
|
+
if (format === 'json' || format === 'raw') {
|
|
410
|
+
console.error(`ERR_RAW_LOAD_REMOVED: --as=${format} is not supported in agent runtime.`);
|
|
411
|
+
console.error('Use: kdna dev decode <asset.kdna> --reveal');
|
|
412
|
+
console.error('Agent consumption: kdna load @scope/name [--as=prompt]');
|
|
413
|
+
process.exit(2);
|
|
465
414
|
}
|
|
466
415
|
|
|
467
416
|
// Load profiles
|
package/src/cli.js
CHANGED
|
@@ -206,9 +206,26 @@ switch (cmd) {
|
|
|
206
206
|
if (sub === 'validate') {
|
|
207
207
|
const schemaFlag = args.includes('--schema');
|
|
208
208
|
const jsonFlag = args.includes('--json');
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
const antiMonolithicFlag = args.includes('--anti-monolithic');
|
|
210
|
+
const strictFlag = args.includes('--strict');
|
|
211
|
+
const target = args.filter(
|
|
212
|
+
(a, i) =>
|
|
213
|
+
i > 1 &&
|
|
214
|
+
a !== '--schema' &&
|
|
215
|
+
a !== '--json' &&
|
|
216
|
+
a !== '--anti-monolithic' &&
|
|
217
|
+
a !== '--strict',
|
|
218
|
+
)[0];
|
|
219
|
+
if (!target)
|
|
220
|
+
error(
|
|
221
|
+
'Usage: kdna dev validate <source-dir> [--schema] [--json] [--anti-monolithic] [--strict]',
|
|
222
|
+
);
|
|
223
|
+
if (antiMonolithicFlag) {
|
|
224
|
+
const { cmdValidateAntiMonolithic } = require('./cmds/domain');
|
|
225
|
+
cmdValidateAntiMonolithic(target, { json: jsonFlag, strict: strictFlag });
|
|
226
|
+
} else {
|
|
227
|
+
cmdValidate(target, schemaFlag, jsonFlag);
|
|
228
|
+
}
|
|
212
229
|
break;
|
|
213
230
|
}
|
|
214
231
|
if (sub === 'pack') {
|
|
@@ -260,6 +277,16 @@ switch (cmd) {
|
|
|
260
277
|
break;
|
|
261
278
|
}
|
|
262
279
|
case 'validate': {
|
|
280
|
+
const v1Target = args.filter((a) => !a.startsWith('--'))[1];
|
|
281
|
+
if (v1Target) {
|
|
282
|
+
const v1 = require('./v1-cli');
|
|
283
|
+
const abs = require('node:path').resolve(v1Target);
|
|
284
|
+
if (v1.isV1SourceDir(abs) || v1.detectContainerFormat(abs) === 'v1') {
|
|
285
|
+
const result = v1.validate(v1Target);
|
|
286
|
+
console.log(JSON.stringify(result, null, 2));
|
|
287
|
+
process.exit(result.overall_valid ? 0 : 1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
263
290
|
error(
|
|
264
291
|
'Directory validation is a dev-only operation. Use: kdna dev validate <source-dir>',
|
|
265
292
|
EXIT.INPUT_ERROR,
|
|
@@ -267,6 +294,23 @@ switch (cmd) {
|
|
|
267
294
|
break;
|
|
268
295
|
}
|
|
269
296
|
case 'pack': {
|
|
297
|
+
const v1Target = args.filter((a) => !a.startsWith('--'))[1];
|
|
298
|
+
if (v1Target) {
|
|
299
|
+
const v1 = require('./v1-cli');
|
|
300
|
+
const abs = require('node:path').resolve(v1Target);
|
|
301
|
+
if (v1.isV1SourceDir(abs)) {
|
|
302
|
+
const out = args.filter((a) => !a.startsWith('--'))[2];
|
|
303
|
+
if (!out) {
|
|
304
|
+
process.stderr.write('Usage: kdna pack <source-dir> <output.kdna>\n');
|
|
305
|
+
process.exit(2);
|
|
306
|
+
}
|
|
307
|
+
const r = v1.pack(v1Target, out);
|
|
308
|
+
process.stdout.write(
|
|
309
|
+
`Packed: ${r.outputPath}\nEntries: ${r.entries.length} (${r.entries.join(', ')})\n`,
|
|
310
|
+
);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
270
314
|
error(
|
|
271
315
|
'Directory packaging is a dev-only operation. Use: kdna dev pack <source-dir>',
|
|
272
316
|
EXIT.INPUT_ERROR,
|
|
@@ -274,6 +318,23 @@ switch (cmd) {
|
|
|
274
318
|
break;
|
|
275
319
|
}
|
|
276
320
|
case 'unpack': {
|
|
321
|
+
const v1Target = args.filter((a) => !a.startsWith('--'))[1];
|
|
322
|
+
if (v1Target) {
|
|
323
|
+
const v1 = require('./v1-cli');
|
|
324
|
+
const abs = require('node:path').resolve(v1Target);
|
|
325
|
+
if (v1.detectContainerFormat(abs) === 'v1') {
|
|
326
|
+
const out = args.filter((a) => !a.startsWith('--'))[2];
|
|
327
|
+
if (!out) {
|
|
328
|
+
process.stderr.write('Usage: kdna unpack <input.kdna> <output-dir>\n');
|
|
329
|
+
process.exit(2);
|
|
330
|
+
}
|
|
331
|
+
const r = v1.unpack(v1Target, out);
|
|
332
|
+
process.stdout.write(
|
|
333
|
+
`Unpacked: ${r.outputDir}\nEntries: ${r.entries.length} (${r.entries.join(', ')})\n`,
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
277
338
|
error(
|
|
278
339
|
'Unpacking exposes internal files and is dev-only. Use: kdna dev unpack <file.kdna>',
|
|
279
340
|
EXIT.INPUT_ERROR,
|
|
@@ -337,7 +398,14 @@ switch (cmd) {
|
|
|
337
398
|
}
|
|
338
399
|
case 'inspect': {
|
|
339
400
|
const target = args.filter((a) => !a.startsWith('--'))[1];
|
|
340
|
-
if (!target) error('Usage: kdna inspect <
|
|
401
|
+
if (!target) error('Usage: kdna inspect <path> [--json] [--locale zh-CN]');
|
|
402
|
+
const v1 = require('./v1-cli');
|
|
403
|
+
const abs = require('node:path').resolve(target);
|
|
404
|
+
if (v1.isV1SourceDir(abs) || v1.detectContainerFormat(abs) === 'v1') {
|
|
405
|
+
const out = v1.inspect(target);
|
|
406
|
+
console.log(JSON.stringify(out, null, 2));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
341
409
|
const localeIdx = args.indexOf('--locale');
|
|
342
410
|
const locale = localeIdx >= 0 ? args[localeIdx + 1] : null;
|
|
343
411
|
cmdInspect(target, args.includes('--json'), locale);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-Monolithic Domain lint for KDNA dev source workspaces.
|
|
3
|
+
*
|
|
4
|
+
* Implements the Anti-Monolithic Domain Principle (RFC-0013 §4).
|
|
5
|
+
* The principle is codified in SPEC §1.6; this file is the runtime
|
|
6
|
+
* check that enforces it.
|
|
7
|
+
*
|
|
8
|
+
* Default mode: WARN (does not block CI).
|
|
9
|
+
* Strict mode: ERROR (blocks CI / official preflight).
|
|
10
|
+
*
|
|
11
|
+
* Trigger conditions (all three must hold):
|
|
12
|
+
* 1. KDNA_Core.json.axioms.length > 6
|
|
13
|
+
* 2. KDNA_Core.json.frameworks.length >= 3
|
|
14
|
+
* 3. The source workspace either:
|
|
15
|
+
* (a) has no module_manifest.json, OR
|
|
16
|
+
* (b) has module_manifest.json but no domain-level
|
|
17
|
+
* decomposition_rationale field.
|
|
18
|
+
*
|
|
19
|
+
* If a trigger fires, the lint produces:
|
|
20
|
+
* - WARNING: domain appears monolithic; recommend split or
|
|
21
|
+
* add module_manifest.json + decomposition_rationale.
|
|
22
|
+
* - ERROR (--strict only): same content, with the strict banner.
|
|
23
|
+
*
|
|
24
|
+
* Optional auxiliary checks (always WARN, never ERROR):
|
|
25
|
+
* - module_manifest.json exists and references modules
|
|
26
|
+
* but has no sub_domain (or all modules are internal_module)
|
|
27
|
+
* and the domain has multiple distinct judgment questions.
|
|
28
|
+
* - module_manifest.json exists with decomposition_rationale
|
|
29
|
+
* but the rationale text is shorter than 30 characters
|
|
30
|
+
* (likely placeholder).
|
|
31
|
+
*
|
|
32
|
+
* Boundary rules per RFC-0013 §3.3:
|
|
33
|
+
* - internal_module: in-domain, not independently loadable
|
|
34
|
+
* - sub_domain: independently loadable as its own .kdna
|
|
35
|
+
* - reference: pure data, not judgment
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
const AXIOM_THRESHOLD = 6; // SPEC §5.2 says "between 2 and 6 axioms"
|
|
42
|
+
const FRAMEWORK_THRESHOLD = 3; // RFC-0013 §4 companion rule
|
|
43
|
+
const RATIONALE_MIN_LENGTH = 30;
|
|
44
|
+
|
|
45
|
+
function readJsonSafe(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(filePath)) return null;
|
|
48
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return { __readError: e.message, __path: filePath };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run the Anti-Monolithic check on a dev source directory.
|
|
56
|
+
* Returns { triggered, warnings, errors, summary }.
|
|
57
|
+
*/
|
|
58
|
+
function runAntiMonolithicCheck(dir, opts = {}) {
|
|
59
|
+
const strict = !!opts.strict;
|
|
60
|
+
const abs = path.resolve(dir);
|
|
61
|
+
const result = {
|
|
62
|
+
path: abs,
|
|
63
|
+
triggered: false,
|
|
64
|
+
warnings: [],
|
|
65
|
+
errors: [],
|
|
66
|
+
summary: {
|
|
67
|
+
axiom_count: 0,
|
|
68
|
+
framework_count: 0,
|
|
69
|
+
has_module_manifest: false,
|
|
70
|
+
has_decomposition_rationale: false,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const corePath = path.join(abs, 'KDNA_Core.json');
|
|
75
|
+
const core = readJsonSafe(corePath);
|
|
76
|
+
if (!core || core.__readError) {
|
|
77
|
+
result.errors.push(
|
|
78
|
+
`Cannot read KDNA_Core.json at ${corePath}: ${(core && core.__readError) || 'file not found'}`,
|
|
79
|
+
);
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const axioms = Array.isArray(core.axioms) ? core.axioms : [];
|
|
84
|
+
const frameworks = Array.isArray(core.frameworks) ? core.frameworks : [];
|
|
85
|
+
result.summary.axiom_count = axioms.length;
|
|
86
|
+
result.summary.framework_count = frameworks.length;
|
|
87
|
+
|
|
88
|
+
const axiomOver = axioms.length > AXIOM_THRESHOLD;
|
|
89
|
+
const frameworkOver = frameworks.length >= FRAMEWORK_THRESHOLD;
|
|
90
|
+
if (!axiomOver || !frameworkOver) {
|
|
91
|
+
return result; // Below threshold: domain is not monolithic by the rule.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Both thresholds exceeded: now check module_manifest.json.
|
|
95
|
+
const manifestPath = path.join(abs, 'module_manifest.json');
|
|
96
|
+
const manifest = readJsonSafe(manifestPath);
|
|
97
|
+
const manifestExists = manifest !== null && !manifest.__readError;
|
|
98
|
+
result.summary.has_module_manifest = manifestExists;
|
|
99
|
+
|
|
100
|
+
let hasRationale = false;
|
|
101
|
+
if (manifestExists) {
|
|
102
|
+
const rationale = (manifest && manifest.decomposition_rationale) || '';
|
|
103
|
+
hasRationale = typeof rationale === 'string' && rationale.trim().length >= RATIONALE_MIN_LENGTH;
|
|
104
|
+
result.summary.has_decomposition_rationale = hasRationale;
|
|
105
|
+
|
|
106
|
+
if (rationale && rationale.trim().length > 0 && rationale.trim().length < RATIONALE_MIN_LENGTH) {
|
|
107
|
+
result.warnings.push(
|
|
108
|
+
`module_manifest.json: decomposition_rationale is only ${rationale.trim().length} chars; ` +
|
|
109
|
+
`minimum is ${RATIONALE_MIN_LENGTH} chars to count as a real sign-off.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!manifestExists) {
|
|
115
|
+
result.triggered = true;
|
|
116
|
+
const msg =
|
|
117
|
+
`Anti-Monolithic Domain Principle: KDNA_Core.json has ` +
|
|
118
|
+
`${axioms.length} axioms (>${AXIOM_THRESHOLD}) and ${frameworks.length} frameworks (>=${FRAMEWORK_THRESHOLD}). ` +
|
|
119
|
+
`No module_manifest.json found. Either split into sub-domains and compose via cluster, ` +
|
|
120
|
+
`or create a module_manifest.json with a decomposition_rationale (>=${RATIONALE_MIN_LENGTH} chars) ` +
|
|
121
|
+
`and a maintainer sign-off. See SPEC §1.6 and RFC-0013 §4.`;
|
|
122
|
+
if (strict) {
|
|
123
|
+
result.errors.push(msg);
|
|
124
|
+
} else {
|
|
125
|
+
result.warnings.push(msg);
|
|
126
|
+
}
|
|
127
|
+
} else if (!hasRationale) {
|
|
128
|
+
result.triggered = true;
|
|
129
|
+
const msg =
|
|
130
|
+
`Anti-Monolithic Domain Principle: KDNA_Core.json has ` +
|
|
131
|
+
`${axioms.length} axioms (>${AXIOM_THRESHOLD}) and ${frameworks.length} frameworks (>=${FRAMEWORK_THRESHOLD}). ` +
|
|
132
|
+
`module_manifest.json exists but decomposition_rationale is missing or too short. ` +
|
|
133
|
+
`Add a substantive rationale (>=${RATIONALE_MIN_LENGTH} chars) to record the maintainer sign-off. ` +
|
|
134
|
+
`See SPEC §1.6 and RFC-0013 §4.`;
|
|
135
|
+
if (strict) {
|
|
136
|
+
result.errors.push(msg);
|
|
137
|
+
} else {
|
|
138
|
+
result.warnings.push(msg);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// Thresholds exceeded but maintainer sign-off is present:
|
|
142
|
+
// emit a soft warning so the maintainer rationale is visible in
|
|
143
|
+
// CI logs even though the rule is satisfied.
|
|
144
|
+
result.warnings.push(
|
|
145
|
+
`Anti-Monolithic Domain Principle: KDNA_Core.json has ${axioms.length} axioms and ` +
|
|
146
|
+
`${frameworks.length} frameworks. Maintainer sign-off recorded in module_manifest.json ` +
|
|
147
|
+
`(decomposition_rationale). Review periodically that the rationale still holds.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Print the check result to stdout. Returns the recommended process
|
|
156
|
+
* exit code: 0 if no errors (warnings OK), 1 if errors.
|
|
157
|
+
*/
|
|
158
|
+
function printAndExit(result, opts = {}) {
|
|
159
|
+
const jsonMode = !!opts.json;
|
|
160
|
+
if (jsonMode) {
|
|
161
|
+
console.log(JSON.stringify(result, null, 2));
|
|
162
|
+
return result.errors.length ? 1 : 0;
|
|
163
|
+
}
|
|
164
|
+
if (result.errors.length) {
|
|
165
|
+
console.error('Errors:');
|
|
166
|
+
result.errors.forEach((e) => console.error(` - ${e}`));
|
|
167
|
+
}
|
|
168
|
+
if (result.warnings.length) {
|
|
169
|
+
console.log('Warnings:');
|
|
170
|
+
result.warnings.forEach((w) => console.log(` - ${w}`));
|
|
171
|
+
}
|
|
172
|
+
if (!result.errors.length && !result.warnings.length) {
|
|
173
|
+
console.log(
|
|
174
|
+
`✓ Anti-Monolithic check passed: ${result.path} ` +
|
|
175
|
+
`(${result.summary.axiom_count} axioms, ${result.summary.framework_count} frameworks)`,
|
|
176
|
+
);
|
|
177
|
+
} else if (!result.errors.length) {
|
|
178
|
+
console.log(
|
|
179
|
+
`✓ Anti-Monolithic check: ${result.warnings.length} warning(s), 0 error(s) ` +
|
|
180
|
+
`(passing; --strict to upgrade warnings to errors)`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return result.errors.length ? 1 : 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
runAntiMonolithicCheck,
|
|
188
|
+
printAndExit,
|
|
189
|
+
AXIOM_THRESHOLD,
|
|
190
|
+
FRAMEWORK_THRESHOLD,
|
|
191
|
+
RATIONALE_MIN_LENGTH,
|
|
192
|
+
};
|
package/src/cmds/domain.js
CHANGED
|
@@ -200,6 +200,109 @@ function cmdValidate(dir, schemaOnly, jsonMode = false) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Anti-Monolithic Domain lint entry point.
|
|
205
|
+
*
|
|
206
|
+
* Runs schema validation (same as `kdna dev validate`) and then runs
|
|
207
|
+
* the Anti-Monolithic Domain Principle check (RFC-0013 §4 / SPEC §1.6)
|
|
208
|
+
* on top. The Anti-Monolithic check produces warnings by default and
|
|
209
|
+
* errors under `--strict`.
|
|
210
|
+
*
|
|
211
|
+
* Exits with non-zero only on schema errors or (under --strict) on
|
|
212
|
+
* Anti-Monolithic trigger.
|
|
213
|
+
*/
|
|
214
|
+
function cmdValidateAntiMonolithic(dir, opts = {}) {
|
|
215
|
+
const jsonMode = !!opts.json;
|
|
216
|
+
const strict = !!opts.strict;
|
|
217
|
+
|
|
218
|
+
// First, run the standard schema validation but capture its result
|
|
219
|
+
// rather than letting it process.exit(0).
|
|
220
|
+
// We achieve this by re-using the underlying helpers.
|
|
221
|
+
const abs = path.resolve(dir);
|
|
222
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
223
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const SCHEMA_DIR = path.join(
|
|
227
|
+
path.dirname(require.resolve('@aikdna/kdna-core/package.json')),
|
|
228
|
+
'schema',
|
|
229
|
+
);
|
|
230
|
+
const { schemaMap, loadedSchemas, missingSchemas } = loadSchemaMap(SCHEMA_DIR);
|
|
231
|
+
|
|
232
|
+
const schemaResult = isClusterDirectory(abs)
|
|
233
|
+
? validateClusterDirectory(abs, schemaMap, false)
|
|
234
|
+
: validateDomainDirectory(abs, schemaMap, false);
|
|
235
|
+
|
|
236
|
+
// Run Anti-Monolithic check on the top-level (or first sub-) directory.
|
|
237
|
+
// For clusters, run on each domain dir and aggregate.
|
|
238
|
+
const amResults = [];
|
|
239
|
+
if (schemaResult.cluster && Array.isArray(schemaResult.domains)) {
|
|
240
|
+
for (const d of schemaResult.domains) {
|
|
241
|
+
amResults.push(
|
|
242
|
+
runAntiMonolithicCheckOnCore(d.path || abs, { strict }),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
amResults.push(runAntiMonolithicCheckOnCore(abs, { strict }));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Aggregate.
|
|
250
|
+
const allErrors = [...schemaResult.errors];
|
|
251
|
+
const allWarnings = [...schemaResult.warnings];
|
|
252
|
+
for (const r of amResults) {
|
|
253
|
+
allErrors.push(...r.errors);
|
|
254
|
+
allWarnings.push(...r.warnings);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (jsonMode) {
|
|
258
|
+
console.log(
|
|
259
|
+
JSON.stringify(
|
|
260
|
+
{
|
|
261
|
+
path: abs,
|
|
262
|
+
schema_validation: {
|
|
263
|
+
valid: schemaResult.errors.length === 0,
|
|
264
|
+
errors: schemaResult.errors,
|
|
265
|
+
warnings: schemaResult.warnings,
|
|
266
|
+
cluster: !!schemaResult.cluster,
|
|
267
|
+
files: schemaResult.files,
|
|
268
|
+
domains: schemaResult.domains,
|
|
269
|
+
schemas_loaded: loadedSchemas.length,
|
|
270
|
+
missing_schemas: missingSchemas,
|
|
271
|
+
},
|
|
272
|
+
anti_monolithic: amResults,
|
|
273
|
+
},
|
|
274
|
+
null,
|
|
275
|
+
2,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
process.exit(allErrors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (allErrors.length) {
|
|
282
|
+
console.error('Errors:');
|
|
283
|
+
allErrors.forEach((e) => console.error(` - ${e}`));
|
|
284
|
+
}
|
|
285
|
+
if (allWarnings.length) {
|
|
286
|
+
console.log('Warnings:');
|
|
287
|
+
allWarnings.forEach((w) => console.log(` - ${w}`));
|
|
288
|
+
}
|
|
289
|
+
if (allErrors.length === 0) {
|
|
290
|
+
if (amResults.some((r) => r.triggered)) {
|
|
291
|
+
console.log(
|
|
292
|
+
`✓ Schema valid; Anti-Monolithic triggered (${amResults.filter((r) => r.triggered).length}/${amResults.length} domain(s))`,
|
|
293
|
+
);
|
|
294
|
+
} else {
|
|
295
|
+
console.log(`✓ Schema valid; Anti-Monolithic check passed (${amResults.length} domain(s))`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
process.exit(allErrors.length ? EXIT.VALIDATION_FAILED : EXIT.OK);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function runAntiMonolithicCheckOnCore(dir, opts) {
|
|
302
|
+
const { runAntiMonolithicCheck } = require('./anti-monolithic');
|
|
303
|
+
return runAntiMonolithicCheck(dir, opts);
|
|
304
|
+
}
|
|
305
|
+
|
|
203
306
|
// ─── Pack / Unpack (.kdna ZIP container) ──────────────────────────────────
|
|
204
307
|
|
|
205
308
|
function cmdPack(dir, outputDir) {
|
|
@@ -918,6 +1021,7 @@ function cmdCard(dir, jsonMode = false, locale = null, options = {}) {
|
|
|
918
1021
|
|
|
919
1022
|
module.exports = {
|
|
920
1023
|
cmdValidate,
|
|
1024
|
+
cmdValidateAntiMonolithic,
|
|
921
1025
|
cmdPack,
|
|
922
1026
|
cmdUnpack,
|
|
923
1027
|
cmdInspect,
|