@aikdna/kdna-studio-cli 0.8.13 → 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 +8 -8
- package/bin/kdna-studio.js +63 -313
- package/package.json +9 -5
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 --
|
|
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
|
|
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
|
|
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
|
|
101
|
-
kdna-studio export . --
|
|
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
|
|
119
|
-
`@aikdna/kdna-studio-core` to compile the Studio project into
|
|
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
|
|
122
|
+
A KDNA runtime export contains only these top-level entries:
|
|
123
123
|
|
|
124
124
|
- `mimetype`
|
|
125
125
|
- `kdna.json`
|
package/bin/kdna-studio.js
CHANGED
|
@@ -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> --
|
|
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,7 @@ 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> --
|
|
100
|
+
kdna-studio export <project> --out <file.kdna> [--allow-incomplete] [--password <pw>|--password-stdin]
|
|
101
101
|
kdna-studio export-route-card <domain-id> [--out=<path>] # Export route-card sidecar skeleton
|
|
102
102
|
kdna-studio export-consumer-index [--entries=<domain-ids>] [--out=<path>] # Export consumer-index skeleton
|
|
103
103
|
|
|
@@ -341,6 +341,13 @@ function titleFromName(name, assetId) {
|
|
|
341
341
|
return assetId;
|
|
342
342
|
}
|
|
343
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
|
+
|
|
344
351
|
function publicManifestMetadata(raw = {}) {
|
|
345
352
|
const copy = { ...raw };
|
|
346
353
|
delete copy.registry;
|
|
@@ -367,8 +374,8 @@ function importedCard(type, fields = {}, id = null) {
|
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
// Bug (#3 + #4 UX): payload.kdnab intentionally carries the same
|
|
370
|
-
// misunderstanding card in two places — `patterns[]` (the
|
|
371
|
-
// primary card list) and `reasoning.failure_modes[]` (the
|
|
377
|
+
// misunderstanding card in two places — `patterns[]` (the runtime schema's
|
|
378
|
+
// primary card list) and `reasoning.failure_modes[]` (the runtime schema's
|
|
372
379
|
// "what bad judgment looks like" view). Both share the same `id`.
|
|
373
380
|
// Prior version imported both, doubling every misunderstanding on
|
|
374
381
|
// re-import. The dedup is by id: when a card with the same id is
|
|
@@ -409,15 +416,15 @@ function decodePayload(bytes, manifest = {}) {
|
|
|
409
416
|
return cbor.decode(raw);
|
|
410
417
|
}
|
|
411
418
|
|
|
412
|
-
//
|
|
413
|
-
// each card's fields. The
|
|
419
|
+
// Runtime path: trust the producer (export-runtime) to have already shaped
|
|
420
|
+
// each card's fields. The producer normalises everything into a
|
|
414
421
|
// canonical shape, so we just round-trip the raw object. Bug (#57):
|
|
415
422
|
// the legacy path below explicitly constructs each field. The two
|
|
416
|
-
// paths cannot be unified without losing either
|
|
423
|
+
// paths cannot be unified without losing either runtime per-field
|
|
417
424
|
// guarantees or legacy's defensive defaults — the asymmetry is
|
|
418
425
|
// intentional. Documented here so a future reader does not "fix" it
|
|
419
426
|
// by collapsing one path into the other.
|
|
420
|
-
function
|
|
427
|
+
function cardsFromRuntimePayload(payload) {
|
|
421
428
|
resetImportedIds();
|
|
422
429
|
const cards = [];
|
|
423
430
|
for (const sourceCard of (Array.isArray(payload.source_cards) ? payload.source_cards : [])) {
|
|
@@ -462,7 +469,7 @@ function cardsFromV1Payload(payload) {
|
|
|
462
469
|
}
|
|
463
470
|
for (const pattern of (Array.isArray(payload.patterns) ? payload.patterns : [])) {
|
|
464
471
|
// Bug (#146): prior version dropped any payload.patterns entry
|
|
465
|
-
// that lacked an explicit `type` field. The
|
|
472
|
+
// that lacked an explicit `type` field. The runtime schema lists
|
|
466
473
|
// misunderstandings in patterns[] without a `type` discriminator
|
|
467
474
|
// (their shape is `{wrong, correct, key_distinction, why}`),
|
|
468
475
|
// and several exporters omit `type` to keep the on-the-wire
|
|
@@ -518,8 +525,8 @@ function cardsFromV1Payload(payload) {
|
|
|
518
525
|
for (const failureMode of (Array.isArray(payload.reasoning?.failure_modes) ? payload.reasoning?.failure_modes : [])) {
|
|
519
526
|
// Bug (#145 follow-up): the prior version dropped three
|
|
520
527
|
// judgment fields (failure_risk, applies_when, does_not_apply_when)
|
|
521
|
-
// when reading failure_modes back out of
|
|
522
|
-
// project that round-tripped through `migrate
|
|
528
|
+
// when reading failure_modes back out of an asset. A Studio
|
|
529
|
+
// project that round-tripped through `migrate` lost
|
|
523
530
|
// these fields every time. The fix reads them through, matching
|
|
524
531
|
// the export-side map in kdna-studio-core's buildPayload.
|
|
525
532
|
pushImportedCard(cards, 'misunderstanding', {
|
|
@@ -640,7 +647,7 @@ function cardsFromLegacyPayload(payload) {
|
|
|
640
647
|
function cardsFromPayload(payload) {
|
|
641
648
|
if (!payload || typeof payload !== 'object') return [];
|
|
642
649
|
if (payload.profile === 'judgment-profile-v1' || payload.core || payload.source_cards) {
|
|
643
|
-
return
|
|
650
|
+
return cardsFromRuntimePayload(payload);
|
|
644
651
|
}
|
|
645
652
|
if (payload.kind === 'kdna.payload' || payload.judgment) {
|
|
646
653
|
return cardsFromLegacyPayload(payload);
|
|
@@ -648,13 +655,13 @@ function cardsFromPayload(payload) {
|
|
|
648
655
|
return [];
|
|
649
656
|
}
|
|
650
657
|
|
|
651
|
-
function
|
|
658
|
+
function buildManifest(project, name) {
|
|
652
659
|
// Bug (#58): prior version was a hand-rolled manifest builder that
|
|
653
660
|
// shared zero logic with the canonical buildManifest in
|
|
654
661
|
// @aikdna/kdna-studio-core/src/export-runtime. The two paths
|
|
655
662
|
// diverged on every field added since 1.0 (lineage, load_contract,
|
|
656
663
|
// authoring block, etc.). The fix delegates the manifest shape to
|
|
657
|
-
// kdna-studio-core's buildManifest so
|
|
664
|
+
// kdna-studio-core's buildManifest so runtime export has a single
|
|
658
665
|
// source of truth.
|
|
659
666
|
//
|
|
660
667
|
// We still need a default manifest here (the exportRuntimeAsset
|
|
@@ -821,19 +828,19 @@ function importFromKdna(kdnaPath) {
|
|
|
821
828
|
const manifest = JSON.parse(entries.get('kdna.json').toString());
|
|
822
829
|
const lineage = {
|
|
823
830
|
type: 'fork',
|
|
824
|
-
parent_name: manifest
|
|
831
|
+
parent_name: packageNameFromManifest(manifest),
|
|
825
832
|
parent_asset_uid: manifest.asset_uid || null,
|
|
826
833
|
parent_version: manifest.version || null,
|
|
827
834
|
parent_asset_digest: manifest.content_digest || manifest.asset_digest || null,
|
|
828
835
|
};
|
|
829
836
|
|
|
830
|
-
// Extract cards.
|
|
837
|
+
// Extract cards. Current packages carry `kdna.json` + `payload.kdnab`
|
|
831
838
|
// and use the unified judgment profile. Legacy packages split content
|
|
832
839
|
// across KDNA_*.json files. Prefer payload.kdnab when present — it
|
|
833
840
|
// covers all 8+ card types (axiom / boundary / risk / aesthetic /
|
|
834
841
|
// ontology / misunderstanding / self_check / scenario / case /
|
|
835
842
|
// reasoning / evolution_stage / stance / framework / term / banned_term)
|
|
836
|
-
// via
|
|
843
|
+
// via cardsFromRuntimePayload / cardsFromLegacyPayload.
|
|
837
844
|
const importedCards = [];
|
|
838
845
|
if (entries.has('payload.kdnab')) {
|
|
839
846
|
try {
|
|
@@ -843,7 +850,7 @@ function importFromKdna(kdnaPath) {
|
|
|
843
850
|
fail(`Could not import cards from payload.kdnab: ${e.message}`);
|
|
844
851
|
}
|
|
845
852
|
} else if (entries.has('KDNA_Core.json') || entries.has('KDNA_Patterns.json')) {
|
|
846
|
-
//
|
|
853
|
+
// Source-tree fallback — used when an imported source has no payload.kdnab
|
|
847
854
|
// (i.e. the asset predates the 0.7 split). Bug (#67): prior
|
|
848
855
|
// version only handled axiom / ontology / boundary / misunderstanding
|
|
849
856
|
// / self_check — 5 of the 14 card types — so every other type
|
|
@@ -942,7 +949,7 @@ function importFromKdna(kdnaPath) {
|
|
|
942
949
|
}
|
|
943
950
|
|
|
944
951
|
if (importedCards.length === 0) {
|
|
945
|
-
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab
|
|
952
|
+
fail(`No cards could be extracted from ${absKdna}. Expected payload.kdnab or an importable KDNA_Core.json + KDNA_Patterns.json source tree.`);
|
|
946
953
|
}
|
|
947
954
|
|
|
948
955
|
// Bug #15 / #28 follow-up: surface the source's KDNA_Patterns /
|
|
@@ -1116,7 +1123,7 @@ function importFromFolder(sourceDir, projectDir, projectName, creatorIdentity, o
|
|
|
1116
1123
|
importedCards.push(cardApi.createCard('self_check', { question: q }));
|
|
1117
1124
|
}
|
|
1118
1125
|
// Bug (aesthetic round-trip): prior version dropped
|
|
1119
|
-
// `patterns.aesthetics` on import. `
|
|
1126
|
+
// `patterns.aesthetics` on import. `cardsFromRuntimePayload` and
|
|
1120
1127
|
// `cardsFromLegacyPayload` both read the field, so a domain
|
|
1121
1128
|
// that authored aesthetic cards in either format would lose
|
|
1122
1129
|
// them when imported through `create --from-folder`. The fix
|
|
@@ -1781,7 +1788,7 @@ function cmdMigrate(args) {
|
|
|
1781
1788
|
// attempted the full pipeline — read source folder, import to
|
|
1782
1789
|
// Studio project, lock all cards, then export. An author migrating
|
|
1783
1790
|
// 20 source-tree assets had no way to know in advance whether
|
|
1784
|
-
// the assets would pass the
|
|
1791
|
+
// the assets would pass the current Human Lock gate. The fix
|
|
1785
1792
|
// adds `--check` which runs the import + the critical-missing
|
|
1786
1793
|
// check + the lock-gate check, then prints a report and exits
|
|
1787
1794
|
// without writing the .kdna file. Pair with `audit-locks` for
|
|
@@ -1806,7 +1813,9 @@ function cmdMigrate(args) {
|
|
|
1806
1813
|
}
|
|
1807
1814
|
|
|
1808
1815
|
const sourcePath = path.resolve(sourceDir);
|
|
1809
|
-
|
|
1816
|
+
if (option(args, '--format')) {
|
|
1817
|
+
fail('The --format option is not supported. KDNA has one current asset format.');
|
|
1818
|
+
}
|
|
1810
1819
|
const isStudioProject = fs.existsSync(path.join(sourcePath, 'studio.project.json'));
|
|
1811
1820
|
let tmpDir = null;
|
|
1812
1821
|
let projectPath = null;
|
|
@@ -1814,14 +1823,14 @@ function cmdMigrate(args) {
|
|
|
1814
1823
|
|
|
1815
1824
|
// Wrap everything in try / finally so a mid-migrate failure (e.g. missing
|
|
1816
1825
|
// critical fields, gate blocked, signing error) still cleans up tmpDir.
|
|
1817
|
-
// Bug: prior code only cleaned up on
|
|
1818
|
-
//
|
|
1826
|
+
// Bug: prior code only cleaned up on its success paths; any
|
|
1827
|
+
// older export path that threw leaked `/tmp/kdna-migrate-*` entries
|
|
1819
1828
|
// indefinitely.
|
|
1820
1829
|
try {
|
|
1821
1830
|
// Step 1: import dev source, or use an existing Studio project directly.
|
|
1822
1831
|
let creatorIdentity = null;
|
|
1823
1832
|
try { creatorIdentity = creatorApi.loadIdentity(); } catch { /* proceed without */ }
|
|
1824
|
-
if (
|
|
1833
|
+
if (isStudioProject) {
|
|
1825
1834
|
const loaded = readProject(sourcePath);
|
|
1826
1835
|
project = loaded.project;
|
|
1827
1836
|
projectPath = loaded.projectPath;
|
|
@@ -1938,34 +1947,8 @@ function cmdMigrate(args) {
|
|
|
1938
1947
|
|
|
1939
1948
|
const absOut = path.resolve(out);
|
|
1940
1949
|
const allowIncomplete = args.includes('--allow-incomplete');
|
|
1941
|
-
// Step 4:
|
|
1942
|
-
|
|
1943
|
-
exportProjectV1(project, name, absOut, { allowIncomplete });
|
|
1944
|
-
if (tmpDir) {
|
|
1945
|
-
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* cleanup */ }
|
|
1946
|
-
}
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
const { result } = compileProject(tmpDir);
|
|
1950
|
-
const files = { ...result.files };
|
|
1951
|
-
files['README.md'] = compileApi.generateReadme(project);
|
|
1952
|
-
files.LICENSE = project.license?.type || 'UNSPECIFIED';
|
|
1953
|
-
files.mimetype = 'application/vnd.aikdna.kdna+zip';
|
|
1954
|
-
|
|
1955
|
-
const entries = [['mimetype', files.mimetype]];
|
|
1956
|
-
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
1957
|
-
entries.push([name, files[name]]);
|
|
1958
|
-
}
|
|
1959
|
-
const zip = buildZip(entries);
|
|
1960
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
1961
|
-
fs.writeFileSync(absOut, zip);
|
|
1962
|
-
console.log(`Exported: ${absOut}`);
|
|
1963
|
-
console.log(` Name: ${name}`);
|
|
1964
|
-
console.log(` Cards: ${project.cards.length} (${locked} locked)`);
|
|
1965
|
-
console.log(` Files: ${Object.keys(files).length}`);
|
|
1966
|
-
console.log(` Build ID: ${result.identity.build_id}`);
|
|
1967
|
-
|
|
1968
|
-
// Cleanup temp
|
|
1950
|
+
// Step 4: export the canonical runtime asset.
|
|
1951
|
+
exportProject(project, name, absOut, { allowIncomplete });
|
|
1969
1952
|
if (tmpDir) {
|
|
1970
1953
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch { /* best effort */ }
|
|
1971
1954
|
tmpDir = null;
|
|
@@ -1982,13 +1965,13 @@ function cmdMigrate(args) {
|
|
|
1982
1965
|
}
|
|
1983
1966
|
}
|
|
1984
1967
|
|
|
1985
|
-
function
|
|
1968
|
+
function exportProject(project, name, outPath, opts = {}) {
|
|
1986
1969
|
let core;
|
|
1987
1970
|
try { core = require('@aikdna/kdna-core'); } catch (e) {
|
|
1988
|
-
fail('@aikdna/kdna-core
|
|
1971
|
+
fail('@aikdna/kdna-core is required for runtime export.', 2);
|
|
1989
1972
|
}
|
|
1990
1973
|
if (!exportRuntime || typeof exportRuntime.exportRuntimeAsset !== 'function') {
|
|
1991
|
-
fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for
|
|
1974
|
+
fail('@aikdna/kdna-studio-core with exportRuntime.exportRuntimeAsset is required for runtime export.', 2);
|
|
1992
1975
|
}
|
|
1993
1976
|
const criticalIssues = criticalAxiomIssues(project);
|
|
1994
1977
|
if (criticalIssues.length > 0) {
|
|
@@ -1999,7 +1982,7 @@ function exportProjectV1(project, name, outPath, opts = {}) {
|
|
|
1999
1982
|
);
|
|
2000
1983
|
} else {
|
|
2001
1984
|
fail(
|
|
2002
|
-
`Cannot export
|
|
1985
|
+
`Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
|
|
2003
1986
|
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
2004
1987
|
` decide when to load this domain). Fix them first, or pass\n` +
|
|
2005
1988
|
` --allow-incomplete to bypass.\n` +
|
|
@@ -2016,13 +1999,13 @@ function exportProjectV1(project, name, outPath, opts = {}) {
|
|
|
2016
1999
|
for (const issue of gate.issues) console.warn(` - ${issue.cardId}: ${issue.reason}`);
|
|
2017
2000
|
} else {
|
|
2018
2001
|
const reasons = gate.issues.map((i) => `${i.cardId}: ${i.reason}`).join('\n - ');
|
|
2019
|
-
fail(`Human Lock Gate blocked
|
|
2002
|
+
fail(`Human Lock Gate blocked export:\n - ${reasons}\nUse --allow-incomplete to bypass.`, EXIT.HUMAN_LOCK_REQUIRED);
|
|
2020
2003
|
}
|
|
2021
2004
|
}
|
|
2022
2005
|
const lockedCards = (project.cards || []).filter(c => c.locked);
|
|
2023
|
-
const
|
|
2006
|
+
const runtimeDir = trackTempDir(fs.mkdtempSync(path.join(os.tmpdir(), 'kdna-runtime-')));
|
|
2024
2007
|
try {
|
|
2025
|
-
const manifestDefaults =
|
|
2008
|
+
const manifestDefaults = buildManifest(project, name);
|
|
2026
2009
|
const runtimeAsset = exportRuntime.exportRuntimeAsset(project, {
|
|
2027
2010
|
asset_id: manifestDefaults.asset_id,
|
|
2028
2011
|
asset_uid: manifestDefaults.asset_uid,
|
|
@@ -2032,15 +2015,15 @@ function exportProjectV1(project, name, outPath, opts = {}) {
|
|
|
2032
2015
|
access: project.release?.access || project.source_manifest?.access || 'public',
|
|
2033
2016
|
password: opts.password || undefined,
|
|
2034
2017
|
});
|
|
2035
|
-
writeFiles(
|
|
2018
|
+
writeFiles(runtimeDir, runtimeAsset.files);
|
|
2036
2019
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2037
|
-
core.pack(
|
|
2020
|
+
core.pack(runtimeDir, outPath);
|
|
2038
2021
|
const vr = core.validate(outPath);
|
|
2039
|
-
if (!vr.overall_valid) fail('
|
|
2040
|
-
console.log('Exported
|
|
2022
|
+
if (!vr.overall_valid) fail('runtime export validation failed: ' + (vr.problems || []).join('; '));
|
|
2023
|
+
console.log('Exported: ' + outPath);
|
|
2041
2024
|
console.log(' Name: ' + name + ' Cards: ' + lockedCards.length + ' Validated: all gates pass');
|
|
2042
2025
|
} finally {
|
|
2043
|
-
try { fs.rmSync(
|
|
2026
|
+
try { fs.rmSync(runtimeDir, { recursive: true }); } catch { /* cleanup */ }
|
|
2044
2027
|
}
|
|
2045
2028
|
}
|
|
2046
2029
|
|
|
@@ -2064,178 +2047,17 @@ function cmdCompile(args) {
|
|
|
2064
2047
|
console.log(`Build ID: ${result.identity.build_id}`);
|
|
2065
2048
|
}
|
|
2066
2049
|
|
|
2067
|
-
function crc32(data) {
|
|
2068
|
-
let crc = ~0;
|
|
2069
|
-
for (const byte of data) {
|
|
2070
|
-
crc ^= byte;
|
|
2071
|
-
for (let k = 0; k < 8; k++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
2072
|
-
}
|
|
2073
|
-
return (~crc) >>> 0;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
function u16(parts, n) {
|
|
2077
|
-
const b = Buffer.alloc(2);
|
|
2078
|
-
b.writeUInt16LE(n);
|
|
2079
|
-
parts.push(b);
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function u32(parts, n) {
|
|
2083
|
-
const b = Buffer.alloc(4);
|
|
2084
|
-
b.writeUInt32LE(n >>> 0);
|
|
2085
|
-
parts.push(b);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
function buildZip(entries) {
|
|
2089
|
-
const chunks = [];
|
|
2090
|
-
const central = [];
|
|
2091
|
-
let offset = 0;
|
|
2092
|
-
for (const [name, content] of entries) {
|
|
2093
|
-
const nameBuf = Buffer.from(name);
|
|
2094
|
-
const raw = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
|
|
2095
|
-
const compressed = name === 'mimetype' ? raw : zlib.deflateRawSync(raw);
|
|
2096
|
-
const method = name === 'mimetype' ? 0 : 8;
|
|
2097
|
-
const crc = crc32(raw);
|
|
2098
|
-
const local = [];
|
|
2099
|
-
u32(local, 0x04034b50);
|
|
2100
|
-
u16(local, 20);
|
|
2101
|
-
u16(local, 0x0800);
|
|
2102
|
-
u16(local, method);
|
|
2103
|
-
u16(local, 0);
|
|
2104
|
-
u16(local, 0);
|
|
2105
|
-
u32(local, crc);
|
|
2106
|
-
u32(local, compressed.length);
|
|
2107
|
-
u32(local, raw.length);
|
|
2108
|
-
u16(local, nameBuf.length);
|
|
2109
|
-
u16(local, 0);
|
|
2110
|
-
local.push(nameBuf, compressed);
|
|
2111
|
-
const localBuf = Buffer.concat(local);
|
|
2112
|
-
chunks.push(localBuf);
|
|
2113
|
-
central.push({ nameBuf, method, crc, compressedSize: compressed.length, size: raw.length, offset });
|
|
2114
|
-
offset += localBuf.length;
|
|
2115
|
-
}
|
|
2116
|
-
const centralStart = offset;
|
|
2117
|
-
for (const entry of central) {
|
|
2118
|
-
const cd = [];
|
|
2119
|
-
u32(cd, 0x02014b50);
|
|
2120
|
-
u16(cd, 20);
|
|
2121
|
-
u16(cd, 20);
|
|
2122
|
-
u16(cd, 0x0800);
|
|
2123
|
-
u16(cd, entry.method);
|
|
2124
|
-
u16(cd, 0);
|
|
2125
|
-
u16(cd, 0);
|
|
2126
|
-
u32(cd, entry.crc);
|
|
2127
|
-
u32(cd, entry.compressedSize);
|
|
2128
|
-
u32(cd, entry.size);
|
|
2129
|
-
u16(cd, entry.nameBuf.length);
|
|
2130
|
-
u16(cd, 0);
|
|
2131
|
-
u16(cd, 0);
|
|
2132
|
-
u16(cd, 0);
|
|
2133
|
-
u16(cd, 0);
|
|
2134
|
-
u32(cd, 0);
|
|
2135
|
-
u32(cd, entry.offset);
|
|
2136
|
-
cd.push(entry.nameBuf);
|
|
2137
|
-
const cdBuf = Buffer.concat(cd);
|
|
2138
|
-
chunks.push(cdBuf);
|
|
2139
|
-
offset += cdBuf.length;
|
|
2140
|
-
}
|
|
2141
|
-
const eocd = [];
|
|
2142
|
-
u32(eocd, 0x06054b50);
|
|
2143
|
-
u16(eocd, 0);
|
|
2144
|
-
u16(eocd, 0);
|
|
2145
|
-
u16(eocd, central.length);
|
|
2146
|
-
u16(eocd, central.length);
|
|
2147
|
-
u32(eocd, offset - centralStart);
|
|
2148
|
-
u32(eocd, centralStart);
|
|
2149
|
-
u16(eocd, 0);
|
|
2150
|
-
chunks.push(Buffer.concat(eocd));
|
|
2151
|
-
return Buffer.concat(chunks);
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
function stableStringify(value) {
|
|
2155
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
2156
|
-
if (value && typeof value === 'object') {
|
|
2157
|
-
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
2158
|
-
}
|
|
2159
|
-
return JSON.stringify(value);
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
function manifestForSigning(manifest) {
|
|
2163
|
-
// Mirrors the kdna-core implementation in
|
|
2164
|
-
// packages/kdna-core/src/asset-reader.js#manifestForSignature /
|
|
2165
|
-
// manifestForDigest. Anything not stripped here will end up in the
|
|
2166
|
-
// signing payload, and a mismatch between producer (this CLI) and
|
|
2167
|
-
// verifier (kdna-core) will cause the signature check to fail.
|
|
2168
|
-
//
|
|
2169
|
-
// Bug: prior version did not strip `_source` (only set by eval/loader
|
|
2170
|
-
// helpers) and did not recursively strip `authoring.content_digest`,
|
|
2171
|
-
// so a payload with either field would diverge from kdna-core's
|
|
2172
|
-
// canonicalisation and report a false-positive verification failure.
|
|
2173
|
-
const copy = { ...(manifest || {}) };
|
|
2174
|
-
delete copy.signature;
|
|
2175
|
-
delete copy.asset_digest;
|
|
2176
|
-
delete copy.container_sha256;
|
|
2177
|
-
delete copy.content_digest;
|
|
2178
|
-
delete copy._source;
|
|
2179
|
-
if (copy.authoring && typeof copy.authoring === 'object') {
|
|
2180
|
-
const auth = { ...copy.authoring };
|
|
2181
|
-
delete auth.content_digest;
|
|
2182
|
-
copy.authoring = auth;
|
|
2183
|
-
}
|
|
2184
|
-
return copy;
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
function canonicalPayload(files) {
|
|
2188
|
-
return ['mimetype', ...Object.keys(files).filter((k) => k !== 'mimetype').sort()]
|
|
2189
|
-
.filter((name) => name !== 'signature.json' && name !== '.DS_Store' && name !== 'build-receipt.json' && !name.startsWith('reports/'))
|
|
2190
|
-
.map((name) => {
|
|
2191
|
-
let content = name === 'mimetype' ? 'application/vnd.aikdna.kdna+zip' : files[name];
|
|
2192
|
-
if (name.endsWith('.json')) {
|
|
2193
|
-
const json = JSON.parse(content);
|
|
2194
|
-
content = stableStringify(name === 'kdna.json' ? manifestForSigning(json) : json);
|
|
2195
|
-
}
|
|
2196
|
-
return `${name}:${crypto.createHash('sha256').update(Buffer.from(content)).digest('hex')}`;
|
|
2197
|
-
})
|
|
2198
|
-
.join('\n');
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
function identityPaths() {
|
|
2202
|
-
const dir = process.env.KDNA_IDENTITY_DIR || path.join(os.homedir(), '.kdna', 'identity');
|
|
2203
|
-
return { privateKey: path.join(dir, 'kdna.key'), publicKey: path.join(dir, 'kdna.pub') };
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
function publicKeyFingerprint(publicKeyPem) {
|
|
2207
|
-
return `ed25519:${crypto.createHash('sha256').update(publicKeyPem).digest('hex')}`;
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
function applySignature(files, passphrase = null) {
|
|
2211
|
-
const paths = identityPaths();
|
|
2212
|
-
if (!fs.existsSync(paths.privateKey) || !fs.existsSync(paths.publicKey)) {
|
|
2213
|
-
fail('Signing requires KDNA identity keys. Run: kdna-studio identity init', EXIT.TRUST_FAILED);
|
|
2214
|
-
}
|
|
2215
|
-
const manifest = JSON.parse(files['kdna.json']);
|
|
2216
|
-
const publicKeyPem = fs.readFileSync(paths.publicKey, 'utf8');
|
|
2217
|
-
manifest.author = manifest.author || {};
|
|
2218
|
-
manifest.author.pubkey = publicKeyFingerprint(publicKeyPem);
|
|
2219
|
-
manifest.author.public_key_pem = publicKeyPem;
|
|
2220
|
-
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
2221
|
-
|
|
2222
|
-
const payload = canonicalPayload(files);
|
|
2223
|
-
let privateKeyPem = fs.readFileSync(paths.privateKey, 'utf8');
|
|
2224
|
-
if (creatorApi.isEncryptedKey(privateKeyPem)) {
|
|
2225
|
-
if (!passphrase) {
|
|
2226
|
-
fail('Private key is encrypted. Export with: kdna-studio export ... --sign --passphrase <your-passphrase>', EXIT.TRUST_FAILED);
|
|
2227
|
-
}
|
|
2228
|
-
privateKeyPem = creatorApi.decryptPrivateKey(privateKeyPem, passphrase);
|
|
2229
|
-
}
|
|
2230
|
-
manifest.signature = `ed25519:${crypto.sign(null, Buffer.from(payload), privateKeyPem).toString('hex')}`;
|
|
2231
|
-
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
2050
|
function cmdExport(args) {
|
|
2235
2051
|
const projectInput = args[0];
|
|
2236
2052
|
const out = option(args, '--out');
|
|
2237
|
-
if (!projectInput || !out) fail('Usage: kdna-studio export <project> --out <file.kdna> [--
|
|
2238
|
-
if (option(args, '--format')
|
|
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
|
+
{
|
|
2239
2061
|
const { project } = readProject(projectInput);
|
|
2240
2062
|
// BUG-11 (2026-06-27): previous logic required --password to be set
|
|
2241
2063
|
// before --password-stdin would take effect. That meant callers using
|
|
@@ -2256,7 +2078,7 @@ function cmdExport(args) {
|
|
|
2256
2078
|
if (process.stdin.isTTY) {
|
|
2257
2079
|
fail(
|
|
2258
2080
|
'--password-stdin requires the password to be piped in on stdin.\n' +
|
|
2259
|
-
'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --
|
|
2081
|
+
'Example: echo "$KDNA_PASSWORD" | kdna-studio export <project> --out <file.kdna> --password-stdin\n' +
|
|
2260
2082
|
'If you are running interactively, omit --password-stdin and you will be prompted.'
|
|
2261
2083
|
);
|
|
2262
2084
|
}
|
|
@@ -2268,7 +2090,7 @@ function cmdExport(args) {
|
|
|
2268
2090
|
} else {
|
|
2269
2091
|
// Bug (#55): prior version only consulted --password / --passphrase
|
|
2270
2092
|
// here, so a caller who set KDNA_PASSPHRASE for the signing path
|
|
2271
|
-
// (works via resolvePassphrase) found that the
|
|
2093
|
+
// (works via resolvePassphrase) found that the runtime encryption
|
|
2272
2094
|
// password was silently null. Resolve from the same sources as
|
|
2273
2095
|
// the signing path so the two stay in sync.
|
|
2274
2096
|
password = process.env.KDNA_PASSPHRASE
|
|
@@ -2276,84 +2098,12 @@ function cmdExport(args) {
|
|
|
2276
2098
|
|| option(args, '--password')
|
|
2277
2099
|
|| option(args, '--passphrase');
|
|
2278
2100
|
}
|
|
2279
|
-
|
|
2101
|
+
exportProject(project, option(args, '--name') || project.name, path.resolve(out), {
|
|
2280
2102
|
allowIncomplete: args.includes('--allow-incomplete'),
|
|
2281
2103
|
password,
|
|
2282
2104
|
});
|
|
2283
2105
|
return;
|
|
2284
2106
|
}
|
|
2285
|
-
const { project, result } = compileProject(projectInput);
|
|
2286
|
-
const criticalIssues = criticalAxiomIssues(project);
|
|
2287
|
-
if (criticalIssues.length > 0) {
|
|
2288
|
-
if (args.includes('--allow-incomplete')) {
|
|
2289
|
-
console.warn(
|
|
2290
|
-
`--allow-incomplete: bypassing ${criticalIssues.length} critical axiom-field check(s). ` +
|
|
2291
|
-
`The exported .kdna will load but kdna-loader may not match this domain on those signals.`,
|
|
2292
|
-
);
|
|
2293
|
-
} else {
|
|
2294
|
-
fail(
|
|
2295
|
-
`Cannot export: ${criticalIssues.length} critical axiom field issue(s).\n` +
|
|
2296
|
-
` These fields are required for domain routing (kdna-loader uses them to\n` +
|
|
2297
|
-
` decide when to load this domain). Fix them first, or pass\n` +
|
|
2298
|
-
` --allow-incomplete to bypass.\n` +
|
|
2299
|
-
` Issues:\n ` + criticalIssues.slice(0, 10).join('\n ') +
|
|
2300
|
-
(criticalIssues.length > 10 ? `\n ... and ${criticalIssues.length - 10} more` : ''),
|
|
2301
|
-
EXIT.HUMAN_LOCK_REQUIRED,
|
|
2302
|
-
);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
const files = { ...result.files };
|
|
2306
|
-
files['README.md'] = compileApi.generateReadme(project);
|
|
2307
|
-
files.LICENSE = project.license?.type || 'UNSPECIFIED';
|
|
2308
|
-
files.mimetype = 'application/vnd.aikdna.kdna+zip';
|
|
2309
|
-
|
|
2310
|
-
// Recompute content_digest after README.md / LICENSE / mimetype are added
|
|
2311
|
-
// so that manifest, receipt, and provenance report all agree with the final ZIP contents.
|
|
2312
|
-
const finalDigest = compileApi.computeContentDigest(files);
|
|
2313
|
-
result.identity.content_digest = finalDigest;
|
|
2314
|
-
|
|
2315
|
-
// Update kdna.json manifest with final digest
|
|
2316
|
-
const manifest = JSON.parse(files['kdna.json']);
|
|
2317
|
-
manifest.content_digest = finalDigest;
|
|
2318
|
-
if (manifest.authoring) manifest.authoring.content_digest = finalDigest;
|
|
2319
|
-
files['kdna.json'] = JSON.stringify(manifest, null, 2);
|
|
2320
|
-
|
|
2321
|
-
// Update build-receipt.json
|
|
2322
|
-
let receiptData = JSON.parse(files['build-receipt.json']);
|
|
2323
|
-
receiptData.content_digest = finalDigest;
|
|
2324
|
-
files['build-receipt.json'] = JSON.stringify(receiptData, null, 2);
|
|
2325
|
-
|
|
2326
|
-
// Update provenance report
|
|
2327
|
-
const provenance = JSON.parse(files['reports/provenance-report.json']);
|
|
2328
|
-
provenance.content_digest = finalDigest;
|
|
2329
|
-
provenance.content_fingerprint = finalDigest;
|
|
2330
|
-
files['reports/provenance-report.json'] = JSON.stringify(provenance, null, 2);
|
|
2331
|
-
|
|
2332
|
-
if (args.includes('--sign')) applySignature(files, resolvePassphrase(args));
|
|
2333
|
-
|
|
2334
|
-
const entries = [['mimetype', files.mimetype]];
|
|
2335
|
-
for (const name of Object.keys(files).filter(k => k !== 'mimetype').sort()) {
|
|
2336
|
-
entries.push([name, files[name]]);
|
|
2337
|
-
}
|
|
2338
|
-
const zip = buildZip(entries);
|
|
2339
|
-
const outPath = path.resolve(out);
|
|
2340
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2341
|
-
fs.writeFileSync(outPath, zip);
|
|
2342
|
-
|
|
2343
|
-
const assetDigest = `sha256:${crypto.createHash('sha256').update(zip).digest('hex')}`;
|
|
2344
|
-
receiptData = JSON.parse(files['build-receipt.json']);
|
|
2345
|
-
receiptData.asset_path = outPath;
|
|
2346
|
-
receiptData.asset_digest = assetDigest;
|
|
2347
|
-
receiptData.signature_status = args.includes('--sign') ? 'signed' : 'unsigned';
|
|
2348
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'build-receipt.json'), JSON.stringify(receiptData, null, 2));
|
|
2349
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'kdna.json'), files['kdna.json']);
|
|
2350
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'provenance-report.json'), files['reports/provenance-report.json']);
|
|
2351
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'quality-gate-report.json'), files['reports/quality-gate-report.json']);
|
|
2352
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'human-lock-report.json'), files['reports/human-lock-report.json']);
|
|
2353
|
-
fs.writeFileSync(path.join(path.dirname(outPath), 'eval-report.json'), files['reports/eval-report.json']);
|
|
2354
|
-
console.log(`Exported canonical .kdna asset: ${outPath}`);
|
|
2355
|
-
console.log(`Asset digest: ${assetDigest}`);
|
|
2356
|
-
console.log(`Build ID: ${result.identity.build_id}`);
|
|
2357
2107
|
}
|
|
2358
2108
|
|
|
2359
2109
|
function cmdStudioInstall(args) {
|
|
@@ -2460,11 +2210,11 @@ function cmdReport(args) {
|
|
|
2460
2210
|
process.exit(gate.blocked ? EXIT.HUMAN_LOCK_REQUIRED : EXIT.OK);
|
|
2461
2211
|
}
|
|
2462
2212
|
|
|
2463
|
-
// Audit-locks: list every card that would fail the
|
|
2213
|
+
// Audit-locks: list every card that would fail the current Human Lock
|
|
2464
2214
|
// gate because of missing or empty fields. Used during the
|
|
2465
2215
|
// pre-migration review of source-tree assets (e.g. pro-20) so an
|
|
2466
2216
|
// author can fill in the missing confidence / evidence_type / risk
|
|
2467
|
-
// description / stance statement BEFORE running `migrate
|
|
2217
|
+
// description / stance statement BEFORE running `migrate`.
|
|
2468
2218
|
//
|
|
2469
2219
|
// Bug (UX pro-20 migration): prior version of `kdna-studio report`
|
|
2470
2220
|
// returned a flat human_lock_gate.issues list, which mixed card
|
|
@@ -2486,7 +2236,7 @@ function cmdAuditLocks(args) {
|
|
|
2486
2236
|
by_type: {},
|
|
2487
2237
|
};
|
|
2488
2238
|
|
|
2489
|
-
// Per-card-type required field lists. Mirrors the
|
|
2239
|
+
// Per-card-type required field lists. Mirrors the current lockCard
|
|
2490
2240
|
// gate (kdna-studio-core/src/cards/index.js) so the output is
|
|
2491
2241
|
// exactly the field set the gate will reject.
|
|
2492
2242
|
const REQUIRED = {
|
|
@@ -2570,7 +2320,7 @@ function cmdAuditLocks(args) {
|
|
|
2570
2320
|
console.log(`Cards: ${findings.card_count} (locked: ${findings.locked_count})`);
|
|
2571
2321
|
const types = Object.keys(findings.by_type);
|
|
2572
2322
|
if (types.length === 0) {
|
|
2573
|
-
console.log('\nAll cards pass the
|
|
2323
|
+
console.log('\nAll cards pass the current Human Lock gate. Ready to migrate.');
|
|
2574
2324
|
return;
|
|
2575
2325
|
}
|
|
2576
2326
|
for (const t of types) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-cli",
|
|
3
|
-
"version": "0.8.
|
|
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-
|
|
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
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
"main": "index.js",
|
|
51
|
+
"directories": {
|
|
52
|
+
"test": "tests"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {},
|
|
55
|
+
"author": ""
|
|
52
56
|
}
|