@aikdna/kdna-studio-cli 0.8.12 → 0.8.14

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 CHANGED
@@ -50,13 +50,13 @@ kdna-studio card add my_domain axiom \
50
50
  --field does_not_apply_when='["pure formatting"]' \
51
51
  --field failure_risk="generic advice"
52
52
  kdna-studio card approve my_domain --all --by expert --statement "I confirm this judgment."
53
- kdna-studio export my_domain --format v1 --out dist/my_domain.kdna
53
+ kdna-studio export my_domain --out dist/my_domain.kdna
54
54
  ```
55
55
 
56
56
  Candidate promotion is scope-gated: only candidates with `status == accepted` and `scope_fit == true` are promoted to cards by default. Use `kdna-studio candidate override <project> <candidate-id>` only when a human intentionally overrides the scope gate.
57
57
 
58
58
  The current Studio CLI export workflow uses approved cards as release evidence.
59
- This is Studio project policy, not a KDNA Core v1 format-validity rule. Human
59
+ This is Studio project policy, not a KDNA format-validity rule. Human
60
60
  Lock and other provenance records are optional review evidence, not validity
61
61
  requirements.
62
62
 
@@ -74,7 +74,7 @@ loading existing `.kdna` assets.
74
74
 
75
75
  ## 5-minute first asset
76
76
 
77
- If you just installed and want to ship a v1 asset without an LLM:
77
+ If you just installed and want to ship a KDNA asset without an LLM:
78
78
 
79
79
  ```bash
80
80
  # 1. Init identity (required for the export signature)
@@ -97,8 +97,8 @@ kdna-studio card add . axiom \
97
97
  # 4. Approve and lock
98
98
  kdna-studio card approve . --all --by me --statement "i confirm"
99
99
 
100
- # 5. Export the v1 asset
101
- kdna-studio export . --format v1 --out my_domain.kdna
100
+ # 5. Export the asset
101
+ kdna-studio export . --out my_domain.kdna
102
102
 
103
103
  # 6. Verify with the runtime CLI
104
104
  kdna load my_domain.kdna --profile=compact
@@ -115,11 +115,11 @@ kdna-studio llm config --provider openai --model gpt-4 --key <your-key>
115
115
 
116
116
  ## Runtime Export Contract
117
117
 
118
- `kdna-studio export --format v1` is the canonical runtime export path. It uses
119
- `@aikdna/kdna-studio-core` to compile the Studio project into a KDNA Core v1
118
+ `kdna-studio export` is the canonical runtime export path. It uses
119
+ `@aikdna/kdna-studio-core` to compile the Studio project into the current KDNA
120
120
  runtime asset and then packs it with `@aikdna/kdna-core`.
121
121
 
122
- A KDNA Core v1 runtime export contains only these top-level entries:
122
+ A KDNA runtime export contains only these top-level entries:
123
123
 
124
124
  - `mimetype`
125
125
  - `kdna.json`
@@ -135,6 +135,16 @@ Being part of the official toolchain means this package is maintained by the
135
135
  KDNA project. It does not make any specific asset endorsed or suitable for
136
136
  every use case.
137
137
 
138
+ ## Consumption sidecars
139
+
140
+ Studio can export route-card and consumer-index drafts for applications that
141
+ use the consumption runtime. These files are separate from the `.kdna` export
142
+ and are disabled by default. They are review inputs, not claims that an asset
143
+ is automatically suitable for a task or ready for a production runtime.
144
+
145
+ Use the KDNA CLI review and evaluation commands before an application enables
146
+ a sidecar entry.
147
+
138
148
  ## Identity
139
149
 
