@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 +27 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +6 -0
- package/src/v1/index.js +208 -2
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|