@aikdna/kdna-studio-cli 0.8.8 → 0.8.9

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.
Files changed (2) hide show
  1. package/bin/kdna-studio.js +222 -6
  2. package/package.json +1 -1
@@ -83,6 +83,8 @@ Create (three entry paths):
83
83
 
84
84
  Migrate (dev source or Studio project → canonical .kdna in one command):
85
85
  kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--passphrase <pw>|--passphrase-stdin]
86
+ kdna-studio migrate <source-dir> --check --name <@scope/name> # pre-flight: report which fields would block the export without writing the .kdna
87
+ kdna-studio audit-locks <project> [--type axiom|risk|stance|...] [--json] # list cards with missing Human Lock fields (per-card-type, per-field)
86
88
 
87
89
  Authoring:
88
90
  kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
@@ -114,6 +116,7 @@ Distillation:
114
116
  kdna-studio candidate override <project> <candidate-id> # override scope gate
115
117
  kdna-studio candidate promote <project> # promote accepted+scope_fit → cards
116
118
  kdna-studio report <project>
119
+ kdna-studio audit-locks <project> # list all axioms/risk/stance missing Human Lock fields
117
120
 
118
121
  Project may be a directory containing studio.project.json or a project JSON file.`);
119
122
  }
@@ -1493,15 +1496,44 @@ function cmdCard(args) {
1493
1496
  if (sub === 'update') {
1494
1497
  const projectInput = args[1];
1495
1498
  const cardId = args[2];
1496
- if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value');
1499
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value OR kdna-studio card update <project> <card-id> --from-file <path.json>');
1497
1500
  const { projectPath, project } = readProject(projectInput);
1498
1501
  const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1499
1502
  if (idx < 0) fail(`Card not found: ${cardId}`);
1500
1503
  const card = project.cards[idx];
1501
1504
  if (card.locked) fail(`Cannot update locked card: ${cardId}. Unlock first with card unlock.`);
1502
- const updates = parseFields(args.slice(3));
1503
- for (const [k, v] of Object.entries(updates)) {
1504
- if (card.fields) card.fields[k] = v;
1505
+ // Bug (UX pro-20 migration): prior version only accepted
1506
+ // --field key=value, so an author migrating 20 source-tree
1507
+ // assets had to run one CLI invocation per field. With
1508
+ // confidence + evidence_type as 8th / 9th required fields on
1509
+ // axiom, a 99-axiom asset became 99 * 2 = 198 invocations.
1510
+ // The fix accepts --from-file <path.json> so an author can
1511
+ // write a single JSON file per card (or per project) and
1512
+ // apply it in one call. JSON shape: {field: value, ...}.
1513
+ // kdna-studio card update . ax_xxx --from-file ./fills/ax_xxx.json
1514
+ // For batched updates across many cards, see the
1515
+ // --from-file-projects variant below, which reads a list
1516
+ // of {id, fields} records.
1517
+ const fromFileIdx = args.indexOf('--from-file');
1518
+ if (fromFileIdx >= 0) {
1519
+ const filePath = args[fromFileIdx + 1];
1520
+ if (!filePath) fail('--from-file requires a path');
1521
+ if (!fs.existsSync(filePath)) fail(`--from-file: file not found: ${filePath}`);
1522
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1523
+ if (typeof data !== 'object' || Array.isArray(data) || data === null) {
1524
+ fail(`--from-file: expected a JSON object mapping field names to values`);
1525
+ }
1526
+ for (const [k, v] of Object.entries(data)) {
1527
+ if (k === 'id' || k === 'type' || k === 'status' || k === 'locked') {
1528
+ fail(`--from-file: cannot edit reserved field "${k}"`);
1529
+ }
1530
+ if (card.fields) card.fields[k] = v;
1531
+ }
1532
+ } else {
1533
+ const updates = parseFields(args.slice(3));
1534
+ for (const [k, v] of Object.entries(updates)) {
1535
+ if (card.fields) card.fields[k] = v;
1536
+ }
1505
1537
  }
1506
1538
  project.cards[idx] = card;
1507
1539
  writeProject(projectPath, project);
@@ -1650,14 +1682,32 @@ function cmdLock(args) {
1650
1682
  }
1651
1683
 
1652
1684
  function cmdMigrate(args) {
1685
+ // Bug (UX pro-20 migration): prior version of `migrate` always
1686
+ // attempted the full pipeline — read source folder, import to
1687
+ // Studio project, lock all cards, then export. An author migrating
1688
+ // 20 source-tree assets had no way to know in advance whether
1689
+ // the assets would pass the v1.7.2+ Human Lock gate. The fix
1690
+ // adds `--check` which runs the import + the critical-missing
1691
+ // check + the lock-gate check, then prints a report and exits
1692
+ // without writing the .kdna file. Pair with `audit-locks` for
1693
+ // the full pre-migration report.
1694
+ //
1695
+ // Usage:
1696
+ // kdna-studio migrate <source-dir> --check --name <@scope/name>
1697
+ //
1698
+ // Exit codes:
1699
+ // 0 — would succeed
1700
+ // 2 — INPUT_ERROR (missing args)
1701
+ // 4 — would fail (Human Lock gate or critical axiom field)
1702
+ const checkOnly = args.includes('--check');
1653
1703
  const sourceDir = args[0];
1654
1704
  const out = option(args, '--out');
1655
1705
  const by = option(args, '--by');
1656
1706
  const statement = option(args, '--statement');
1657
1707
  const requestedName = option(args, '--name');
1658
1708
  let name = requestedName || path.basename(path.resolve(sourceDir || '.'));
1659
- if (!sourceDir || !out || !by || !statement) {
1660
- fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--sign] [--passphrase <pw>|--passphrase-stdin]');
1709
+ if (!sourceDir || (!out && !checkOnly) || (!by && !checkOnly) || (!statement && !checkOnly)) {
1710
+ fail('Usage: kdna-studio migrate <source-dir> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--check] [--sign] [--passphrase <pw>|--passphrase-stdin]');
1661
1711
  }
1662
1712
 
1663
1713
  const sourcePath = path.resolve(sourceDir);
@@ -1724,6 +1774,42 @@ function cmdMigrate(args) {
1724
1774
  }
1725
1775
  }
1726
1776
 
1777
+ // --check branch: print a pre-migration report and exit. The
1778
+ // project object already has every card from the source tree
1779
+ // imported, so we can run the same audit-locks logic here
1780
+ // without re-reading the source. This is the single-call
1781
+ // equivalent of `kdna-studio create --from-folder <src> &&
1782
+ // kdna-studio audit-locks .` — but it requires no
1783
+ // intermediate write.
1784
+ if (checkOnly) {
1785
+ const REQUIRED = {
1786
+ axiom: ['confidence', 'evidence_type', 'one_sentence', 'full_statement', 'why',
1787
+ 'applies_when', 'does_not_apply_when', 'failure_risk'],
1788
+ risk: ['name', 'description', 'mitigation'],
1789
+ stance: ['statement', 'applies_when'],
1790
+ misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
1791
+ boundary: ['scope', 'out_of_scope'],
1792
+ };
1793
+ const summary = { project: name, source: sourceDir, total: project.cards.length, by_type: {}, would_block: false };
1794
+ for (const card of (project.cards || [])) {
1795
+ const req = REQUIRED[card.type];
1796
+ if (!req) continue;
1797
+ const missing = [];
1798
+ for (const f of req) {
1799
+ const v = card.fields?.[f];
1800
+ if (v === undefined || v === null || v === '' ||
1801
+ (Array.isArray(v) && v.length === 0)) missing.push(f);
1802
+ }
1803
+ if (missing.length > 0) {
1804
+ if (!summary.by_type[card.type]) summary.by_type[card.type] = [];
1805
+ summary.by_type[card.type].push({ id: card.id, missing });
1806
+ summary.would_block = true;
1807
+ }
1808
+ }
1809
+ console.log(JSON.stringify(summary, null, 2));
1810
+ process.exit(summary.would_block ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
1811
+ }
1812
+
1727
1813
  // Step 2: approve and Human Lock all cards
1728
1814
  let locked = 0;
1729
1815
  for (let i = 0; i < (project.cards || []).length; i++) {
@@ -2250,6 +2336,135 @@ function cmdReport(args) {
2250
2336
  process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
2251
2337
  }
2252
2338
 
2339
+ // Audit-locks: list every card that would fail the v1.7.2+ Human Lock
2340
+ // gate because of missing or empty fields. Used during the
2341
+ // pre-migration review of source-tree assets (e.g. pro-20) so an
2342
+ // author can fill in the missing confidence / evidence_type / risk
2343
+ // description / stance statement BEFORE running `migrate --format v1`.
2344
+ //
2345
+ // Bug (UX pro-20 migration): prior version of `kdna-studio report`
2346
+ // returned a flat human_lock_gate.issues list, which mixed card
2347
+ // types and field names. An author migrating 20 source-tree
2348
+ // assets with 99 axioms + 140 risk cards + 70 stance cards had no
2349
+ // way to see, for a single asset, "which axioms are missing
2350
+ // confidence?" — they had to grep the project JSON. The fix adds
2351
+ // a per-type, per-field breakdown.
2352
+ function cmdAuditLocks(args) {
2353
+ const { project } = readProject(args[0]);
2354
+ const useJson = args.includes('--json');
2355
+ const onlyType = option(args, '--type'); // 'axiom' | 'risk' | 'stance' | 'misunderstanding' | null
2356
+
2357
+ const cards = project.cards || [];
2358
+ const findings = {
2359
+ project: project.name,
2360
+ card_count: cards.length,
2361
+ locked_count: cards.filter(c => c.locked).length,
2362
+ by_type: {},
2363
+ };
2364
+
2365
+ // Per-card-type required field lists. Mirrors the v1.7.2+ lockCard
2366
+ // gate (kdna-studio-core/src/cards/index.js) so the output is
2367
+ // exactly the field set the gate will reject.
2368
+ const REQUIRED = {
2369
+ axiom: ['one_sentence', 'full_statement', 'why', 'applies_when',
2370
+ 'does_not_apply_when', 'failure_risk', 'confidence', 'evidence_type'],
2371
+ risk: ['name', 'description', 'mitigation'],
2372
+ stance: ['statement', 'applies_when'],
2373
+ misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
2374
+ boundary: ['scope', 'out_of_scope'],
2375
+ self_check: ['question'],
2376
+ };
2377
+ const THRESHOLDS = {
2378
+ axiom: {
2379
+ one_sentence: { min: 5, type: 'string' },
2380
+ full_statement: { min: 20, type: 'string' },
2381
+ why: { min: 20, type: 'string' },
2382
+ failure_risk: { min: 1, type: 'string' },
2383
+ confidence: { allowed: ['low', 'medium', 'high'] },
2384
+ evidence_type: { allowed: ['case_observation', 'theoretical', 'empirical', 'analogy', 'principle'] },
2385
+ },
2386
+ risk: {
2387
+ name: { min: 1, type: 'string' },
2388
+ description: { min: 20, type: 'string' },
2389
+ mitigation: { min: 1, type: 'string' },
2390
+ },
2391
+ stance: {
2392
+ statement: { min: 1, type: 'string' },
2393
+ applies_when: { type: 'array', minLength: 0 },
2394
+ },
2395
+ };
2396
+
2397
+ function checkField(type, fields, name, rule) {
2398
+ const value = fields[name];
2399
+ if (value === undefined || value === null || value === '') {
2400
+ return { field: name, issue: 'missing' };
2401
+ }
2402
+ if (rule.type === 'string' && rule.min && String(value).length < rule.min) {
2403
+ return { field: name, issue: 'too_short', have_length: String(value).length, need_min: rule.min };
2404
+ }
2405
+ if (rule.allowed && !rule.allowed.includes(value)) {
2406
+ return { field: name, issue: 'invalid_value', have: value, allowed: rule.allowed };
2407
+ }
2408
+ if (rule.type === 'array' && !Array.isArray(value)) {
2409
+ return { field: name, issue: 'wrong_type', have_type: typeof value, need_type: 'array' };
2410
+ }
2411
+ return null;
2412
+ }
2413
+
2414
+ for (const card of cards) {
2415
+ const req = REQUIRED[card.type];
2416
+ if (!req) continue;
2417
+ if (onlyType && card.type !== onlyType) continue;
2418
+ const rule = THRESHOLDS[card.type] || {};
2419
+ const missing = [];
2420
+ for (const name of req) {
2421
+ const issue = checkField(card.type, card.fields || {}, name, rule[name] || {});
2422
+ if (issue) missing.push(issue);
2423
+ }
2424
+ if (missing.length > 0) {
2425
+ if (!findings.by_type[card.type]) {
2426
+ findings.by_type[card.type] = { missing_count: 0, cards: [] };
2427
+ }
2428
+ findings.by_type[card.type].missing_count += 1;
2429
+ findings.by_type[card.type].cards.push({
2430
+ id: card.id,
2431
+ one_sentence: card.fields?.one_sentence || card.fields?.statement || card.fields?.question || '',
2432
+ missing,
2433
+ });
2434
+ }
2435
+ }
2436
+
2437
+ if (useJson) {
2438
+ console.log(JSON.stringify(findings, null, 2));
2439
+ } else {
2440
+ console.log(`Project: ${findings.project}`);
2441
+ console.log(`Cards: ${findings.card_count} (locked: ${findings.locked_count})`);
2442
+ const types = Object.keys(findings.by_type);
2443
+ if (types.length === 0) {
2444
+ console.log('\nAll cards pass the v1.7.2+ Human Lock gate. Ready to migrate.');
2445
+ return;
2446
+ }
2447
+ for (const t of types) {
2448
+ const block = findings.by_type[t];
2449
+ console.log(`\n${t}: ${block.missing_count} card(s) with missing fields`);
2450
+ for (const card of block.cards) {
2451
+ console.log(` ${card.id} ${(card.one_sentence || '').slice(0, 60)}`);
2452
+ for (const m of card.missing) {
2453
+ if (m.issue === 'missing') {
2454
+ console.log(` - ${m.field}: missing`);
2455
+ } else if (m.issue === 'too_short') {
2456
+ console.log(` - ${m.field}: too short (have ${m.have_length}, need ≥ ${m.need_min})`);
2457
+ } else if (m.issue === 'invalid_value') {
2458
+ console.log(` - ${m.field}: invalid value "${m.have}", allowed: ${m.allowed.join('|')}`);
2459
+ } else {
2460
+ console.log(` - ${m.field}: ${m.issue}`);
2461
+ }
2462
+ }
2463
+ }
2464
+ }
2465
+ }
2466
+ }
2467
+
2253
2468
  // ─── Distillation Commands ──────────────────────────────────────────
2254
2469
 
2255
2470
  function cmdTarget(args) {
@@ -2677,6 +2892,7 @@ try {
2677
2892
  else if (cmd === 'llm') cmdLlm(args.slice(1));
2678
2893
  else if (cmd === 'identity') cmdIdentity(args.slice(1));
2679
2894
  else if (cmd === 'report') cmdReport(args.slice(1));
2895
+ else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
2680
2896
  else if (cmd === 'install') cmdStudioInstall(args.slice(1));
2681
2897
  else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
2682
2898
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "Official KDNA Studio command-line authoring entry for creating, reviewing, compiling, and exporting .kdna judgment assets through the KDNA toolchain.",
5
5
  "type": "commonjs",
6
6
  "bin": {