@aikdna/kdna-core 0.15.6 → 0.15.8

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,32 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.15.8 (2026-06-28)
4
+
5
+ Story 11 — RAG namespace isolation (RFC #148 v2.x Phase 3).
6
+
7
+ - **`rag_namespace`** field added to each entry in `resolved_dependencies`.
8
+ Format: `name@version` (or bare `name` when version is absent). Provides a
9
+ stable scoped identifier for per-component namespace isolation.
10
+ - **`rag_isolation_policy`** object added to load output when
11
+ `resolved_dependencies` is non-empty:
12
+ `{ default: "fenced", cross_namespace_blocked: true, namespaces: [...] }`.
13
+ Consumers MUST NOT mix content across namespaces without explicit permission.
14
+ - **`--as=prompt` namespace header**: each component section in multi-asset
15
+ prompt output is now prefixed with `[NAMESPACE: name@version]` so RAG
16
+ systems can attribute and isolate content per source component (per SPEC
17
+ §13.8 source attribution).
18
+ - No breaking changes to existing single-asset load output or to
19
+ `resolved_dependencies` shape (fields are additive only).
20
+
21
+ ## v0.15.7 (2026-06-28)
22
+
23
+ Story 6 — dependencies runtime. Supports semver-matching local/registry dependency resolution and topological sort-based loading order.
24
+
25
+ - **Dependencies manifest schema support**: Explicitly declared `"dependencies"` as a first-class manifest property.
26
+ - **Topological Sorting Resolver**: Added a robust, pure post-order DFS topological sorter with circular cycle, missing dependency, and version range mismatch checks.
27
+ - **Topological Prompt Composition**: Enabled recursive, multi-domain prompt composition and flat topological JSON return inside `loadAuthorized`.
28
+
29
+
3
30
  ## v0.15.6 (2026-06-28)
4
31
 
5
32
  Story 5 — Bundle payload type. Bumps KDNA manifest format schema to v2, introduces bundle-profile-v1 payload profile, and starts KDNA v1 format deprecation window.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.15.6",
3
+ "version": "0.15.8",
4
4
  "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -195,6 +195,12 @@
195
195
  }
196
196
  }
197
197
  },
198
+ "dependencies": {
199
+ "type": "object",
200
+ "additionalProperties": {
201
+ "type": "string"
202
+ }
203
+ },
198
204
  "encryption": { "type": "object" },
199
205
  "load_contract": { "$ref": "load-contract.schema.json" },
200
206
  "scope": { "type": "object" },
package/src/v1/index.js CHANGED
@@ -1241,6 +1241,46 @@ function planLoad(inputPath, opts = {}) {
1241
1241
  }
1242
1242
 
1243
1243
  const manifest = v1.manifest;
1244
+
1245
+ // Resolve dependencies if declared (Story 6)
1246
+ const resolvedDeps = [];
1247
+ if (manifest.dependencies && typeof manifest.dependencies === 'object' && Object.keys(manifest.dependencies).length > 0) {
1248
+ if (typeof opts.resolveAsset === 'function') {
1249
+ try {
1250
+ const resolved = resolveDependencies(manifest, opts.resolveAsset);
1251
+ resolvedDeps.push(...resolved);
1252
+ } catch (err) {
1253
+ plan.state = 'invalid';
1254
+ plan.required_action = 'block';
1255
+ plan.can_load_now = false;
1256
+ plan.issues.push(buildLoadPlanIssue(
1257
+ 'KDNA_DEPENDENCY_RESOLUTION_FAILED',
1258
+ 'blocking',
1259
+ err.message
1260
+ ));
1261
+ return finalizeLoadPlan(plan);
1262
+ }
1263
+ } else {
1264
+ plan.state = 'invalid';
1265
+ plan.required_action = 'block';
1266
+ plan.can_load_now = false;
1267
+ plan.issues.push(buildLoadPlanIssue(
1268
+ 'KDNA_DEPENDENCY_RESOLVER_MISSING',
1269
+ 'blocking',
1270
+ 'Asset has dependencies but no dependency resolver callback was provided.'
1271
+ ));
1272
+ return finalizeLoadPlan(plan);
1273
+ }
1274
+ }
1275
+
1276
+ if (resolvedDeps.length > 0) {
1277
+ plan.resolved_dependencies = resolvedDeps.map(dep => ({
1278
+ name: dep.name,
1279
+ version: dep.version,
1280
+ path: dep.path
1281
+ }));
1282
+ }
1283
+
1244
1284
  const payloadDeclaredEncrypted =
