@aikdna/kdna-studio-cli 0.5.11 → 0.6.1

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.
@@ -44,22 +44,25 @@ Identity:
44
44
  kdna-studio identity show
45
45
 
46
46
  Create (three entry paths):
47
- kdna-studio create <project-dir> --name <@scope/name> # blank
47
+ kdna-studio create <project-dir> --name <@scope/name> [--author <name>] # blank
48
48
  kdna-studio create <project-dir> --from-kdna <file.kdna> --name <@scope/name>
49
49
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
50
50
 
51
51
  Migrate (dev source or Studio project → canonical .kdna in one command):
52
- kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text>
52
+ kdna-studio migrate <source-dir|project> --format v1 --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete]
53
53
 
54
54
  Authoring:
55
55
  kdna-studio import <project> <source-file-or-dir> # import evidence (txt/md/json/yaml/csv/log/srt/vtt/html)
56
56
  kdna-studio filter <project> # check evidence for sensitive content
57
57
  kdna-studio source classify <project> # classify evidence against declared target
58
58
  kdna-studio card list <project>
59
- kdna-studio card add <project> <type> --field key=value [--field key=value]
59
+ kdna-studio card add <project> <type> --field key=value [--field key=value] [--template <name>] [--no-strict]
60
+ kdna-studio card update <project> <card-id> --field key=value
61
+ kdna-studio card remove <project> <card-id>
60
62
  kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign] [--passphrase <pass>]
63
+ kdna-studio card unlock <project> <card-id> --by <id> --statement <text>
61
64
  kdna-studio compile <project> --out <dir>
62
- kdna-studio export <project> --format v1 --out <file.kdna>
65
+ kdna-studio export <project> --format v1 --out <file.kdna> [--allow-incomplete]
63
66
 
64
67
  AI Authoring (requires LLM config: kdna-studio llm config):
65
68
  kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