140
150
  ```bash
@@ -82,7 +82,7 @@ Create (three entry paths):
82
82
  kdna-studio create <project-dir> --from-folder <source-dir> --name <@scope/name>
83
83
 
84
84
  Migrate (dev source or Studio project → canonical .kdna in one command):
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]
85
+ kdna-studio migrate <source-dir|project> --out <file.kdna> --name <@scope/name> --by <id> --statement <text> [--allow-incomplete] [--sign] [--passphrase <pw>|--passphrase-stdin]
86
86
  kdna-studio migrate <source-dir> --check --name <@scope/name> # pre-flight: report which fields would block the export without writing the .kdna
87
87
  kdna-studio audit-locks <project> [--type axiom|risk|stance|...] [--json] # list cards with missing Human Lock fields (per-card-type, per-field)
88
88
 
@@ -97,7 +97,9 @@ Authoring:
97
97
  kdna-studio card approve <project> <card-id|--all> --by <id> --statement <text> [--sign] [--passphrase <pass>]
98
98
  kdna-studio card unlock <project> <card-id> --by <id> --statement <text>
99
99
  kdna-studio compile <project> --out <dir>
100
- kdna-studio export <project> --format v1 --out <file.kdna> [--allow-incomplete] [--password <pw>|--password-stdin]
100
+ kdna-studio export <project> --out <file.kdna> [--allow-incomplete] [--password <pw>|--password-stdin]
101
+ kdna-studio export-route-card <domain-id> [--out=<path>] # Export route-card sidecar skeleton
102
+ kdna-studio export-consumer-index [--entries=<domain-ids>] [--out=<path>] # Export consumer-index skeleton
101
103
 
102
104
  AI Authoring (requires LLM config: kdna-studio llm config):
103
105
  kdna-studio distill <project> --ai # AI-driven candidate extraction from evidence
@@ -339,6 +341,13 @@ function titleFromName(name, assetId) {
339
341
  return assetId;
340
342
  }
341
343
 
344
+ function packageNameFromManifest(manifest = {}) {
345
+ if (manifest.name) return manifest.name;
346
+ const match = String(manifest.asset_id || '').match(/^kdna:([^:]+):(.+)$/);
347
+ if (match) return `@${match[1]}/${match[2]}`;
348
+ return manifest.title || manifest.asset_id || null;
349
+ }
350
+
342
351
  function publicManifestMetadata(raw = {}) {
343
352
  const copy = { ...raw };
344
353
  delete copy.registry;
@@ -365,8 +374,8 @@ function importedCard(type, fields = {}, id = null) {
365
374
  }
366
375
 
367
376
  // Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
368
- // misunderstanding card in two places — `patterns[]` (the v1 schema's
369
- // primary card list) and `reasoning.failure_modes[]` (the v1 schema's
377
+ // misunderstanding card in two places — `patterns[]` (the runtime schema's
378
+ // primary card list) and `reasoning.failure_modes[]` (the runtime schema's
370
379
  // "what bad judgment looks like" view). Both share the same `id`.
371
380
  // Prior version imported both, doubling every misunderstanding on
372
381
  // re-import. The dedup is by id: when a card with the same id is
@@ -407,15 +416,15 @@ function decodePayload(bytes, manifest = {}) {
407
416
  return cbor.decode(raw);
408
417
  }
409
418
 
410
- // V1 path: trust the producer (export-runtime) to have already shaped
411
- // each card's fields. The v1 producer normalises everything into a
419
+ // Runtime path: trust the producer (export-runtime) to have already shaped
420
+ // each card's fields. The producer normalises everything into a
412
421
  // canonical shape, so we just round-trip the raw object. Bug (#57):
413
422
  // the legacy path below explicitly constructs each field. The two
414
- // paths cannot be unified without losing either v1's per-field
423
+ // paths cannot be unified without losing either runtime per-field
415
424
  // guarantees or legacy's defensive defaults — the asymmetry is
416
425
  // intentional. Documented here so a future reader does not "fix" it
417
426
  // by collapsing one path into the other.
418
- function cardsFromV1Payload(payload) {
427
+ function cardsFromRuntimePayload(payload) {
419
428
  resetImportedIds();
420
429
  const cards = [];
421
430
  for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
@@ -460,7 +469,7 @@ function cardsFromV1Payload(payload) {
460
469
  }
461
470
  for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
462
471
  // Bug (#146): prior version dropped any payload.patterns entry
463
- // that lacked an explicit `type` field. The v1 schema lists
472
+ // that lacked an explicit `type` field. The runtime schema lists
464
473
  // misunderstandings in patterns[] without a `type` discriminator
465
474
  // (their shape is `{wrong, correct, key_distinction, why}`),
466
475
  // and several exporters omit `type` to keep the on-the-wire
@@ -516,8 +525,8 @@ function cardsFromV1Payload(payload) {
516
525
  for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
517
526
  // Bug (#145 follow-up): the prior version dropped three
518
527
  // judgment fields (failure_risk, applies_when, does_not_apply_when)
519
- // when reading failure_modes back out of a v1 asset. A Studio
520
- // project that round-tripped through `migrate --format v1` lost
528
+ // when reading failure_modes back out of an asset. A Studio
529
+ // project that round-tripped through `migrate` lost
521
530
  // these fields every time. The fix reads them through, matching
522
531
  // the export-side map in kdna-studio-core's buildPayload.
523
532
  pushImportedCard(cards, 'misunderstanding', {
@@ -638,7 +647,7 @@ function cardsFromLegacyPayload(payload) {
638
647
  function cardsFromPayload(payload) {
639
648
  if (!payload || typeof payload !== 'object') return [];
640
649
  if (payload.profile === 'judgment-profile-v1' || payload.core || payload.source_cards) {
641
- return cardsFromV1Payload(payload);
650
+ return cardsFromRuntimePayload(payload);
642
651
  }
643
652
  if (payload.kind === 'kdna.payload' || payload.judgment) {
644
653
  return cardsFromLegacyPayload(payload);
@@ -646,13 +655,13 @@ function cardsFromPayload(payload) {
646
655
  return [];
647
656
  }
648
657
 
649
- function buildV1Manifest(project, name) {
658
+ function buildManifest(project, name) {
650
659
  // Bug (#58): prior version was a hand-rolled manifest builder that
651
660
  // shared zero logic with the canonical buildManifest in
652
661
  // @aikdna/kdna-studio-core/src/export-runtime. The two paths
653
662
  // diverged on every field added since 1.0 (lineage, load_contract,
654
663
  // authoring block, etc.). The fix delegates the manifest shape to
655
- // kdna-studio-core's buildManifest so the v1 path is the single
664
+ // kdna-studio-core's buildManifest so runtime export has a single
656
665
  // source of truth.
657
666
  //
658
667
  // We still need a default manifest here (the exportRuntimeAsset
@@ -819,19 +828,19 @@ function importFromKdna(kdnaPath) {
819
828
  const manifest = JSON.parse(entries.get('kdna.json').toString());
820
829
  const lineage = {
821
830
  type: 'fork',
822
- parent_name: manifest.name || manifest.title || manifest.asset_id || null,
831
+ parent_name: packageNameFromManifest(manifest),
823
832
  parent_asset_uid: manifest.asset_uid || null,
824
833
  parent_version: manifest.version || null,
825
834
  parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
826
835
  };
827
836
 
828
- // Extract cards. Modern v1 packages carry `kdna.json` + `payload.kdnab`
837
+ // Extract cards. Current packages carry `kdna.json` + `payload.kdnab`
829
838
  // and use the unified judgment profile. Legacy packages split content
830
839
  // across KDNA_*.json files. Prefer payload.kdnab when present — it
831
840
  // covers all 8+ card types (axiom / boundary / risk / aesthetic /
832
841
  // ontology / misunderstanding / self_check / scenario / case /
833
842
  // reasoning / evolution_stage / stance / framework / term / banned_term)
834
- // via cardsFromPayload / cardsFromV1Payload.
843
+ // via cardsFromRuntimePayload / cardsFromLegacyPayload.
835
844
  const importedCards = [];
836
845
  if (entries.has('payload.kdnab')) {
837
846
  try {
@@ -841,7 +850,7 @@ function importFromKdna(kdnaPath) {
841
850
  fail(`Could not import cards from payload.kdnab: ${e.message}`);
842
851
  }
843
852
  } else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
844
- // Legacy fallback — used when the v1 asset has no payload.kdnab
853
+ // Source-tree fallback — used when an imported source has no payload.kdnab
845
854
  // (i.e. the asset predates the 0.7 split). Bug (#67): prior
846
855
  // version only handled axiom / ontology / boundary / misunderstanding
847
856
  // / self_check — 5 of the 14 card types — so every other type
@@ -940,7 +949,7 @@ function importFromKdna(kdnaPath) {
940
949
  }
941
950
 
942
951
  if (importedCards.length === 0) {
943
- fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab (v1) or KDNA_Core.json + KDNA_Patterns.json (legacy).`);
952
+ fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab or an importable KDNA_Core.json + KDNA_Patterns.json source tree.`);
944
953
  }
945
954
 
946
955
  // Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
@@ -1114,7 +1123,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
1114
1123
  importedCards.push(cardApi.createCard('self_check', { question: q }));
1115
1124
  }
1116
1125
  // Bug (aesthetic round-trip): prior version dropped
1117
- // `patterns.aesthetics` on import. `cardsFromV1Payload` and
1126
+ // `patterns.aesthetics` on import. `cardsFromRuntimePayload` and
1118
1127
  // `cardsFromLegacyPayload` both read the field, so a domain
1119
1128
  // that authored aesthetic cards in either format would lose
1120
1129
  // them when imported through `create --from-folder`. The fix
@@ -1779,7 +1788,7 @@ function cmdMigrate(args) {
1779
1788
  // attempted the full pipeline — read source folder, import to
1780
1789
  // Studio project, lock all cards, then export. An author migrating
1781
1790
  // 20 source-tree assets had no way to know in advance whether
1782
- // the assets would pass the v1.7.2+ Human Lock gate. The fix
1791
+ // the assets would pass the current Human Lock gate. The fix
1783
1792
  // adds `--check` which runs the import + the critical-missing
1784
1793
  // check + the lock-gate check, then prints a report and exits
1785
1794
  // without writing the .kdna file. Pair with `audit-locks` for
@@ -1804,7 +1813,9 @@ function cmdMigrate(args) {
1804
1813
  }
1805
1814
 
1806
1815
  const sourcePath = path.resolve(sourceDir);
1807
- const format = option(args, '--format');
1816
+ if (option(args, '--format')) {
1817
+ fail('The --format option is not supported. KDNA has one current asset format.');
1818
+ }
1808
1819
  const isStudioProject = fs.existsSync(path.join(sourcePath, 'studio.project.json'));
1809
1820
  let tmpDir = null;
1810
1821
  let projectPath = null;
@@ -1812,14 +1823,14 @@ function cmdMigrate(args) {
1812
1823
 
1813
1824
  // Wrap everything in try / finally so a mid-migrate failure (e.g. missing
1814
1825
  // critical fields, gate blocked, signing error) still cleans up tmpDir.
1815
- // Bug: prior code only cleaned up on the v1 and v2 success paths; any
1816
- // non-v1 export path that threw leaked `/tmp/kdna-migrate-*` entries
1826
+ // Bug: prior code only cleaned up on its success paths; any
1827
+ // older export path that threw leaked `/tmp/kdna-migrate-*` entries
1817
1828
  // indefinitely.
1818
1829
  try {
1819
1830
  // Step 1: import dev source, or use an existing Studio project directly.
1820
1831
  let creatorIdentity = null;
1821
1832
  try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
1822
- if (format === 'v1' && isStudioProject) {
1833
+ if (isStudioProject) {
1823
1834
  const loaded = readProject(sourcePath);
1824
1835
  project = loaded.project;
1825
1836
  projectPath = loaded.projectPath;
@@ -1936,34 +1947,8 @@ function cmdMigrate(args) {
1936
1947
 
1937
1948
  const absOut = path.resolve(out);
1938
1949
  const allowIncomplete = args.includes('--allow-incomplete');
1939
- // Step 4: v1 export or v2 compile + export
1940
- if (format === 'v1') {
1941
- exportProjectV1(project, name, absOut, { allowIncomplete });
1942
- if (tmpDir) {
1943
- try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
1944
- }
1945
- return;
1946
- }
1947
- const { result } = compileProject(tmpDir);
1948
- const files = { ...result.files };
1949
- files['README.md'] = compileApi.generateReadme(project);
1950
- files.LICENSE = project.license?.type || 'UNSPECIFIED';
1951
- files.mimetype = 'application/vnd.aikdna.kdna+zip';
1952
-
1953
- const entries = [['mimetype', files.mimetype]];
1954
- for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
1955
- entries.push([name, files[name]]);
1956
- }
1957
- const zip = buildZip(entries);
1958
- fs.mkdirSync(path.dirname(absOut), { recursive: true });
1959
- fs.writeFileSync(absOut, zip);
1960
- console.log(`Exported: ${absOut}`);
1961
- console.log(` Name: ${name}`);
1962
- console.log(` Cards: ${project.cards.length} (${locked} locked)`);
1963
- console.log(` Files: ${Object.keys(files).length}`);
1964
- console.log(` Build ID: ${result.identity.build_id}`);
1965
-
1966
- // Cleanup temp
1950
+ // Step 4: export the canonical runtime asset.
1951
+ exportProject(project, name, absOut, { allowIncomplete });
1967
1952
  if (tmpDir) {
1968
1953
  try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
1969
1954
  tmpDir = null;
@@ -1980,13 +1965,13 @@ function cmdMigrate(args) {
1980
1965
  }
1981
1966
  }
1982
1967
 
1983
- function exportProjectV1(project, name, outPath, opts = {}) {
1968
+ function exportProject(project, name, outPath, opts = {}) {
1984
1969
  let core;
1985
1970
  try { core = require('@aikdna/kdna-core'); } catch (e) {
1986
- fail('@aikdna/kdna-core@0.11.0+ is required for v1 export.', 2);
1971
+ fail('@aikdna/kdna-core is required for runtime export.', 2);
1987
1972
  }
1988
1973
  if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
1989
- fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for v1 export.', 2);
1974
+ fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for runtime export.', 2);
1990
1975
  }
1991
1976
  const criticalIssues = criticalAxiomIssues(project);
1992
1977
  if (criticalIssues.length > 0) {
@@ -1997,7 +1982,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
1997
1982
  );
1998
1983
  } else {
1999
1984
  fail(
2000
- `Cannot export v1: ${criticalIssues.length} critical axiom field issue(s).\n` +
1985
+ `Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
2001
1986
  ` These fields are required for domain routing (kdna-loader uses them to\n` +