1245
1285
  manifest.payload && manifest.payload.encrypted === true;
1246
1286
  const encryptedEntries = Array.isArray(manifest.encryption && manifest.encryption.encrypted_entries)
@@ -1622,7 +1662,8 @@ function loadAuthorized(inputPath, opts = {}) {
1622
1662
  err.plan = plan;
1623
1663
  throw err;
1624
1664
  }
1625
- return loadV1Unsafe(inputPath, opts);
1665
+ const mergedOpts = { ...opts, resolvedDependencies: plan.resolved_dependencies };
1666
+ return loadV1Unsafe(inputPath, mergedOpts);
1626
1667
  }
1627
1668
 
1628
1669
  function loadV1(inputPath, opts = {}) {
@@ -1803,6 +1844,38 @@ function loadV1Unsafe(inputPath, opts = {}) {
1803
1844
  throw new Error(`unknown load profile: ${profile}`);
1804
1845
  }
1805
1846
 
1847
+ if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
1848
+ result.resolved_dependencies = opts.resolvedDependencies.map(dep => {
1849
+ const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
1850
+ // RAG namespace (Story 11): scoped identifier for namespace isolation.
1851
+ // Format: name@version (or just name when version is absent).
1852
+ const ragNamespace = dep.name
1853
+ ? (dep.version ? `${dep.name}@${dep.version}` : dep.name)
1854
+ : null;
1855
+ return {
1856
+ name: dep.name,
1857
+ version: dep.version,
1858
+ path: dep.path,
1859
+ rag_namespace: ragNamespace,
1860
+ status: depLoaded.status,
1861
+ profile: depLoaded.profile,
1862
+ content: depLoaded.content
1863
+ };
1864
+ });
1865
+
1866
+ // RAG isolation policy (Story 11): consumers MUST NOT mix content
1867
+ // across namespaces without explicit permission. cross_namespace_blocked
1868
+ // is the default; a consumer may opt in to cross-namespace access by
1869
+ // setting its own policy, but Core never does it silently.
1870
+ result.rag_isolation_policy = {
1871
+ default: 'fenced',
1872
+ cross_namespace_blocked: true,
1873
+ namespaces: result.resolved_dependencies
1874
+ .map(d => d.rag_namespace)
1875
+ .filter(Boolean),
1876
+ };
1877
+ }
1878
+
1806
1879
  if (as === 'prompt') {
1807
1880
  const c = result.content;
1808
1881
  if (!c || (c.scenarios && c.scenarios.length === 0 && !c.highest_question && !(c.axioms && c.axioms.length) && !(c.boundaries && c.boundaries.length))) {
@@ -1826,8 +1899,141 @@ function loadV1Unsafe(inputPath, opts = {}) {
1826
1899
  if (c.failure_modes && c.failure_modes.length) text += 'Failure modes:\n' + c.failure_modes.map((f) => '- ' + renderPromptItem(f)).join('\n') + '\n';
1827
1900
  if (c.patterns && c.patterns.length) text += 'Patterns:\n' + c.patterns.map((p) => '- ' + renderPromptItem(p)).join('\n') + '\n';
1828
1901
  if (c.note) text += 'Note: ' + c.note + '\n';
1829
- return { status: result.status, profile: result.profile, text: text.trim() };
1902
+
1903
+ let textOut = text.trim();
1904
+ if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
1905
+ for (const dep of opts.resolvedDependencies) {
1906
+ const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
1907
+ if (depLoaded.text) {
1908
+ // Namespace header (Story 11): each component section is prefixed
1909
+ // with [NAMESPACE: id] so consumers can isolate RAG content per source.
1910
+ const ns = dep.name
1911
+ ? (dep.version ? `${dep.name}@${dep.version}` : dep.name)
1912
+ : null;
1913
+ const nsHeader = ns ? `[NAMESPACE: ${ns}]\n` : '';
1914
+ textOut += '\n\n---\n\n' + nsHeader + depLoaded.text;
1915
+ }
1916
+ }
1917
+ }
1918
+ return { status: result.status, profile: result.profile, text: textOut };
1830
1919
  }
1831
1920
 
1832
1921
  return result;
1833
1922
  }
