@aikdna/kdna-studio-cli 0.5.10 → 0.6.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.
@@ -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,37 @@ function cmdFilter(args) {
904
908
  }
905
909
  }
906
910
 
911
+ const AXIOM_REQUIRED_FIELDS = [
912
+ 'one_sentence', 'full_statement', 'why',
913
+ 'applies_when', 'does_not_apply_when', 'failure_risk',
914
+ 'confidence', 'evidence_type',
915
+ ];
916
+
917
+ function checkRequiredFields(type, fields) {
918
+ if (type !== 'axiom') return [];
919
+ const missing = [];
920
+ for (const f of AXIOM_REQUIRED_FIELDS) {
921
+ const val = fields[f];
922
+ if (val === undefined || val === null || val === '' ||
923
+ (Array.isArray(val) && val.length === 0)) {
924
+ missing.push(f);
925
+ }
926
+ }
927
+ return missing;
928
+ }
929
+
930
+ function axiomStrictTemplate(fields) {
931
+ const filled = { ...fields };
932
+ for (const f of AXIOM_REQUIRED_FIELDS) {
933
+ const val = filled[f];
934
+ if (val === undefined || val === null || val === '' ||
935
+ (Array.isArray(val) && val.length === 0)) {
936
+ filled[f] = `<TBD: ${f}>`;
937
+ }
938
+ }
939
+ return filled;
940
+ }
941
+
907
942
  function cmdCard(args) {
908
943
  const sub = args[0];
909
944
  if (sub === 'list') {
@@ -916,9 +951,27 @@ function cmdCard(args) {
916
951
  if (sub === 'add') {
917
952
  const projectInput = args[1];
918
953
  const type = args[2];
919
- if (!projectInput || !type) fail('Usage: kdna-studio card add <project> <type> --field key=value');
954
+ if (!projectInput || !type) fail('Usage: kdna-studio card add <project> <type> --field key=value [--template <name>] [--no-strict]');
920
955
  const { projectPath, project } = readProject(projectInput);
921
- const card = cardApi.createCard(type, parseFields(args.slice(3)));
956
+ let fields = parseFields(args.slice(3));
957
+
958
+ const useTemplate = option(args, '--template');
959
+ if (useTemplate === 'axiom-strict') {
960
+ fields = axiomStrictTemplate(fields);
961
+ }
962
+
963
+ const isNoStrict = args.includes('--no-strict');
964
+ if (!isNoStrict && type === 'axiom') {
965
+ const missing = checkRequiredFields(type, fields);
966
+ if (missing.length > 0) {
967
+ fail(
968
+ `Missing required fields for axiom: ${missing.join(', ')}\n` +
969
+ `Use --no-strict to add a partial card, or --template axiom-strict for placeholders.`
970
+ );
971
+ }
972
+ }
973
+
974
+ const card = cardApi.createCard(type, fields);
922
975
  project.cards.push(card);
923
976
  if (project.stages?.judgment_cards) {
924
977
  project.stages.judgment_cards.total = project.cards.length;
@@ -928,6 +981,40 @@ function cmdCard(args) {
928
981
  console.log(`Added card: ${card.id}`);
929
982
  return;
930
983
  }
984
+ if (sub === 'update') {
985
+ const projectInput = args[1];
986
+ const cardId = args[2];
987
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card update <project> <card-id> --field key=value');
988
+ const { projectPath, project } = readProject(projectInput);
989
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
990
+ if (idx < 0) fail(`Card not found: ${cardId}`);
991
+ const card = project.cards[idx];
992
+ if (card.locked) fail(`Cannot update locked card: ${cardId}. Unlock first with card unlock.`);
993
+ const updates = parseFields(args.slice(3));
994
+ for (const [k, v] of Object.entries(updates)) {
995
+ if (card.fields) card.fields[k] = v;
996
+ }
997
+ project.cards[idx] = card;
998
+ writeProject(projectPath, project);
999
+ console.log(`Updated card: ${cardId}`);
1000
+ return;
1001
+ }
1002
+ if (sub === 'remove') {
1003
+ const projectInput = args[1];
1004
+ const cardId = args[2];
1005
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card remove <project> <card-id>');
1006
+ const { projectPath, project } = readProject(projectInput);
1007
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1008
+ if (idx < 0) fail(`Card not found: ${cardId}`);
1009
+ if (project.cards[idx].locked) fail(`Cannot remove locked card: ${cardId}. Unlock first with card unlock.`);
1010
+ project.cards.splice(idx, 1);
1011
+ if (project.stages?.judgment_cards) {
1012
+ project.stages.judgment_cards.total = project.cards.length;
1013
+ }
1014
+ writeProject(projectPath, project);
1015
+ console.log(`Removed card: ${cardId}`);
1016
+ return;
1017
+ }
931
1018
  if (sub === 'approve') {
932
1019
  const projectInput = args[1];
933
1020
  const approveAll = args.includes('--all');
@@ -939,6 +1026,7 @@ function cmdCard(args) {
939
1026
  const statement = option(args, '--statement');
940
1027
  if (!by || !statement) fail('card approve requires --by and --statement');
941
1028
  const { projectPath, project } = readProject(projectInput);
1029
+ const errors = [];
942
1030
 
943
1031
  const lockOneCard = (card) => {
944
1032
  if (card.status === 'draft') card = cardApi.transitionCard(card, 'revised', { by });
@@ -949,42 +1037,57 @@ function cmdCard(args) {
949
1037
  };
950
1038
  if (args.includes('--sign')) {
951
1039
  const identity = creatorApi.loadIdentity();
952
- if (!identity) fail('No creator identity. Run: kdna-studio identity init --name "Your Name"', EXIT.TRUST_FAILED);
1040
+ if (!identity) {
1041
+ errors.push(`${card.id}: No creator identity. Run: kdna-studio identity init`);
1042
+ return null;
1043
+ }
953
1044
  lockPayload.creator_id = identity.creator_id;
954
1045
  const passphrase = option(args, '--passphrase');
955
1046
  try {
956
1047
  if (identity.encrypted && !passphrase) {
957
- fail('Private key is encrypted — provide --passphrase to sign.', EXIT.TRUST_FAILED);
1048
+ errors.push(`${card.id}: Private key is encrypted — provide --passphrase`);
1049
+ return null;
958
1050
  }
959
1051
  lockPayload.signature = creatorApi.signHumanLock(
960
1052
  card.id, statement, cardApi.cardJudgmentFingerprint(card), null, passphrase,
961
1053
  );
962
1054
  } catch (e) {
963
- fail(`Failed to sign Human Lock: ${e.message}`, EXIT.TRUST_FAILED);
1055
+ errors.push(`${card.id}: Signing failed: ${e.message}`);
1056
+ return null;
964
1057
  }
965
1058
  }
966
- return [card, cardApi.lockCard(card, lockPayload)];
1059
+ return cardApi.lockCard(card, lockPayload);
967
1060
  };
968
1061
 
969
1062
  if (cardId) {
970
1063
  const idx = (project.cards || []).findIndex((c) => c.id === cardId);
971
1064
  if (idx < 0) fail(`Card not found: ${cardId}`);
972
- const [, locked] = lockOneCard(project.cards[idx]);
1065
+ if (project.cards[idx].locked) fail(`Card already locked: ${cardId}`);
1066
+ const locked = lockOneCard(project.cards[idx]);
1067
+ if (!locked) fail(errors[0]);
973
1068
  project.cards[idx] = locked;
974
1069
  const signed = args.includes('--sign') ? ' (signed)' : '';
975
1070
  console.log(`Approved and Human Locked: ${cardId}${signed}`);
976
1071
  } else {
977
1072
  let approved = 0;
1073
+ let skipped = 0;
978
1074
  for (let i = 0; i < (project.cards || []).length; i++) {
979
1075
  const card = project.cards[i];
980
- if (card.locked) continue;
981
- const [, locked] = lockOneCard(card);
1076
+ if (card.locked) { skipped++; continue; }
1077
+ const locked = lockOneCard(card);
1078
+ if (!locked) continue;
982
1079
  project.cards[i] = locked;
983
1080
  approved++;
984
1081
  }
985
- if (approved === 0) fail('No unlocked cards to approve.');
1082
+ if (errors.length > 0) {
1083
+ fail(`Failed to approve ${errors.length} card(s):\n ${errors.join('\n ')}`);
1084
+ }
1085
+ if (approved === 0) {
1086
+ if (skipped > 0) fail(`No unlocked cards to approve (${skipped} already locked).`);
1087
+ fail('No unlocked cards to approve.');
1088
+ }
986
1089
  const signed = args.includes('--sign') ? ' (signed)' : '';
987
- console.log(`Approved and Human Locked: ${approved} cards${signed}`);
1090
+ console.log(`Approved and Human Locked: ${approved} cards${signed}${skipped > 0 ? ` (${skipped} already locked)` : ''}`);
988
1091
  }
989
1092
  if (project.stages?.judgment_cards) {
990
1093
  project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
@@ -993,7 +1096,37 @@ function cmdCard(args) {
993
1096
  writeProject(projectPath, project);
994
1097
  return;
995
1098
  }
996
- fail('Usage: kdna-studio card <list|add|approve> ...');
1099
+ if (sub === 'unlock') {
1100
+ const projectInput = args[1];
1101
+ const cardId = args[2];
1102
+ if (!projectInput || !cardId) fail('Usage: kdna-studio card unlock <project> <card-id> --by <id> --statement <text>');
1103
+ const by = option(args, '--by');
1104
+ const statement = option(args, '--statement');
1105
+ if (!by || !statement) fail('card unlock requires --by and --statement');
1106
+ const { projectPath, project } = readProject(projectInput);
1107
+ const idx = (project.cards || []).findIndex((c) => c.id === cardId);
1108
+ if (idx < 0) fail(`Card not found: ${cardId}`);
1109
+ const card = project.cards[idx];
1110
+ if (!card.locked) fail(`Card is not locked: ${cardId}`);
1111
+ card.locked = false;
1112
+ card.status = 'draft';
1113
+ delete card.human_lock;
1114
+ if (!card.audit_log) card.audit_log = [];
1115
+ card.audit_log.push({
1116
+ action: 'unlocked',
1117
+ by,
1118
+ statement,
1119
+ timestamp: new Date().toISOString(),
1120
+ });
1121
+ project.cards[idx] = card;
1122
+ if (project.stages?.judgment_cards) {
1123
+ project.stages.judgment_cards.locked = project.cards.filter((c) => c.locked).length;
1124
+ }
1125
+ writeProject(projectPath, project);
1126
+ console.log(`Unlocked card: ${cardId}`);
1127
+ return;
1128
+ }
1129
+ fail('Usage: kdna-studio card <list|add|update|remove|approve|unlock> ...');
997
1130
  }
998
1131
 
999
1132
  function cmdLock(args) {
@@ -1103,9 +1236,10 @@ function cmdMigrate(args) {
1103
1236
  }
1104
1237
 
1105
1238
  const absOut = path.resolve(out);
1239
+ const allowIncomplete = args.includes('--allow-incomplete');
1106
1240
  // Step 4: v1 export or v2 compile + export
1107
1241
  if (format === 'v1') {
1108
- exportProjectV1(project, name, absOut);
1242
+ exportProjectV1(project, name, absOut, { allowIncomplete });
1109
1243
  if (tmpDir) {
1110
1244
  try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
1111
1245
  }
@@ -1136,7 +1270,7 @@ function cmdMigrate(args) {
1136
1270
  }
1137
1271
  }
1138
1272
 
1139
- function exportProjectV1(project, name, outPath) {
1273
+ function exportProjectV1(project, name, outPath, opts = {}) {
1140
1274
  let core;
1141
1275
  try { core = require('@aikdna/kdna-core'); } catch (e) {
1142
1276
  fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
@@ -1146,8 +1280,13 @@ function exportProjectV1(project, name, outPath) {
1146
1280
  }
1147
1281
  const gate = projectApi.checkHumanLockGate(project);
1148
1282
  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);
1283
+ if (opts.allowIncomplete) {
1284
+ console.warn('Human Lock Gate bypassed (--allow-incomplete):');
1285
+ for (const issue of gate.issues) console.warn(` - ${issue.cardId}: ${issue.reason}`);
1286
+ } else {
1287
+ const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
1288
+ fail(`Human Lock Gate blocked v1 export:\n - ${reasons}\nUse --allow-incomplete to bypass.`, EXIT.HUMAN_LOCK_REQUIRED);
1289
+ }
1151
1290
  }
1152
1291
  const lockedCards = (project.cards || []).filter(c => c.locked);
1153
1292
  const v1Dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-'));
@@ -1347,10 +1486,10 @@ function applySignature(files, passphrase = null) {
1347
1486
  function cmdExport(args) {
1348
1487
  const projectInput = args[0];
1349
1488
  const out = option(args, '--out');
1350
- if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign]');
1489
+ if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign] [--allow-incomplete]');
1351
1490
  if (option(args, '--format') === 'v1') {
1352
1491
  const { project } = readProject(projectInput);
1353
- exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out));
1492
+ exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), { allowIncomplete: args.includes('--allow-incomplete') });
1354
1493
  return;
1355
1494
  }
1356
1495
  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.10",
3
+ "version": "0.6.0",
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": {
@@ -44,6 +44,6 @@
44
44
  "cbor-x": "^1.6.4"
45
45
  },
46
46
  "devDependencies": {
47
- "@aikdna/kdna-core": "^0.12.4"
47
+ "@aikdna/kdna-core": "^0.12.5"
48
48
  }
49
49
  }
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];
@@ -740,6 +755,8 @@ test('E2E fork: parent → fork → approve → export → lineage check', (t) =
740
755
  '--field', 'applies_when=["fork testing"]',
741
756
  '--field', 'does_not_apply_when=["not forking"]',
742
757
  '--field', 'failure_risk=forked content may lose lineage',
758
+ '--field', 'confidence=high',
759
+ '--field', 'evidence_type=practice',
743
760
  ], { tmp });
744
761
  const pCards = run(['card', 'list', parentDir], { tmp });
745
762
  for (const line of pCards.stdout.trim().split('\n')) {