2002
1987
  ` decide when to load this domain). Fix them first, or pass\n` +
2003
1988
  ` --allow-incomplete to bypass.\n` +
@@ -2014,13 +1999,13 @@ function exportProjectV1(project, name, outPath, opts = {}) {
2014
1999
  for (const issue of gate.issues) console.warn(` - ${issue.cardId}: ${issue.reason}`);
2015
2000
  } else {
2016
2001
  const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
2017
- fail(`Human Lock Gate blocked v1 export:\n - ${reasons}\nUse --allow-incomplete to bypass.`, EXIT.HUMAN_LOCK_REQUIRED);
2002
+ fail(`Human Lock Gate blocked export:\n - ${reasons}\nUse --allow-incomplete to bypass.`, EXIT.HUMAN_LOCK_REQUIRED);
2018
2003
  }
2019
2004
  }
2020
2005
  const lockedCards = (project.cards || []).filter(c => c.locked);
2021
- const v1Dir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-v1-')));
2006
+ const runtimeDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-runtime-')));
2022
2007
  try {
2023
- const manifestDefaults = buildV1Manifest(project, name);
2008
+ const manifestDefaults = buildManifest(project, name);
2024
2009
  const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
2025
2010
  asset_id: manifestDefaults.asset_id,
2026
2011
  asset_uid: manifestDefaults.asset_uid,
@@ -2030,15 +2015,15 @@ function exportProjectV1(project, name, outPath, opts = {}) {
2030
2015
  access: project.release?.access || project.source_manifest?.access || 'public',
2031
2016
  password: opts.password || undefined,
2032
2017
  });