@@ -425,11 +428,12 @@ function buildV1Manifest(project, name) {
425
428
 
426
429
  function cmdCreate(args) {
427
430
  const dir = args[0];
428
- if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--from-kdna <file> | --from-folder <dir>]');
431
+ if (!dir) fail('Usage: kdna-studio create <project-dir> --name <name> [--author <name>] [--from-kdna <file> | --from-folder <dir>]');
429
432
  const abs = path.resolve(dir);
430
433
  if (fs.existsSync(abs)) fail(`Directory already exists: ${abs}`);
431
434
 
432
435
  const name = option(args, '--name', path.basename(abs));
436
+ const authorName = option(args, '--author', '');
433
437
  const fromKdna = option(args, '--from-kdna');
434
438
  const fromFolder = option(args, '--from-folder');
435
439
 
@@ -448,7 +452,7 @@ function cmdCreate(args) {
448
452
  const sourceAuthor = kdnaData.source_manifest?.author || {};
449
453
  const project = projectApi.createProject(name, 'domain', {
450
454
  author: {
451
- name: option(args, '--author-name', sourceAuthor.name || ''),
455
+ name: authorName || option(args, '--author-name', sourceAuthor.name || ''),
452
456
  id: option(args, '--author-id', sourceAuthor.id || ''),
453
457
  },
454
458
  sourceMode,
@@ -485,7 +489,7 @@ function cmdCreate(args) {
485
489
  }
486
490
 
487
491
  const project = projectApi.createProject(name, 'domain', {
488
- author: { name: option(args, '--author-name', ''), id: option(args, '--author-id', '') },
492
+ author: { name: authorName || option(args, '--author-name', ''), id: option(args, '--author-id', '') },
489
493
  sourceMode,
490
494
  creatorIdentity,
491
495
  lineage,
@@ -904,6 +908,49 @@ function cmdFilter(args) {
904
908
  }
905
909
  }
906
910
 
911
+ const CARD_REQUIRED_FIELDS = {
912
+ axiom: [
913
+ 'one_sentence', 'full_statement', 'why',
914
+ 'applies_when', 'does_not_apply_when', 'failure_risk',
915
+ 'confidence', 'evidence_type',
916
+ ],
917
+ boundary: ['scope', 'out_of_scope'],
918
+ misunderstanding: ['wrong', 'correct', 'key_distinction', 'why'],
919
+ self_check: ['question'],
920
+ scenario: ['name', 'trigger', 'action', 'expected'],
921
+ case: ['title', 'scenario', 'input', 'expected'],
922
+ risk: ['name', 'description', 'mitigation'],
923
+ stance: ['statement'],
924
+ };
925
+
926
+ function checkRequiredFields(type, fields) {
927
+ const required = CARD_REQUIRED_FIELDS[type];
928
+ if (!required) return [];
929
+ const missing = [];
930
+ for (const f of required) {
931
+ const val = fields[f];
932
+ if (val === undefined || val === null || val === '' ||
933
+ (Array.isArray(val) && val.length === 0)) {
934
+ missing.push(f);
935
+ }
936
+ }
937
+ return missing;
938
+ }
939
+
940
+ function cardStrictTemplate(type, fields) {
941
+ const required = CARD_REQUIRED_FIELDS[type];
942
+ if (!required) return fields;
943
+ const filled = { ...fields };
944
+ for (const f of required) {
945
+ const val = filled[f];
946
+ if (val === undefined || val === null || val === '' ||
947
+ (Array.isArray(val) && val.length === 0)) {
948
+ filled[f] = `<TBD: ${f}>`;
949
+ }
950
+ }
951
+ return filled;
952
+ }
953
+
907
954
  function cmdCard(args) {
908
955
  const sub = args[0];
909
956
  if (sub === 'list') {
@@ -916,9 +963,28 @@ function cmdCard(args) {
916
963
  if (sub === 'add') {
917
964
  const projectInput = args[1];
918
965
  const type = args[2];
919
- if (!projectInput || !type) fail('Usage: kdna-studio card add <project> <type> --field key=value');
966
+ if (!projectInput || !type) fail('Usage: kdna-studio card add <project> <type> --field key=value [--template <name>] [--no-strict]');
920
967
  const { projectPath, project } = readProject(projectInput);
921
- const card = cardApi.createCard(type, parseFields(args.slice(3)));
968
+ let fields = parseFields(args.slice(3));
969
+
970
+ const useTemplate = option(args, '--template');
971
+ if (useTemplate === 'axiom-strict') {
972
+ if (type !== 'axiom') fail('--template axiom-strict only applies to axiom cards');
973
+ fields = cardStrictTemplate('axiom', fields);
974
+ }
975
+
976
+ const isNoStrict = args.includes('--no-strict');
977
+ if (!isNoStrict) {
978
+ const missing = checkRequiredFields(type, fields);
979
+ if (missing.length > 0) {
980
+ fail(
981
+ `Missing required fields for ${type}: ${missing.join(', ')}\n` +
982
+ `Use --no-strict to add a partial card.`
983
+ );
984
+ }
985
+ }
986
+
987
+ const card = cardApi.createCard(type, fields);
922
988
  project.cards.push(card);
923
989
  if (project.stages?.judgment_cards) {
924
990
  project.stages.judgment_cards.total = project.cards.length;
@@ -928,6 +994,40 @@ function cmdCard(args) {
928
994
  console.log(`Added card: ${card.id}`);
929
995
  return;
930
996
  }
997
+ if (sub === 'update') {
998
+ const projectInput = args[1];
999
+ const cardId = args[2];
1000
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value');
1001
+ const { projectPath, project } = readProject(projectInput);
1002
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1003
+ if (idx < 0) fail(`Card not found: ${cardId}`);
1004
+ const card = project.cards[idx];
1005
+ if (card.locked) fail(`Cannot update locked card: ${cardId}. Unlock first with card unlock.`);
1006
+ const updates = parseFields(args.slice(3));
1007
+ for (const [k, v] of Object.entries(updates)) {
1008
+ if (card.fields) card.fields[k] = v;
1009
+ }
1010
+ project.cards[idx] = card;
1011
+ writeProject(projectPath, project);
1012
+ console.log(`Updated card: ${cardId}`);
1013
+ return;
1014
+ }
1015
+ if (sub === 'remove') {
1016
+ const projectInput = args[1];
1017
+ const cardId = args[2];
1018
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card remove <project> <card-id>');
1019
+ const { projectPath, project } = readProject(projectInput);
1020
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1021
+ if (idx < 0) fail(`Card not found: ${cardId}`);
1022
+ if (project.cards[idx].locked) fail(`Cannot remove locked card: ${cardId}. Unlock first with card unlock.`);
1023
+ project.cards.splice(idx, 1);
1024
+ if (project.stages?.judgment_cards) {
1025
+ project.stages.judgment_cards.total = project.cards.length;
1026
+ }
1027
+ writeProject(projectPath, project);
1028
+ console.log(`Removed card: ${cardId}`);
1029
+ return;
1030
+ }
931
1031
  if (sub === 'approve') {
932
1032
  const projectInput = args[1];
933
1033
  const approveAll = args.includes('--all');
@@ -939,6 +1039,7 @@ function cmdCard(args) {
939
1039
  const statement = option(args, '--statement');
940
1040
  if (!by || !statement) fail('card approve requires --by and --statement');
941
1041
  const { projectPath, project } = readProject(projectInput);
1042
+ const errors = [];
942
1043
 
943
1044
  const lockOneCard = (card) => {
944
1045
  if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
@@ -949,42 +1050,57 @@ function cmdCard(args) {
949
1050
  };
950
1051
  if (args.includes('--sign')) {
951
1052
  const identity = creatorApi.loadIdentity();
952
- if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
1053
+ if (!identity) {
1054
+ errors.push(`${card.id}: No creator identity. Run: kdna-studio identity init`);
1055
+ return null;
1056
+ }
953
1057
  lockPayload.creator_id = identity.creator_id;
954
1058
  const passphrase = option(args, '--passphrase');
955
1059
  try {
956
1060
  if (identity.encrypted && !passphrase) {
957
- fail('Private key is encrypted — provide --passphrase to sign.', EXIT.TRUST_FAILED);
1061
+ errors.push(`${card.id}: Private key is encrypted — provide --passphrase`);
1062
+ return null;
958
1063
  }
959
1064
  lockPayload.signature = creatorApi.signHumanLock(
960
1065
  card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
961
1066
  );
962
1067
  } catch (e) {
963
- fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
1068
+ errors.push(`${card.id}: Signing failed: ${e.message}`);
1069
+ return null;
964
1070
  }
965
1071
  }
966
- return [card, cardApi.lockCard(card, lockPayload)];
1072
+ return cardApi.lockCard(card, lockPayload);
967
1073
  };
968
1074
 
969
1075
  if (cardId) {
970
1076
  const idx = (project.cards || []).findIndex((c) => c.id === cardId);
971
1077
  if (idx < 0) fail(`Card not found: ${cardId}`);
972
- const [, locked] = lockOneCard(project.cards[idx]);
1078
+ if (project.cards[idx].locked) fail(`Card already locked: ${cardId}`);
1079
+ const locked = lockOneCard(project.cards[idx]);
1080
+ if (!locked) fail(errors[0]);
973
1081
  project.cards[idx] = locked;
974
1082
  const signed = args.includes('--sign') ? ' (signed)' : '';
975
1083
  console.log(`Approved and Human Locked: ${cardId}${signed}`);
976
1084
  } else {
977
1085
  let approved = 0;
1086
+ let skipped = 0;
978
1087
  for (let i = 0; i < (project.cards || []).length; i++) {
979
1088
  const card = project.cards[i];
980
- if (card.locked) continue;
981
- const [, locked] = lockOneCard(card);
1089
+ if (card.locked) { skipped++; continue; }
1090
+ const locked = lockOneCard(card);
1091
+ if (!locked) continue;
982
1092
  project.cards[i] = locked;
983
1093
  approved++;
984
1094
  }
985
- if (approved === 0) fail('No unlocked cards to approve.');
1095
+ if (errors.length > 0) {
1096
+ fail(`Failed to approve ${errors.length} card(s):\n ${errors.join('\n ')}`);
1097
+ }
1098
+ if (approved === 0) {
1099
+ if (skipped > 0) fail(`No unlocked cards to approve (${skipped} already locked).`);
1100
+ fail('No unlocked cards to approve.');
1101
+ }
986
1102
  const signed = args.includes('--sign') ? ' (signed)' : '';
987
- console.log(`Approved and Human Locked: ${approved} cards${signed}`);
1103
+ console.log(`Approved and Human Locked: ${approved} cards${signed}${skipped > 0 ? ` (${skipped} already locked)` : ''}`);
988
1104
  }
989
1105
  if (project.stages?.judgment_cards) {
990
1106
  project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
@@ -993,7 +1109,37 @@ function cmdCard(args) {
993
1109
  writeProject(projectPath, project);
994
1110
  return;
995
1111
  }
996
- fail('Usage: kdna-studio card <list|add|approve> ...');
1112
+ if (sub === 'unlock') {
1113
+ const projectInput = args[1];
1114
+ const cardId = args[2];
1115
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card unlock <project> <card-id> --by <id> --statement <text>');
1116
+ const by = option(args, '--by');
1117
+ const statement = option(args, '--statement');
1118
+ if (!by || !statement) fail('card unlock requires --by and --statement');
1119
+ const { projectPath, project } = readProject(projectInput);
1120
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1121
+ if (idx < 0) fail(`Card not found: ${cardId}`);
1122
+ const card = project.cards[idx];
1123
+ if (!card.locked) fail(`Card is not locked: ${cardId}`);
1124
+ card.locked = false;
1125
+ card.status = 'draft';
1126
+ delete card.human_lock;
1127
+ if (!card.audit_log) card.audit_log = [];
1128
+ card.audit_log.push({
1129
+ action: 'unlocked',
1130
+ by,
1131
+ statement,
1132
+ timestamp: new Date().toISOString(),
1133
+ });
1134
+ project.cards[idx] = card;
1135
+ if (project.stages?.judgment_cards) {
1136
+ project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
1137
+ }
1138
+ writeProject(projectPath, project);
1139
+ console.log(`Unlocked card: ${cardId}`);
1140
+ return;
1141
+ }
1142
+ fail('Usage: kdna-studio card <list|add|update|remove|approve|unlock> ...');
997
1143
  }
998
1144
 
999
1145
  function cmdLock(args) {
@@ -1103,9 +1249,10 @@ function cmdMigrate(args) {
1103
1249
  }
1104
1250
 
1105
1251
  const absOut = path.resolve(out);
1252
+ const allowIncomplete = args.includes('--allow-incomplete');
1106
1253
  // Step 4: v1 export or v2 compile + export
1107
1254
  if (format === 'v1') {
1108
- exportProjectV1(project, name, absOut);
1255
+ exportProjectV1(project, name, absOut, { allowIncomplete });
1109
1256
  if (tmpDir) {
1110
1257
  try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
1111
1258
  }
@@ -1136,7 +1283,7 @@ function cmdMigrate(args) {
1136
1283
  }
1137
1284
  }
1138
1285
 
1139
- function exportProjectV1(project, name, outPath) {
1286
+ function exportProjectV1(project, name, outPath, opts = {}) {
1140
1287
  let core;
1141
1288
  try { core = require('@aikdna/kdna-core'); } catch (e) {
1142
1289
  fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
@@ -1146,8 +1293,13 @@ function exportProjectV1(project, name, outPath) {
1146
1293
  }
1147
1294
  const gate = projectApi.checkHumanLockGate(project);
1148
1295
  if (gate.blocked) {
1149
- const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
1150
- fail(`Human Lock Gate blocked v1 export:\n - ${reasons}`, EXIT.HUMAN_LOCK_REQUIRED);
1296
+ if (opts.allowIncomplete) {
1297
+ console.warn('Human Lock Gate bypassed (--allow-incomplete):');
1298
+ for (const issue of gate.issues) console.warn(` - ${issue.cardId}: ${issue.reason}`);
1299
+ } else {
1300
+ const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
1301
+ fail(`Human Lock Gate blocked v1 export:\n - ${reasons}\nUse --allow-incomplete to bypass.`, EXIT.HUMAN_LOCK_REQUIRED);
1302
+ }
1151
1303
  }
1152
1304
  const lockedCards = (project.cards || []).filter(c => c.locked);
1153
1305
  const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
@@ -1347,10 +1499,10 @@ function applySignature(files, passphrase = null) {
1347
1499
  function cmdExport(args) {
1348
1500
  const projectInput = args[0];
1349
1501
  const out = option(args, '--out');
1350
- if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign]');
1502
+ if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign] [--allow-incomplete]');
1351
1503
  if (option(args, '--format') === 'v1') {
1352
1504
  const { project } = readProject(projectInput);
1353
- exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out));
1505
+ exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), { allowIncomplete: args.includes('--allow-incomplete') });
1354
1506
  return;
1355
1507
  }
1356
1508
  const { project, result } = compileProject(projectInput);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.5.11",
3
+ "version": "0.6.1",
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": {
package/tests/cli.test.js CHANGED
@@ -65,6 +65,10 @@ function createLockedProject(t) {
65
65
  'does_not_apply_when=["pure formatting"]',
66
66
  '--field',
67
67
  'failure_risk=generic advice',
68
+ '--field',
69
+ 'confidence=high',
70
+ '--field',
71
+ 'evidence_type=practice',
68
72
  ], { tmp });
69
73
  assert.equal(result.status, 0, result.stderr);
70
74
  const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
@@ -123,6 +127,10 @@ test('card approve --all locks every unlocked card', (t) => {
123
127
  'does_not_apply_when=["pure formatting"]',
124
128
  '--field',
125
129
  'failure_risk=generic advice',
130
+ '--field',
131
+ 'confidence=high',
132
+ '--field',
133
+ 'evidence_type=practice',
126
134
  ], { tmp });
127
135
  assert.equal(result.status, 0, result.stderr);
128
136
 
@@ -174,6 +182,7 @@ test('blocks compile and export until judgment cards are Human Locked', (t) => {
174
182
  'add',
175
183
  projectDir,
176
184
  'axiom',
185
+ '--no-strict',
177
186
  '--field',
178
187
  'one_sentence=Prefer specific evidence over broad claims',
179
188
  ], { tmp });
@@ -550,6 +559,8 @@ test('card approve --sign binds Human Lock to creator identity', (t) => {
550
559
  '--field', 'applies_when=["reviewing content"]',
551
560
  '--field', 'does_not_apply_when=["pure formatting"]',
552
561
  '--field', 'failure_risk=generic advice',
562
+ '--field', 'confidence=high',
563
+ '--field', 'evidence_type=practice',
553
564
  ], { tmp });
554
565
  assert.equal(result.status, 0, result.stderr);
555
566
  const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
@@ -615,6 +626,8 @@ test('export includes creator and lineage in kdna.json', (t) => {
615
626
  '--field', 'applies_when=["testing"]',
616
627
  '--field', 'does_not_apply_when=["not testing"]',
617
628
  '--field', 'failure_risk=test may pass incorrectly',
629
+ '--field', 'confidence=high',
630
+ '--field', 'evidence_type=practice',
618
631
  ], { tmp });
619
632
  let result = run(['card', 'list', projectDir], { tmp });
620
633
  const cardId = result.stdout.match(/(\S+)\s+axiom/)?.[1];
@@ -652,6 +665,8 @@ test('E2E blank: create → approve → lock → export → runtime digest match
652
665
  '--field', 'applies_when=["e2e testing"]',
653
666
  '--field', 'does_not_apply_when=["production"]',
654
667
  '--field', 'failure_risk=e2e test may incorrectly pass',
668
+ '--field', 'confidence=high',
669
+ '--field', 'evidence_type=practice',
655
670
  ], { tmp });
656
671
  assert.equal(result.status, 0, result.stderr);
657
672
  const cardId = result.stdout.match(/Added card: (\S+)/)?.[1];
@@ -662,6 +677,7 @@ test('E2E blank: create → approve → lock → export → runtime digest match
662
677
  '--field', 'wrong=E2E tests are unnecessary.',
663
678
  '--field', 'correct=E2E tests catch integration issues not visible in unit tests.',
664
679
  '--field', 'key_distinction=E2E tests validate the full pipeline, not individual functions.',
680
+ '--field', 'why=E2E test failures are hard to debug without a full integration test running.',
665
681
  ], { tmp });
666
682
  assert.equal(result.status, 0, result.stderr);
667
683
 
@@ -740,6 +756,8 @@ test('E2E fork: parent → fork → approve → export → lineage check', (t) =
740
756
  '--field', 'applies_when=["fork testing"]',
741
757
  '--field', 'does_not_apply_when=["not forking"]',
742
758
  '--field', 'failure_risk=forked content may lose lineage',
759
+ '--field', 'confidence=high',
760
+ '--field', 'evidence_type=practice',
743
761
  ], { tmp });
744
762
  const pCards = run(['card', 'list', parentDir], { tmp });
745
763
  for (const line of pCards.stdout.trim().split('\n')) {