@aikdna/kdna-cli 0.9.0 → 0.12.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/src/verify.js CHANGED
@@ -7,15 +7,19 @@
7
7
  *
8
8
  * No flag = run all three.
9
9
  *
10
- * Exit codes:
10
+ * Exit codes (semantic, from cmds/_common.js):
11
11
  * 0 all checks passed (warnings allowed)
12
- * 1 one or more layers failed
12
+ * 1 VALIDATION_FAILED structure layer failed
13
+ * 2 INPUT_ERROR — invalid name or not installed
14
+ * 3 TRUST_FAILED — trust layer failed
15
+ * 4 JUDGMENT_QUALITY_FAILED — judgment layer failed
13
16
  */
14
17
 
15
18
  const fs = require('fs');
16
19
  const path = require('path');
17
20
  const crypto = require('crypto');
18
21
  const { RegistryResolver, parseName } = require('./registry');
22
+ const { EXIT } = require('./cmds/_common');
19
23
 
20
24
  const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
21
25
  const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
@@ -213,7 +217,7 @@ function checkJudgment(destDir) {
213
217
  const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
214
218
  const manifest = readJson(path.join(destDir, 'kdna.json'));
215
219
 
216
- // 1. Boundary declaration in README
220
+ // 1. Boundary declaration in README (REQUIRED)
217
221
  // Either classic "## Scope" + "## Out of Scope" pair,
218
222
  // OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
219
223
  const readmePath = path.join(destDir, 'README.md');
@@ -234,9 +238,12 @@ function checkJudgment(destDir) {
234
238
  if ((hasScope && hasOutOfScope) || hasFourQuestions) {
235
239
  bump(2, 2, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
236
240
  } else if (hasScope || hasOutOfScope) {
237
- bump(2, 1, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
241
+ score.max += 2;
242
+ score.total += 1;
243
+ issues.push({ severity: 'warn', msg: 'partial: README boundary declaration incomplete (missing Scope or Out-of-Scope section)' });
238
244
  } else {
239
- bump(2, 0, 'README declares boundary (Scope+Out-of-Scope, or v2.1 Four Questions)');
245
+ score.max += 2;
246
+ issues.push({ severity: 'error', msg: 'README missing boundary declaration: require ## Scope + ## Out of Scope (or v2.1 Four Questions)' });
240
247
  }
241
248
 
242
249
  // 2. v2.1 axiom governance fields
@@ -305,23 +312,31 @@ function checkJudgment(destDir) {
305
312
  issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
306
313
  }
307
314
 
308
- // 5. eval cases present
315
+ // 5. eval cases present (REQUIRED: ≥4 cases)
309
316
  const evalDir = path.join(destDir, 'evals');
310
317
  if (fs.existsSync(evalDir)) {
311
318
  const files = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
312
- if (files.length >= 4) bump(2, 2, `evals/ directory has ${files.length} case files`);
313
- else if (files.length > 0)
314
- bump(2, 1, `evals/ has ${files.length} files (recommend ≥4: core/boundary/failure/excluded)`);
315
- else bump(2, 0, 'evals/ has case files');
319
+ if (files.length >= 4) {
320
+ bump(2, 2, `evals/ directory has ${files.length} case files`);
321
+ } else if (files.length > 0) {
322
+ score.max += 2;
323
+ score.total += 1;
324
+ issues.push({ severity: 'warn', msg: `evals/ has only ${files.length} files (require ≥4: core/boundary/failure/excluded)` });
325
+ } else {
326
+ score.max += 2;
327
+ issues.push({ severity: 'error', msg: 'evals/ directory exists but contains no case files' });
328
+ }
316
329
  } else {
317
- bump(2, 0, 'evals/ directory present');
330
+ score.max += 2;
331
+ issues.push({ severity: 'error', msg: 'evals/ directory missing: require ≥4 evaluation cases' });
318
332
  }
319
333
 
320
- // 6. judgment_version manifest field
334
+ // 6. judgment_version manifest field (REQUIRED)
321
335
  if (manifest?.judgment_version) {
322
336
  bump(1, 1, `judgment_version: ${manifest.judgment_version}`);
323
337
  } else {
324
- bump(1, 0, 'kdna.json has judgment_version');
338
+ score.max += 1;
339
+ issues.push({ severity: 'error', msg: 'kdna.json missing required field: judgment_version' });
325
340
  }
326
341
 
327
342
  return { layer: 'judgment', issues, passed, score };
@@ -355,6 +370,8 @@ function renderLayer(result) {
355
370
  // ─── Main ──────────────────────────────────────────────────────────────
356
371
 
357
372
  function cmdVerify(input, args = []) {
373
+ const jsonMode = args.includes('--json');
374
+
358
375
  const want = {
359
376
  structure: args.includes('--structure'),
360
377
  trust: args.includes('--trust'),
@@ -366,14 +383,22 @@ function cmdVerify(input, args = []) {
366
383
  // Resolve name → installed path + scope/entry
367
384
  const parsed = parseName(input);
368
385
  if (!parsed) {
369
- console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
370
- process.exit(2);
386
+ if (jsonMode) {
387
+ console.log(JSON.stringify({ name: input, ok: false, error: `Invalid name "${input}". Use @scope/name or bare name.` }));
388
+ } else {
389
+ console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
390
+ }
391
+ process.exit(EXIT.INPUT_ERROR);
371
392
  }
372
393
 
373
394
  const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
374
395
  if (!fs.existsSync(destDir)) {
375
- console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
376
- process.exit(2);
396
+ if (jsonMode) {
397
+ console.log(JSON.stringify({ name: parsed.full, ok: false, error: `${parsed.full} is not installed. Run: kdna install ${input}` }));
398
+ } else {
399
+ console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
400
+ }
401
+ process.exit(EXIT.INPUT_ERROR);
377
402
  }
378
403
 
379
404
  let scope = null,
@@ -385,20 +410,55 @@ function cmdVerify(input, args = []) {
385
410
  scope = r.scope;
386
411
  entry = r.entry;
387
412
  } catch (e) {
388
- console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
413
+ if (!jsonMode) console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
389
414
  }
390
415
  }
391
416
 
392
- console.log('═'.repeat(64));
393
- console.log(` Verify ${parsed.full}`);
394
- console.log(` Path: ${destDir}`);
395
- console.log('═'.repeat(64));
396
-
397
417
  const results = [];
398
418
  if (want.structure) results.push(checkStructure(destDir));
399
419
  if (want.trust) results.push(checkTrust(destDir, scope, entry));
400
420
  if (want.judgment) results.push(checkJudgment(destDir));
401
421
 
422
+ // ── JSON output ──────────────────────────────────────────────────────
423
+ if (jsonMode) {
424
+ const layers = {};
425
+ for (const r of results) {
426
+ layers[r.layer] = {
427
+ passed: r.passed,
428
+ errors: r.issues.filter((i) => i.severity === 'error').map((i) => i.msg),
429
+ warnings: r.issues.filter((i) => i.severity === 'warn').map((i) => i.msg),
430
+ };
431
+ if (r.score) layers[r.layer].score = r.score;
432
+ }
433
+
434
+ const structureResult = results.find((r) => r.layer === 'structure');
435
+ const trustResult = results.find((r) => r.layer === 'trust');
436
+ const judgmentResult = results.find((r) => r.layer === 'judgment');
437
+
438
+ let exitCode = EXIT.OK;
439
+ if (structureResult && structureResult.issues.some((i) => i.severity === 'error')) {
440
+ exitCode = EXIT.VALIDATION_FAILED;
441
+ } else if (trustResult && trustResult.issues.some((i) => i.severity === 'error')) {
442
+ exitCode = EXIT.TRUST_FAILED;
443
+ } else if (judgmentResult && judgmentResult.issues.some((i) => i.severity === 'error')) {
444
+ exitCode = EXIT.JUDGMENT_QUALITY_FAILED;
445
+ }
446
+
447
+ console.log(JSON.stringify({
448
+ name: parsed.full,
449
+ path: destDir,
450
+ layers,
451
+ ok: exitCode === EXIT.OK,
452
+ }, null, 2));
453
+ process.exit(exitCode);
454
+ }
455
+
456
+ // ── Text output (default) ────────────────────────────────────────────
457
+ console.log('═'.repeat(64));
458
+ console.log(` Verify ${parsed.full}`);
459
+ console.log(` Path: ${destDir}`);
460
+ console.log('═'.repeat(64));
461
+
402
462
  for (const r of results) renderLayer(r);
403
463
 
404
464
  const totalErrors = results.reduce(
@@ -417,7 +477,21 @@ function cmdVerify(input, args = []) {
417
477
  }
418
478
  console.log('═'.repeat(64));
419
479
 
420
- process.exit(totalErrors === 0 ? 0 : 1);
480
+ // Semantic exit codes for text mode
481
+ const structureResult = results.find((r) => r.layer === 'structure');
482
+ const trustResult = results.find((r) => r.layer === 'trust');
483
+ const judgmentResult = results.find((r) => r.layer === 'judgment');
484
+
485
+ let exitCode = EXIT.OK;
486
+ if (structureResult && structureResult.issues.some((i) => i.severity === 'error')) {
487
+ exitCode = EXIT.VALIDATION_FAILED;
488
+ } else if (trustResult && trustResult.issues.some((i) => i.severity === 'error')) {
489
+ exitCode = EXIT.TRUST_FAILED;
490
+ } else if (judgmentResult && judgmentResult.issues.some((i) => i.severity === 'error')) {
491
+ exitCode = EXIT.JUDGMENT_QUALITY_FAILED;
492
+ }
493
+
494
+ process.exit(exitCode);
421
495
  }
422
496
 
423
497
  module.exports = { cmdVerify, checkStructure, checkTrust, checkJudgment };
package/src/version.js CHANGED
@@ -6,10 +6,11 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const { EXIT } = require('./cmds/_common');
9
10
 
10
- function error(msg) {
11
+ function error(msg, code = EXIT.VALIDATION_FAILED) {
11
12
  console.error(`Error: ${msg}`);
12
- process.exit(1);
13
+ process.exit(code);
13
14
  }
14
15
 
15
16
  function readJson(filePath) {
@@ -109,4 +110,110 @@ function cmdVersionBump(level, domainPath) {
109
110
  console.log(`Done. Version: ${oldVersion} → ${newVersion}`);
110
111
  }
111
112
 
112
- module.exports = { cmdVersionBump };
113
+ /**
114
+ * kdna version bump --suggest [path]
115
+ * Suggest version bump based on judgment changes detected by kdna diff.
116
+ * Compares installed vs registry-current and suggests patch/minor/major.
117
+ */
118
+ function cmdVersionSuggest(domainPath = '.', args = []) {
119
+ const jsonMode = args.includes('--json');
120
+ const abs = path.resolve(domainPath);
121
+
122
+ const manifest = readJson(path.join(abs, 'kdna.json'));
123
+ if (!manifest) {
124
+ if (jsonMode) {
125
+ console.log(JSON.stringify({ error: 'No kdna.json found', suggestion: 'none' }));
126
+ process.exit(EXIT.OK);
127
+ }
128
+ console.log('No kdna.json found in current directory. Cannot suggest version bump.');
129
+ process.exit(EXIT.OK);
130
+ }
131
+
132
+ const currentVersion = manifest.version;
133
+ if (!currentVersion) {
134
+ if (jsonMode) {
135
+ console.log(JSON.stringify({ error: 'No version field', suggestion: 'none' }));
136
+ process.exit(EXIT.OK);
137
+ }
138
+ console.log('No version field in kdna.json.');
139
+ process.exit(EXIT.OK);
140
+ }
141
+
142
+ // Rules for suggesting:
143
+ // - If no previous version to diff against, suggest 'none'
144
+ // - Check for judgment_version changes
145
+ // - Check for axiom/ontology/misunderstanding changes
146
+
147
+ const changes = detectChanges(abs);
148
+
149
+ if (jsonMode) {
150
+ console.log(JSON.stringify({
151
+ current_version: currentVersion,
152
+ suggested_bump: changes.suggestion,
153
+ reasons: changes.reasons,
154
+ change_summary: changes.summary,
155
+ }, null, 2));
156
+ return;
157
+ }
158
+
159
+ console.log(`Current version: ${currentVersion}`);
160
+ console.log(`Suggested bump: ${changes.suggestion}`);
161
+ console.log('');
162
+ if (changes.reasons.length) {
163
+ console.log('Reasons:');
164
+ changes.reasons.forEach((r) => console.log(` - ${r}`));
165
+ }
166
+ if (changes.suggestion !== 'none') {
167
+ console.log('');
168
+ console.log(`Run: kdna version bump ${changes.suggestion} ${domainPath}`);
169
+ }
170
+ }
171
+
172
+ function detectChanges(domainPath) {
173
+ const reasons = [];
174
+ let axiomChanges = 0;
175
+ const ontologyChanges = 0;
176
+ const misunderstandingChanges = 0;
177
+
178
+ // Simple heuristic: count content vs previous git state
179
+ // For now, use a heuristic based on file modification
180
+ const core = readJson(path.join(domainPath, 'KDNA_Core.json'));
181
+
182
+ // Check if evals/ dir has recent changes
183
+ const evalsDir = path.join(domainPath, 'evals');
184
+ if (fs.existsSync(evalsDir)) {
185
+ reasons.push('evals/ directory present');
186
+ }
187
+
188
+ // Check for judgment_version in manifest
189
+ const manifest = readJson(path.join(domainPath, 'kdna.json'));
190
+ if (manifest?.judgment_version) {
191
+ reasons.push(`judgment_version: ${manifest.judgment_version}`);
192
+ }
193
+
194
+ // Count axioms with applies_when (v2.1 governance) vs without
195
+ if (core?.axioms) {
196
+ const total = core.axioms.length;
197
+ const governed = core.axioms.filter((a) => a.applies_when?.length && a.does_not_apply_when?.length).length;
198
+ if (governed < total) {
199
+ axiomChanges = total - governed;
200
+ reasons.push(`${axiomChanges} axioms missing v2.1 governance fields`);
201
+ }
202
+ }
203
+
204
+ let suggestion = 'none';
205
+ if (axiomChanges > 0) suggestion = 'patch';
206
+ if (axiomChanges >= 3) suggestion = 'minor';
207
+
208
+ return {
209
+ suggestion,
210
+ reasons,
211
+ summary: {
212
+ axiom_changes: axiomChanges,
213
+ ontology_changes: ontologyChanges,
214
+ misunderstanding_changes: misunderstandingChanges,
215
+ },
216
+ };
217
+ }
218
+
219
+ module.exports = { cmdVersionBump, cmdVersionSuggest };