2033
- writeFiles(v1Dir, runtimeAsset.files);
2018
+ writeFiles(runtimeDir, runtimeAsset.files);
2034
2019
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
2035
- core.pack(v1Dir, outPath);
2020
+ core.pack(runtimeDir, outPath);
2036
2021
  const vr = core.validate(outPath);
2037
- if (!vr.overall_valid) fail('v1 export validation failed: ' + (vr.problems || []).join('; '));
2038
- console.log('Exported (v1): ' + outPath);
2022
+ if (!vr.overall_valid) fail('runtime export validation failed: ' + (vr.problems || []).join('; '));
2023
+ console.log('Exported: ' + outPath);
2039
2024
  console.log(' Name: ' + name + ' Cards: ' + lockedCards.length + ' Validated: all gates pass');
2040
2025
  } finally {
2041
- try { fs.rmSync(v1Dir, { recursive: true }); } catch { /* cleanup */ }
2026
+ try { fs.rmSync(runtimeDir, { recursive: true }); } catch { /* cleanup */ }
2042
2027
  }
2043
2028
  }
2044
2029
 
@@ -2062,178 +2047,17 @@ function cmdCompile(args) {
2062
2047
  console.log(`Build ID: ${result.identity.build_id}`);
2063
2048
  }
2064
2049
 
