@aikdna/kdna-core 0.15.8 → 0.15.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## v0.15.10 (2026-06-30)
5
+
6
+ **API**: export `parseSemver`, `compareSemver`, `satisfies` from public module.
7
+
8
+ These semver utilities were already used internally by kdna-core (Story 6 dependency
9
+ resolution) but were not part of the public API. Exporting them allows consumers
10
+ (e.g. `@aikdna/kdna-cli`) to use the canonical implementation rather than maintaining
11
+ a parallel copy.
12
+
13
+ - **`parseSemver(v)`** — parse a semver string into `{major, minor, patch}` or `null`
14
+ - **`compareSemver(a, b)`** — compare two version strings, returns `< 0 | 0 | > 0`
15
+ - **`satisfies(version, range)`** — test a version against a range (`^`, `~`, `>=`, etc.)
16
+
17
+ No breaking changes. Purely additive.
18
+
19
+ ## v0.15.9 (2026-06-28)
20
+
21
+ Story 12 — Asset inheritance (`extends` field, RFC #148 v2.x Phase 3).
22
+
23
+ - **`extends` field in `manifest.schema.json`**: assets may declare
24
+ `extends: "@scope/name@^1.0.0"` (string) or `extends: { name, version }`
25
+ (object). Distinct from `dependencies` (peer composition): `extends`
26
+ creates a single-inheritance hierarchy where the child specialises the parent.
27
+ - **`planLoad` extends resolution**: when a `resolveAsset` callback is provided
28
+ and the manifest declares `extends`, `planLoad` resolves the base asset and
29
+ records `extends_chain` in the plan output. Non-blocking — missing base
30
+ emits a WARNING issue, not a blocking error.
31
+ - **`loadAuthorized` passes `extends_chain`** from the plan to `loadV1Unsafe`
32
+ so the inheritance merge is applied at load time.
33
+ - **`loadV1Unsafe` inheritance merge**: when `extendsChain` is non-empty, the
34
+ base asset's content is loaded and merged with the child's content. Merge
35
+ rules: child axioms override parent axioms with the same `id`; parent axioms
36
+ not in child are inherited; same for boundaries (by `scope` text);
37
+ `highest_question` falls back to parent if child omits it. Non-blocking.
38
+ - **`result.extends_chain`** and **`result.inheritance_applied`** fields added
39
+ to load output when inheritance is applied.
40
+ - Blocks Story 13 (trust levels) per Section 6 sequencing.
41
+
3
42
  ## v0.15.8 (2026-06-28)
4
43
 
5
44
  Story 11 — RAG namespace isolation (RFC #148 v2.x Phase 3).
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
1
  {
2
- "name": "@aikdna/kdna-core",
3
- "version": "0.15.8",
4
- "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
- "engines": {
6
- "node": ">=18.0.0"
7
- },
8
- "type": "commonjs",
9
- "main": "src/index.js",
10
- "module": "src/index.mjs",
11
- "exports": {
12
- ".": {
13
- "import": "./src/index.mjs",
14
- "require": "./src/index.js",
15
- "types": "./src/types.d.ts"
2
+ "name": "@aikdna/kdna-core",
3
+ "version": "0.15.10",
4
+ "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
+ "engines": {
6
+ "node": ">=18.0.0"
16
7
  },
17
- "./package.json": "./package.json",
18
- "./schema/*": "./schema/*",
19
- "./v1": {
20
- "import": "./src/v1/index.mjs",
21
- "require": "./src/v1/index.js",
22
- "types": "./src/types.d.ts"
23
- }
24
- },
25
- "types": "src/types.d.ts",
26
- "scripts": {
27
- "test": "node ../../scripts/run-node-tests.js test/*.test.js && node -e \"const m = require('./src/index.js'); console.log('kdna-core exports:', Object.keys(m).join(', '));\"",
28
- "prepublishOnly": "npm run release:check",
29
- "release:check": "node scripts/check-release-readiness.js"
30
- },
31
- "files": [
32
- "src/",
33
- "schema/",
34
- "README.md",
35
- "CHANGELOG.md",
36
- "LICENSE",
37
- "NOTICE"
38
- ],
39
- "keywords": [
40
- "kdna",
41
- "kdna-core",
42
- "ai-agent",
43
- "judgment-assets"
44
- ],
45
- "license": "Apache-2.0",
46
- "repository": {
47
- "type": "git",
48
- "url": "git+https://github.com/aikdna/kdna.git",
49
- "directory": "packages/kdna-core"
50
- },
51
- "homepage": "https://aikdna.com",
52
- "peerDependencies": {
53
- "ajv": "^8.17.1",
54
- "ajv-formats": "^3.0.1"
55
- },
56
- "peerDependenciesMeta": {
57
- "ajv": {
58
- "optional": true
8
+ "type": "commonjs",
9
+ "main": "src/index.js",
10
+ "module": "src/index.mjs",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./src/index.mjs",
14
+ "require": "./src/index.js",
15
+ "types": "./src/types.d.ts"
16
+ },
17
+ "./package.json": "./package.json",
18
+ "./schema/*": "./schema/*",
19
+ "./v1": {
20
+ "import": "./src/v1/index.mjs",
21
+ "require": "./src/v1/index.js",
22
+ "types": "./src/types.d.ts"
23
+ }
24
+ },
25
+ "types": "src/types.d.ts",
26
+ "scripts": {
27
+ "test": "node ../../scripts/run-node-tests.js test/*.test.js && node -e \"const m = require('./src/index.js'); console.log('kdna-core exports:', Object.keys(m).join(', '));\"",
28
+ "prepublishOnly": "npm run release:check",
29
+ "release:check": "node scripts/check-release-readiness.js"
30
+ },
31
+ "files": [
32
+ "src/",
33
+ "schema/",
34
+ "README.md",
35
+ "CHANGELOG.md",
36
+ "LICENSE",
37
+ "NOTICE"
38
+ ],
39
+ "keywords": [
40
+ "kdna",
41
+ "kdna-core",
42
+ "ai-agent",
43
+ "judgment-assets"
44
+ ],
45
+ "license": "Apache-2.0",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/aikdna/kdna.git",
49
+ "directory": "packages/kdna-core"
50
+ },
51
+ "homepage": "https://aikdna.com",
52
+ "peerDependencies": {
53
+ "ajv": "^8.17.1",
54
+ "ajv-formats": "^3.0.1"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "ajv": {
58
+ "optional": true
59
+ },
60
+ "ajv-formats": {
61
+ "optional": true
62
+ }
59
63
  },
60
- "ajv-formats": {
61
- "optional": true
64
+ "dependencies": {
65
+ "@noble/hashes": "^1.8.0",
66
+ "cbor-x": "^1.6.4",
67
+ "ajv": "^8.20.0",
68
+ "ajv-formats": "^3.0.1"
62
69
  }
63
- },
64
- "dependencies": {
65
- "@noble/hashes": "^1.8.0",
66
- "cbor-x": "^1.6.4",
67
- "ajv": "^8.20.0",
68
- "ajv-formats": "^3.0.1"
69
- }
70
70
  }
@@ -201,6 +201,24 @@
201
201
  "type": "string"
202
202
  }
203
203
  },