1923
+
1924
+ // ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
1925
+
1926
+ function parseSemver(v) {
1927
+ if (!v) return null;
1928
+ const match = v.trim().match(/^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:-[a-zA-Z0-9.]+)?/);
1929
+ if (!match) return null;
1930
+ return {
1931
+ major: parseInt(match[1], 10),
1932
+ minor: parseInt(match[2], 10),
1933
+ patch: parseInt(match[3], 10)
1934
+ };
1935
+ }
1936
+
1937
+ function compareSemver(v1, v2) {
1938
+ const p1 = parseSemver(v1);
1939
+ const p2 = parseSemver(v2);
1940
+ if (!p1 || !p2) return 0;
1941
+ if (p1.major !== p2.major) return p1.major - p2.major;
1942
+ if (p1.minor !== p2.minor) return p1.minor - p2.minor;
1943
+ return p1.patch - p2.patch;
1944
+ }
1945
+
1946
+ function satisfies(version, range) {
1947
+ if (!range || range === '*' || range.trim() === '') return true;
1948
+ const r = range.trim();
1949
+
1950
+ // Specific version comparison (e.g. "1.2.3")
1951
+ if (/^[0-9]/.test(r)) {
1952
+ return compareSemver(version, r) === 0;
1953
+ }
1954
+
1955
+ // ^ range comparison (e.g. "^1.2.3")
1956
+ if (r.startsWith('^')) {
1957
+ const min = r.slice(1);
1958
+ const parsedMin = parseSemver(min);
1959
+ if (!parsedMin) return false;
1960
+ const parsedVer = parseSemver(version);
1961
+ if (!parsedVer) return false;
1962
+ // same major, and version >= min
1963
+ if (parsedVer.major !== parsedMin.major) return false;
1964
+ return compareSemver(version, min) >= 0;
1965
+ }
1966
+
1967
+ // ~ range comparison (e.g. "~1.2.3")
1968
+ if (r.startsWith('~')) {
1969
+ const min = r.slice(1);
1970
+ const parsedMin = parseSemver(min);
1971
+ if (!parsedMin) return false;
1972
+ const parsedVer = parseSemver(version);
1973
+ if (!parsedVer) return false;
1974
+ // same major, same minor, and version >= min
1975
+ if (parsedVer.major !== parsedMin.major || parsedVer.minor !== parsedMin.minor) return false;
1976
+ return compareSemver(version, min) >= 0;
1977
+ }
1978
+
1979
+ // Custom ranges like ">=1.0.0 <2.0.0"
1980
+ if (r.includes(' ')) {
1981
+ const parts = r.split(/\s+/);
1982
+ return parts.every(part => satisfies(version, part));
1983
+ }
1984
+
1985
+ if (r.startsWith('>=')) {
1986
+ return compareSemver(version, r.slice(2)) >= 0;
1987
+ }
1988
+ if (r.startsWith('>')) {
1989
+ return compareSemver(version, r.slice(1)) > 0;
1990
+ }
1991
+ if (r.startsWith('<=')) {
1992
+ return compareSemver(version, r.slice(2)) <= 0;
1993
+ }
1994
+ if (r.startsWith('<')) {
1995
+ return compareSemver(version, r.slice(1)) < 0;
1996
+ }
1997
+
1998
+ return false;
1999
+ }
2000
+
2001
+ function resolveDependencies(manifest, resolveAssetCallback, seen = new Set(), stack = []) {
2002
+ const resolved = [];
2003
+ const deps = manifest.dependencies || {};
2004
+
2005
+ for (const [name, range] of Object.entries(deps)) {
2006
+ if (stack.includes(name)) {
2007
+ throw new Error(`Circular dependency detected: ${stack.join(' -> ')} -> ${name}`);
2008
+ }
2009
+
2010
+ if (seen.has(name)) continue;
2011
+
2012
+ // Resolve the dependency using the callback
2013
+ const depAsset = resolveAssetCallback(name);
2014
+ if (!depAsset) {
2015
+ throw new Error(`Dependency not satisfied: "${name}" matching "${range}" is not installed.`);
2016
+ }
2017
+
2018
+ // Check if the resolved version satisfies the range
2019
+ if (!satisfies(depAsset.version, range)) {
2020
+ throw new Error(`Dependency mismatch: installed "${name}" version "${depAsset.version}" does not satisfy range "${range}".`);
2021
+ }
2022
+
2023
+ // Recurse
2024
+ stack.push(name);
2025
+ const subResolved = resolveDependencies(depAsset.manifest, resolveAssetCallback, seen, stack);
2026
+ stack.pop();
2027
+
2028
+ resolved.push(...subResolved);
2029
+ resolved.push({
2030
+ name,
2031
+ version: depAsset.version,
2032
+ path: depAsset.path,
2033
+ manifest: depAsset.manifest
2034
+ });
2035
+ seen.add(name);
2036
+ }
2037
+
2038
+ return resolved;
2039
+ }