2065
- function crc32(data) {
2066
- let crc = ~0;
2067
- for (const byte of data) {
2068
- crc ^= byte;
2069
- for (let k = 0; k < 8; k++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
2070
- }
2071
- return (~crc) >>> 0;
2072
- }
2073
-
2074
- function u16(parts, n) {
2075
- const b = Buffer.alloc(2);
2076
- b.writeUInt16LE(n);
2077
- parts.push(b);
2078
- }
2079
-
2080
- function u32(parts, n) {
2081
- const b = Buffer.alloc(4);
2082
- b.writeUInt32LE(n >>> 0);
2083
- parts.push(b);
2084
- }
2085
-
2086
- function buildZip(entries) {
2087
- const chunks = [];
2088
- const central = [];
2089
- let offset = 0;
2090
- for (const [name, content] of entries) {
2091
- const nameBuf = Buffer.from(name);
2092
- const raw = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
2093
- const compressed = name === 'mimetype' ? raw : zlib.deflateRawSync(raw);
2094
- const method = name === 'mimetype' ? 0 : 8;
2095
- const crc = crc32(raw);
2096
- const local = [];
2097
- u32(local, 0x04034b50);
2098
- u16(local, 20);
2099
- u16(local, 0x0800);
2100
- u16(local, method);
2101
- u16(local, 0);
2102
- u16(local, 0);
2103
- u32(local, crc);
2104
- u32(local, compressed.length);
2105
- u32(local, raw.length);
2106
- u16(local, nameBuf.length);
2107
- u16(local, 0);
2108
- local.push(nameBuf, compressed);
2109
- const localBuf = Buffer.concat(local);
2110
- chunks.push(localBuf);
2111
- central.push({ nameBuf, method, crc, compressedSize: compressed.length, size: raw.length, offset });
2112
- offset += localBuf.length;
2113
- }
2114
- const centralStart = offset;
2115
- for (const entry of central) {
2116
- const cd = [];
2117
- u32(cd, 0x02014b50);
2118
- u16(cd, 20);
2119
- u16(cd, 20);
2120
- u16(cd, 0x0800);
2121
- u16(cd, entry.method);
2122
- u16(cd, 0);
2123
- u16(cd, 0);
2124
- u32(cd, entry.crc);
2125
- u32(cd, entry.compressedSize);
2126
- u32(cd, entry.size);
2127
- u16(cd, entry.nameBuf.length);
2128
- u16(cd, 0);
2129
- u16(cd, 0);
2130
- u16(cd, 0);
2131
- u16(cd, 0);
2132
- u32(cd, 0);
2133
- u32(cd, entry.offset);
2134
- cd.push(entry.nameBuf);
2135
- const cdBuf = Buffer.concat(cd);
2136
- chunks.push(cdBuf);
2137
- offset += cdBuf.length;
2138
- }
2139
- const eocd = [];
2140
- u32(eocd, 0x06054b50);
2141
- u16(eocd, 0);
2142
- u16(eocd, 0);
2143
- u16(eocd, central.length);
2144
- u16(eocd, central.length);
2145
- u32(eocd, offset - centralStart);
2146
- u32(eocd, centralStart);
2147
- u16(eocd, 0);
2148
- chunks.push(Buffer.concat(eocd));
2149
- return Buffer.concat(chunks);
2150
- }
2151
-
2152
- function stableStringify(value) {
2153
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
2154
- if (value && typeof value === 'object') {
2155
- return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
2156
- }
2157
- return JSON.stringify(value);
2158
- }
2159
-
2160
- function manifestForSigning(manifest) {
2161
- // Mirrors the kdna-core implementation in
2162
- // packages/kdna-core/src/asset-reader.js#manifestForSignature /
2163
- // manifestForDigest. Anything not stripped here will end up in the
2164
- // signing payload, and a mismatch between producer (this CLI) and
2165
- // verifier (kdna-core) will cause the signature check to fail.
2166
- //
2167
- // Bug: prior version did not strip `_source` (only set by eval/loader
2168
- // helpers) and did not recursively strip `authoring.content_digest`,
2169
- // so a payload with either field would diverge from kdna-core's
2170
- // canonicalisation and report a false-positive verification failure.
2171
- const copy = { ...(manifest || {}) };
2172
- delete copy.signature;
2173
- delete copy.asset_digest;
2174
- delete copy.container_sha256;
2175
- delete copy.content_digest;
2176
- delete copy._source;
2177
- if (copy.authoring && typeof copy.authoring === 'object') {
2178
- const auth = { ...copy.authoring };
2179
- delete auth.content_digest;
2180
- copy.authoring = auth;
2181
- }
2182
- return copy;
2183
- }
2184
-
2185
- function canonicalPayload(files) {
2186
- return ['mimetype', ...Object.keys(files).filter((k) => k !== 'mimetype').sort()]
2187
- .filter((name) => name !== 'signature.json' && name !== '.DS_Store' && name !== 'build-receipt.json' && !name.startsWith('reports/'))
2188
- .map((name) => {
2189
- let content = name === 'mimetype' ? 'application/vnd.aikdna.kdna+zip' : files[name];
2190
- if (name.endsWith('.json')) {
2191
- const json = JSON.parse(content);
2192
- content = stableStringify(name === 'kdna.json' ? manifestForSigning(json) : json);
2193
- }
2194
- return `${name}:${crypto.createHash('sha256').update(Buffer.from(content)).digest('hex')}`;
2195
- })
2196
- .join('\n');
2197
- }
2198
-
2199
- function identityPaths() {
2200
- const dir = process.env.KDNA_IDENTITY_DIR || path.join(os.homedir(), '.kdna', 'identity');
2201
- return { privateKey: path.join(dir, 'kdna.key'), publicKey: path.join(dir, 'kdna.pub') };
2202
- }
2203
-
2204
- function publicKeyFingerprint(publicKeyPem) {
2205
- return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
2206
- }
2207
-
2208
- function applySignature(files, passphrase = null) {
2209
- const paths = identityPaths();
2210
- if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
2211
- fail('Signing requires KDNA identity keys. Run: kdna-studio identity init', EXIT.TRUST_FAILED);
2212
- }
2213
- const manifest = JSON.parse(files['kdna.json']);
2214
- const publicKeyPem = fs.readFileSync(paths.publicKey, 'utf8');
2215
- manifest.author = manifest.author || {};
2216
- manifest.author.pubkey = publicKeyFingerprint(publicKeyPem);
2217
- manifest.author.public_key_pem = publicKeyPem;
2218
- files['kdna.json'] = JSON.stringify(manifest, null, 2);
2219
-
2220
- const payload = canonicalPayload(files);
2221
- let privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
2222
- if (creatorApi.isEncryptedKey(privateKeyPem)) {
2223
- if (!passphrase) {
2224
- fail('Private key is encrypted. Export with: kdna-studio export ... --sign --passphrase <your-passphrase>', EXIT.TRUST_FAILED);
2225
- }
2226
- privateKeyPem = creatorApi.decryptPrivateKey(privateKeyPem, passphrase);
2227
- }
2228
- manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
2229
- files['kdna.json'] = JSON.stringify(manifest, null, 2);
2230
- }
2231
-
2232
2050
  function cmdExport(args) {
2233
2051
  const projectInput = args[0];
2234
2052
  const out = option(args, '--out');
2235
- if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--format v1] [--sign] [--allow-incomplete]');
2236
- if (option(args, '--format') === 'v1') {
2053
+ if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--allow-incomplete]');
2054
+ if (option(args, '--format')) {
2055
+ fail('The --format option is not supported. KDNA has one current asset format.');
2056
+ }
2057
+ if (args.includes('--sign')) {
2058
+ fail('Asset signing is a separate runtime step. Export first, then run: kdna sign <asset.kdna>');
2059
+ }
2060
+ {
2237
2061
  const { project } = readProject(projectInput);
2238
2062
  // BUG-11 (2026-06-27): previous logic required --password to be set
2239
2063
  // before --password-stdin would take effect. That meant callers using
@@ -2254,7 +2078,7 @@ function cmdExport(args) {
2254
2078
  if (process.stdin.isTTY) {
2255
2079
  fail(
2256
2080
  '--password-stdin requires the password to be piped in on stdin.\n' +
2257
- 'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --format v1 --out <file.kdna> --password-stdin\n' +
2081
+ 'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --out <file.kdna> --password-stdin\n' +
2258
2082
  'If you are running interactively, omit --password-stdin and you will be prompted.'
2259
2083
  );
2260
2084
  }
@@ -2266,7 +2090,7 @@ function cmdExport(args) {
2266
2090
  } else {
2267
2091
  // Bug (#55): prior version only consulted --password / --passphrase
2268
2092
  // here, so a caller who set KDNA_PASSPHRASE for the signing path
2269
- // (works via resolvePassphrase) found that the v1 encryption
2093
+ // (works via resolvePassphrase) found that the runtime encryption
2270
2094
  // password was silently null. Resolve from the same sources as
2271
2095
  // the signing path so the two stay in sync.
2272
2096
  password = process.env.KDNA_PASSPHRASE
@@ -2274,84 +2098,12 @@ function cmdExport(args) {
2274
2098
  || option(args, '--password')
2275
2099
  || option(args, '--passphrase');
2276
2100
  }
2277
- exportProjectV1(project, option(args, '--name') || project.name, path.resolve(out), {
2101
+ exportProject(project, option(args, '--name') || project.name, path.resolve(out), {
2278
2102
  allowIncomplete: args.includes('--allow-incomplete'),
2279
2103
  password,
2280
2104
  });
2281
2105
  return;
2282
2106
  }
2283
- const { project, result } = compileProject(projectInput);
2284
- const criticalIssues = criticalAxiomIssues(project);
2285
- if (criticalIssues.length > 0) {
2286
- if (args.includes('--allow-incomplete')) {
2287
- console.warn(
2288
- `--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
2289
- `The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
2290
- );
2291
- } else {
2292
- fail(
2293
- `Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
2294
- ` These fields are required for domain routing (kdna-loader uses them to\n` +
2295
- ` decide when to load this domain). Fix them first, or pass\n` +
2296
- ` --allow-incomplete to bypass.\n` +
2297
- ` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
2298
- (criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
2299
- EXIT.HUMAN_LOCK_REQUIRED,
2300
- );
2301
- }
2302
- }
2303
- const files = { ...result.files };
2304
- files['README.md'] = compileApi.generateReadme(project);
2305
- files.LICENSE = project.license?.type || 'UNSPECIFIED';
2306
- files.mimetype = 'application/vnd.aikdna.kdna+zip';
2307
-
2308
- // Recompute content_digest after README.md / LICENSE / mimetype are added
2309
- // so that manifest, receipt, and provenance report all agree with the final ZIP contents.
2310
- const finalDigest = compileApi.computeContentDigest(files);
2311
- result.identity.content_digest = finalDigest;
2312
-
2313
- // Update kdna.json manifest with final digest
2314
- const manifest = JSON.parse(files['kdna.json']);
2315
- manifest.content_digest = finalDigest;
2316
- if (manifest.authoring) manifest.authoring.content_digest = finalDigest;
2317
- files['kdna.json'] = JSON.stringify(manifest, null, 2);
2318
-
2319
- // Update build-receipt.json
2320
- let receiptData = JSON.parse(files['build-receipt.json']);
2321
- receiptData.content_digest = finalDigest;
2322
- files['build-receipt.json'] = JSON.stringify(receiptData, null, 2);
2323
-
2324
- // Update provenance report
2325
- const provenance = JSON.parse(files['reports/provenance-report.json']);
2326
- provenance.content_digest = finalDigest;
2327
- provenance.content_fingerprint = finalDigest;
2328
- files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
2329
-
2330
- if (args.includes('--sign')) applySignature(files, resolvePassphrase(args));
2331
-
2332
- const entries = [['mimetype', files.mimetype]];
2333
- for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
2334
- entries.push([name, files[name]]);
2335
- }
2336
- const zip = buildZip(entries);
2337
- const outPath = path.resolve(out);
2338
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
2339
- fs.writeFileSync(outPath, zip);
2340
-
2341
- const assetDigest = `sha256:${crypto.createHash('sha256').update(zip).digest('hex')}`;
2342
- receiptData = JSON.parse(files['build-receipt.json']);
2343
- receiptData.asset_path = outPath;
2344
- receiptData.asset_digest = assetDigest;
2345
- receiptData.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
2346
- fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receiptData, null, 2));
2347
- fs.writeFileSync(path.join(path.dirname(outPath), 'kdna.json'), files['kdna.json']);
2348
- fs.writeFileSync(path.join(path.dirname(outPath), 'provenance-report.json'), files['reports/provenance-report.json']);
2349
- fs.writeFileSync(path.join(path.dirname(outPath), 'quality-gate-report.json'), files['reports/quality-gate-report.json']);
2350
- fs.writeFileSync(path.join(path.dirname(outPath), 'human-lock-report.json'), files['reports/human-lock-report.json']);
2351
- fs.writeFileSync(path.join(path.dirname(outPath), 'eval-report.json'), files['reports/eval-report.json']);
2352
- console.log(`Exported canonical .kdna asset: ${outPath}`);
2353
- console.log(`Asset digest: ${assetDigest}`);
2354
- console.log(`Build ID: ${result.identity.build_id}`);
2355
2107
  }
2356
2108
 
2357
2109
  function cmdStudioInstall(args) {
@@ -2393,7 +2145,7 @@ function cmdLlm(args) {
2393
2145
  console.log(`Provider : ${cfg.provider || '(not set)'}`);
2394
2146
  console.log(`Model : ${cfg.model || '(not set)'}`);
2395
2147
  console.log(`Base URL : ${cfg.baseURL || '(not set)'}`);
2396
- console.log(`API Key : ${cfg.apiKey ? '********' + cfg.apiKey.slice(-4) : '(not set)'}`);
2148
+ console.log(`API Key : ${cfg.apiKey ? '(set)' : '(not set)'}`);
2397
2149
  return;
2398
2150
  }
2399
2151
  if (sub === 'config') {
@@ -2458,11 +2210,11 @@ function cmdReport(args) {
2458
2210
  process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
2459
2211
  }
2460
2212
 
2461
- // Audit-locks: list every card that would fail the v1.7.2+ Human Lock
2213
+ // Audit-locks: list every card that would fail the current Human Lock
2462
2214
  // gate because of missing or empty fields. Used during the
2463
2215
  // pre-migration review of source-tree assets (e.g. pro-20) so an
2464
2216
  // author can fill in the missing confidence / evidence_type / risk
2465
- // description / stance statement BEFORE running `migrate --format v1`.
2217
+ // description / stance statement BEFORE running `migrate`.
2466
2218
  //
2467
2219
  // Bug (UX pro-20 migration): prior version of `kdna-studio report`
2468
2220
  // returned a flat human_lock_gate.issues list, which mixed card
@@ -2484,7 +2236,7 @@ function cmdAuditLocks(args) {
2484
2236
  by_type: {},
2485
2237
  };
2486
2238
 
2487
- // Per-card-type required field lists. Mirrors the v1.7.2+ lockCard
2239
+ // Per-card-type required field lists. Mirrors the current lockCard
2488
2240
  // gate (kdna-studio-core/src/cards/index.js) so the output is
2489
2241
  // exactly the field set the gate will reject.
2490
2242
  const REQUIRED = {
@@ -2568,7 +2320,7 @@ function cmdAuditLocks(args) {
2568
2320
  console.log(`Cards: ${findings.card_count} (locked: ${findings.locked_count})`);
2569
2321
  const types = Object.keys(findings.by_type);
2570
2322
  if (types.length === 0) {
2571
- console.log('\nAll cards pass the v1.7.2+ Human Lock gate. Ready to migrate.');
2323
+ console.log('\nAll cards pass the current Human Lock gate. Ready to migrate.');
2572
2324
  return;
2573
2325
  }
2574
2326
  for (const t of types) {
@@ -3035,6 +2787,8 @@ try {
3035
2787
  else if (cmd === 'audit-locks') cmdAuditLocks(args.slice(1));
3036
2788
  else if (cmd === 'install') cmdStudioInstall(args.slice(1));
3037
2789
  else if (cmd === 'update') cmdStudioUpdate(args.slice(1));
2790
+ else if (cmd === 'export-route-card') cmdExportRouteCard(args.slice(1));
2791
+ else if (cmd === 'export-consumer-index') cmdExportConsumerIndex(args.slice(1));
3038
2792
  else {
3039
2793
  usage();
3040
2794
  fail(`Unknown command: ${cmd}`);
@@ -3043,3 +2797,68 @@ try {
3043
2797
  fail(err.message || String(err));
3044
2798
  }
3045
2799
  })();
2800
+
2801
+ function cmdExportRouteCard(args) {
2802
+ const domainId = args[0];
2803
+ if (!domainId) fail('Usage: kdna-studio export-route-card <domain-id> [--out=<path>]');
2804
+ const outArg = option(args, '--out');
2805
+ const outPath = outArg || `${slugSegment(domainId, 'domain')}.route-card.json`;
2806
+
2807
+ const card = {
2808
+ route_card: "0.1.0",
2809
+ domain_id: domainId,
2810
+ role: "primary",
2811
+ boundaries: {
2812
+ applies_when: [],
2813
+ does_not_apply_when: [],
2814
+ },
2815
+ neighbors: [],
2816
+ advisor_edges: [],
2817
+ provenance: {
2818
+ generated_by: "kdna-studio",
2819
+ generated_at: new Date().toISOString(),
2820
+ input_hash: crypto.createHash("sha256").update(domainId).digest("hex").slice(0, 12),
2821
+ review_status: "draft_generated",
2822
+ },
2823
+ };
2824
+
2825
+ const absOut = path.resolve(outPath);
2826
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
2827
+ fs.writeFileSync(absOut, JSON.stringify(card, null, 2) + "\n");
2828
+ console.log(`Route card skeleton written: ${absOut}`);
2829
+ }
2830
+
2831
+ function cmdExportConsumerIndex(args) {
2832
+ const entriesArg = option(args, '--entries');
2833
+ const outArg = option(args, '--out');
2834
+ const outPath = outArg || 'consumer-index.json';
2835
+
2836
+ const domainIds = entriesArg
2837
+ ? entriesArg.split(",").map((s) => s.trim()).filter(Boolean)
2838
+ : [];
2839
+
2840
+ const index = {
2841
+ consumer_index: "0.1.0",
2842
+ entries: domainIds.map((domainId) => ({
2843
+ domain_id: domainId,
2844
+ status: "draft_generated",
2845
+ enabled: false,
2846
+ route_preference: {
2847
+ primary_for: [],
2848
+ advisor_for: [],
2849
+ never_for: [],
2850
+ },
2851
+ provenance: {
2852
+ generated_by: "kdna-studio",
2853
+ generated_at: new Date().toISOString(),
2854
+ input_hash: crypto.createHash("sha256").update(domainId).digest("hex").slice(0, 12),
2855
+ source: "studio-export",
2856
+ },
2857
+ })),
2858
+ };
2859
+
2860
+ const absOut = path.resolve(outPath);
2861
+ fs.mkdirSync(path.dirname(absOut), { recursive: true });
2862
+ fs.writeFileSync(absOut, JSON.stringify(index, null, 2) + "\n");
2863
+ console.log(`Consumer index skeleton written: ${absOut}`);
2864
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-cli",
3
- "version": "0.8.12",
3
+ "version": "0.8.14",
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": {
@@ -41,12 +41,16 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "dependencies": {
44
- "@aikdna/kdna-studio-core": "^1.7.7",
44
+ "@aikdna/kdna-core": "0.15.12",
45
+ "@aikdna/kdna-studio-core": "1.7.11",
45
46
  "ajv": "^8.20.0",
46
47
  "ajv-formats": "^3.0.1",
47
48
  "cbor-x": "^1.6.4"
48
49
  },
49
- "devDependencies": {
50
- "@aikdna/kdna-core": "^0.15.9"
51
- }
50
+ "main": "index.js",
51
+ "directories": {
52
+ "test": "tests"
53
+ },
54
+ "devDependencies": {},
55
+ "author": ""
52
56
  }