204
+ "extends": {
205
+ "description": "Asset inheritance declaration (Story 12). This asset inherits judgment content from the named base asset. Distinct from 'dependencies' (peer composition): 'extends' creates a single-inheritance hierarchy where the child specialises the parent. The child's cards override the parent's cards with the same ID; parent cards not overridden are inherited. Format: '@scope/name@semver-range' string, or an object with 'name' and optional 'version' fields.",
206
+ "oneOf": [
207
+ {
208
+ "type": "string",
209
+ "description": "Scoped asset name with optional version, e.g. '@aikdna/sales@^1.0.0'."
210
+ },
211
+ {
212
+ "type": "object",
213
+ "required": ["name"],
214
+ "additionalProperties": false,
215
+ "properties": {
216
+ "name": { "type": "string", "description": "Scoped asset name, e.g. '@aikdna/sales'." },
217
+ "version": { "type": "string", "description": "Semver range, e.g. '^1.0.0'. Defaults to '*' if absent." }
218
+ }
219
+ }
220
+ ]
221
+ },
204
222
  "encryption": { "type": "object" },
205
223
  "load_contract": { "$ref": "load-contract.schema.json" },
206
224
  "scope": { "type": "object" },
@@ -311,28 +311,42 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
311
311
  // Skip if no public key available (unsigned assets are valid)
312
312
  return;
313
313
  }
314
- const axioms = coreData?.axioms || [];
315
- if (!Array.isArray(axioms) || axioms.length === 0) return;
314
+
315
+ // All card-type arrays in KDNA_Core.json that may carry human_lock.
316
+ // Ordered by how they appear in the compiled payload (compileCore in
317
+ // kdna-studio-core/src/compile/index.js).
318
+ const CORE_CARD_ARRAYS = [
319
+ ['axioms', 'axiom'],
320
+ ['ontology', 'ontology'],
321
+ ['frameworks', 'framework'],
322
+ ['boundaries', 'boundary'],
323
+ ['risks', 'risk'],
324
+ ['stances', 'stance'],
325
+ ];
316
326
 
317
327
  let verified = 0, missing = 0, invalid = 0;
318
- for (const ax of axioms) {
319
- const hl = ax.human_lock;
320
- if (!hl || !hl.signature) {
321
- // Only warn for locked cards without signature
322
- if (ax.status === 'locked' || ax.status === 'tested' || ax.status === 'published') {
323
- missing++;
328
+ for (const [arrayName, _cardType] of CORE_CARD_ARRAYS) {
329
+ const cards = coreData?.[arrayName];
330
+ if (!Array.isArray(cards) || cards.length === 0) continue;
331
+
332
+ for (const card of cards) {
333
+ const hl = card.human_lock;
334
+ if (!hl || !hl.signature) {
335
+ if (card.status === 'locked' || card.status === 'tested' || card.status === 'published') {
336
+ missing++;
337
+ }
338
+ continue;
339
+ }
340
+ try {
341
+ const payload = [card.id, hl.statement || '', hl.judgment_fingerprint || ''].join('\n');
342
+ const sig = Buffer.from(String(hl.signature).replace(/^ed25519:/, ''), 'hex');
343
+ const key = crypto.createPublicKey(publicKeyPEM);
344
+ const ok = crypto.verify(null, Buffer.from(payload), key, sig);
345
+ if (ok) { verified++; }
346
+ else { invalid++; }
347
+ } catch {
348
+ invalid++;
324
349
  }
325
- continue;
326
- }
327
- try {
328
- const payload = [ax.id, hl.statement || '', hl.judgment_fingerprint || ''].join('\n');
329
- const sig = Buffer.from(String(hl.signature).replace(/^ed25519:/, ''), 'hex');
330
- const key = crypto.createPublicKey(publicKeyPEM);
331
- const ok = crypto.verify(null, Buffer.from(payload), key, sig);
332
- if (ok) { verified++; }
333
- else { invalid++; }
334
- } catch {
335
- invalid++;
336
350
  }
337
351
  }
338
352
 
package/src/v1/index.js CHANGED
@@ -1273,6 +1273,55 @@ function planLoad(inputPath, opts = {}) {
1273
1273
  }
1274
1274
  }
1275
1275
 
1276
+ // Resolve extends chain if declared (Story 12)
1277
+ // 'extends' is single-inheritance: child overrides parent's cards with same ID.
1278
+ // Distinct from 'dependencies' (peer composition).
1279
+ if (manifest.extends) {
1280
+ const extendsDecl = manifest.extends;
1281
+ const extendsName = typeof extendsDecl === 'string'
1282
+ ? extendsDecl.replace(/@[^@]+$/, '') || extendsDecl // strip @version for resolver key
1283
+ : extendsDecl.name;
1284
+ const extendsVersion = typeof extendsDecl === 'string'
1285
+ ? (extendsDecl.match(/@([^@]+)$/) || [])[1] || '*'
1286
+ : (extendsDecl.version || '*');
1287
+
1288
+ if (typeof opts.resolveAsset === 'function') {
1289
+ try {
1290
+ const baseAsset = opts.resolveAsset(extendsName);
1291
+ if (!baseAsset) {
1292
+ plan.issues.push(buildLoadPlanIssue(
1293
+ 'KDNA_EXTENDS_NOT_FOUND',
1294
+ 'warning',
1295
+ `Base asset "${extendsName}" declared in 'extends' could not be resolved. Inheritance chain incomplete.`
1296
+ ));
1297
+ } else if (baseAsset.manifest && !satisfies(baseAsset.version, extendsVersion)) {
1298
+ plan.issues.push(buildLoadPlanIssue(
1299
+ 'KDNA_EXTENDS_VERSION_MISMATCH',
1300
+ 'warning',
1301
+ `Base asset "${extendsName}@${baseAsset.version}" does not satisfy extends range "${extendsVersion}".`
1302
+ ));
1303
+ } else if (baseAsset.path) {
1304
+ plan.extends_chain = [
1305
+ { name: extendsName, version: baseAsset.version || extendsVersion, path: baseAsset.path },
1306
+ ];
1307
+ }
1308
+ } catch (err) {
1309
+ plan.issues.push(buildLoadPlanIssue(
1310
+ 'KDNA_EXTENDS_RESOLUTION_FAILED',
1311
+ 'warning',
1312
+ `extends resolution failed: ${err.message}`
1313
+ ));
1314
+ }
1315
+ } else {
1316
+ // No resolver: extends chain unresolvable — warn, don't block
1317
+ plan.issues.push(buildLoadPlanIssue(
1318
+ 'KDNA_EXTENDS_RESOLVER_MISSING',
1319
+ 'warning',
1320
+ `Asset declares 'extends: ${JSON.stringify(manifest.extends)}' but no resolver was provided. Inheritance chain will not be applied.`
1321
+ ));
1322
+ }
1323
+ }
1324
+
1276
1325
  if (resolvedDeps.length > 0) {
1277
1326
  plan.resolved_dependencies = resolvedDeps.map(dep => ({
1278
1327
  name: dep.name,
@@ -1567,6 +1616,12 @@ module.exports = {
1567
1616
  unpack,
1568
1617
  loadV1,
1569
1618
  FORBIDDEN_OUTPUT_TERMS,
1619
+ // Semver utilities (Story 6 / Story 13) — exported so consumers
1620
+ // (e.g. kdna-cli) can use the canonical implementation instead of
1621
+ // duplicating it.
1622
+ parseSemver,
1623
+ compareSemver,
1624
+ satisfies,
1570
1625
  };
1571
1626
 
1572
1627
  // ─── loadV1 — v1 runtime loading / load contract ──────────────────────
@@ -1662,7 +1717,12 @@ function loadAuthorized(inputPath, opts = {}) {
1662
1717
  err.plan = plan;
1663
1718
  throw err;
1664
1719
  }
1665
- const mergedOpts = { ...opts, resolvedDependencies: plan.resolved_dependencies };
1720
+ const mergedOpts = {
1721
+ ...opts,
1722
+ resolvedDependencies: plan.resolved_dependencies,
1723
+ // Pass extends_chain from plan to loadV1Unsafe so inheritance is applied (Story 12)
1724
+ extendsChain: plan.extends_chain || [],
1725
+ };
1666
1726
  return loadV1Unsafe(inputPath, mergedOpts);
1667
1727
  }
1668
1728
 
@@ -1844,6 +1904,54 @@ function loadV1Unsafe(inputPath, opts = {}) {
1844
1904
  throw new Error(`unknown load profile: ${profile}`);
1845
1905
  }
1846
1906
 
1907
+ // Asset inheritance (Story 12): if the manifest declares 'extends' and
1908
+ // the plan resolved an extends_chain, load the base asset and merge its
1909
+ // content with the child's content. Child cards with the same ID override
1910
+ // the parent's; parent cards not overridden are inherited.
1911
+ if (opts.extendsChain && opts.extendsChain.length > 0 && result.content) {
1912
+ try {
1913
+ const base = opts.extendsChain[0];
1914
+ const baseLoaded = loadV1Unsafe(base.path, {
1915
+ ...opts,
1916
+ resolvedDependencies: [],
1917
+ extendsChain: [],
1918
+ });
1919
+ if (baseLoaded && baseLoaded.content) {
1920
+ const bc = baseLoaded.content;
1921
+ const cc = result.content;
1922
+ // Merge axioms: child overrides by id; parent axioms not in child are added
1923
+ if (bc.axioms || cc.axioms) {
1924
+ const childIds = new Set((cc.axioms || []).map((a) => a.id).filter(Boolean));
1925
+ const inheritedAxioms = (bc.axioms || []).filter((a) => a.id && !childIds.has(a.id));
1926
+ cc.axioms = [...inheritedAxioms, ...(cc.axioms || [])];
1927
+ }
1928
+ // Merge boundaries: child overrides by scope text; parent not overridden are inherited
1929
+ if (bc.boundaries || cc.boundaries) {
1930
+ const childScopes = new Set(
1931
+ (cc.boundaries || []).map((b) => (b.scope || '').toLowerCase().trim()),
1932
+ );
1933
+ const inheritedBoundaries = (bc.boundaries || []).filter(
1934
+ (b) => !childScopes.has((b.scope || '').toLowerCase().trim()),
1935
+ );
1936
+ cc.boundaries = [...inheritedBoundaries, ...(cc.boundaries || [])];
1937
+ }
1938
+ // Inherit highest_question from parent if child doesn't declare one
1939
+ if (!cc.highest_question && bc.highest_question) {
1940
+ cc.highest_question = bc.highest_question;
1941
+ }
1942
+ result.extends_chain = opts.extendsChain.map((e) => ({
1943
+ name: e.name,
1944
+ version: e.version,
1945
+ path: e.path,
1946
+ }));
1947
+ result.inheritance_applied = true;
1948
+ }
1949
+ } catch (_) {
1950
+ // extends merge is best-effort — never block load
1951
+ result.inheritance_applied = false;
1952
+ }
1953
+ }
1954
+
1847
1955
  if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
1848
1956
  result.resolved_dependencies = opts.resolvedDependencies.map(dep => {
1849
1957
  const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
@@ -1901,7 +2009,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
1901
2009
  if (c.note) text += 'Note: ' + c.note + '\n';
1902
2010
 
1903
2011
  let textOut = text.trim();
1904
- if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
2012
+ if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
1905
2013
  for (const dep of opts.resolvedDependencies) {
1906
2014
  const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
1907
2015
  if (depLoaded.text) {