@aikdna/kdna-core 0.2.3 → 0.3.0
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/package.json +1 -1
- package/schema/Composition_Policy.schema.json +130 -0
- package/schema/KDNA_Cases.schema.json +28 -2
- package/schema/KDNA_Cluster.schema.json +103 -24
- package/schema/KDNA_Core.schema.json +188 -7
- package/schema/KDNA_Evolution.schema.json +56 -3
- package/schema/KDNA_Patterns.schema.json +267 -6
- package/schema/KDNA_Reasoning.schema.json +38 -18
- package/schema/KDNA_Scenarios.schema.json +36 -6
- package/schema/eval.schema.json +58 -0
- package/src/compose.js +255 -1
- package/src/index.mjs +1 -1
- package/src/lint-pure.js +32 -0
- package/src/loader.js +60 -0
- package/src/validate-pure.js +1 -1
package/src/compose.js
CHANGED
|
@@ -120,4 +120,258 @@ function loadAndCompose(dataMaps, options = {}) {
|
|
|
120
120
|
return { domains, context, activeIndices };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Compose context with source attribution — every axiom, misunderstanding,
|
|
125
|
+
* banned term, and self-check is prefixed with its origin domain.
|
|
126
|
+
*
|
|
127
|
+
* @param {Array<{id:string, core:object, patterns:object}>} domains
|
|
128
|
+
* @param {object} [options]
|
|
129
|
+
* @param {string} [options.separator] — section separator
|
|
130
|
+
* @returns {{context: string, attributionMap: object}}
|
|
131
|
+
*/
|
|
132
|
+
function composeContextWithAttribution(domains, options = {}) {
|
|
133
|
+
if (!domains || !domains.length) return { context: '', attributionMap: {} };
|
|
134
|
+
|
|
135
|
+
const separator = options.separator || '\n\n---\n\n';
|
|
136
|
+
const lines = [];
|
|
137
|
+
const attributionMap = {};
|
|
138
|
+
let axiomIndex = 0;
|
|
139
|
+
let misreadingIndex = 0;
|
|
140
|
+
let termIndex = 0;
|
|
141
|
+
let checkIndex = 0;
|
|
142
|
+
|
|
143
|
+
for (const domain of domains) {
|
|
144
|
+
if (!domain || !domain.core) continue;
|
|
145
|
+
const name = domain.id || domain.core.meta?.domain || 'unknown';
|
|
146
|
+
const core = domain.core;
|
|
147
|
+
const patterns = domain.patterns || {};
|
|
148
|
+
|
|
149
|
+
lines.push(`## [${name}] Domain cognition`);
|
|
150
|
+
|
|
151
|
+
if (core.axioms?.length) {
|
|
152
|
+
lines.push(`### Axioms`);
|
|
153
|
+
for (const a of core.axioms) {
|
|
154
|
+
const tag = `[${name}:axiom.${a.id}]`;
|
|
155
|
+
lines.push(`- ${tag} ${a.one_sentence}`);
|
|
156
|
+
if (a.applies_when?.length) {
|
|
157
|
+
lines.push(` APPLIES WHEN: ${a.applies_when.join('; ')}`);
|
|
158
|
+
}
|
|
159
|
+
if (a.does_not_apply_when?.length) {
|
|
160
|
+
lines.push(` DOES NOT APPLY WHEN: ${a.does_not_apply_when.join('; ')}`);
|
|
161
|
+
}
|
|
162
|
+
if (a.failure_risk) lines.push(` RISK IF MISAPPLIED: ${a.failure_risk}`);
|
|
163
|
+
attributionMap[tag] = { domain: name, type: 'axiom', id: a.id, index: axiomIndex++ };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (patterns.misunderstandings?.length) {
|
|
168
|
+
lines.push(`### Misunderstandings`);
|
|
169
|
+
for (const m of patterns.misunderstandings) {
|
|
170
|
+
const tag = `[${name}:misunderstanding.${m.id}]`;
|
|
171
|
+
lines.push(`- ${tag} WRONG: ${m.wrong}`);
|
|
172
|
+
lines.push(` CORRECT: ${m.correct}`);
|
|
173
|
+
if (m.failure_risk) lines.push(` RISK: ${m.failure_risk}`);
|
|
174
|
+
attributionMap[tag] = { domain: name, type: 'misunderstanding', id: m.id, index: misreadingIndex++ };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (patterns.terminology?.banned_terms?.length) {
|
|
179
|
+
lines.push(`### Banned terms`);
|
|
180
|
+
for (const t of patterns.terminology.banned_terms) {
|
|
181
|
+
const term = typeof t === 'string' ? t : t.term;
|
|
182
|
+
const tag = `[${name}:banned_term.${term}]`;
|
|
183
|
+
const replace = typeof t === 'object' ? t.replace_with : null;
|
|
184
|
+
lines.push(`- ${tag} "${term}"${replace ? ` → use: ${replace}` : ''}`);
|
|
185
|
+
attributionMap[tag] = { domain: name, type: 'banned_term', term, index: termIndex++ };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (patterns.self_check?.length) {
|
|
190
|
+
lines.push(`### Self-checks`);
|
|
191
|
+
for (const q of patterns.self_check) {
|
|
192
|
+
const text = typeof q === 'string' ? q : q.question;
|
|
193
|
+
const tag = `[${name}:self_check.${checkIndex}]`;
|
|
194
|
+
if (text) lines.push(`- ${tag} ${text}`);
|
|
195
|
+
attributionMap[tag] = { domain: name, type: 'self_check', index: checkIndex++ };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
lines.push(separator.trimEnd());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { context: lines.join('\n'), attributionMap };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Classify signals across a cluster of domains. Returns which domains
|
|
207
|
+
* matched and which were excluded. Unlike classifySignals (which just
|
|
208
|
+
* returns indices), this provides full diagnostic info.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} input
|
|
211
|
+
* @param {Array<{id:string, name:string, role:string, core:object}>} domainEntries
|
|
212
|
+
* @returns {{selected: Array, excluded: Array}}
|
|
213
|
+
*/
|
|
214
|
+
function classifySignalsAcrossDomains(input, domainEntries) {
|
|
215
|
+
if (!input || !domainEntries?.length) return { selected: [], excluded: [] };
|
|
216
|
+
|
|
217
|
+
const lower = input.toLowerCase();
|
|
218
|
+
const selected = [];
|
|
219
|
+
const excluded = [];
|
|
220
|
+
|
|
221
|
+
for (const entry of domainEntries) {
|
|
222
|
+
const signals = entry.core?.trigger_signals || [];
|
|
223
|
+
const negativeSignals = entry.core?.negative_signals || [];
|
|
224
|
+
const doesNotApply = [];
|
|
225
|
+
for (const a of entry.core?.axioms || []) {
|
|
226
|
+
if (Array.isArray(a.does_not_apply_when)) doesNotApply.push(...a.does_not_apply_when);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check negative signals
|
|
230
|
+
const blocked = negativeSignals.some((s) => lower.includes(s.toLowerCase())) ||
|
|
231
|
+
doesNotApply.some((s) => lower.includes(s.toLowerCase()));
|
|
232
|
+
|
|
233
|
+
if (blocked) {
|
|
234
|
+
excluded.push({ id: entry.id, name: entry.name, reason: 'blocked by does_not_apply_when', role: entry.role });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// No signals defined → required domain (always active)
|
|
239
|
+
if (!signals.length) {
|
|
240
|
+
selected.push({ id: entry.id, name: entry.name, role: entry.role, reason: 'required' });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check positive signals
|
|
245
|
+
const matched = signals.some((s) => lower.includes(s.toLowerCase()));
|
|
246
|
+
if (matched) {
|
|
247
|
+
selected.push({ id: entry.id, name: entry.name, role: entry.role, reason: 'signal_match' });
|
|
248
|
+
} else {
|
|
249
|
+
excluded.push({ id: entry.id, name: entry.name, reason: 'no signal match', role: entry.role });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { selected, excluded };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Load a cluster from its manifest.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} clusterManifestPath — path to kdna.cluster.json
|
|
260
|
+
* @param {function} domainLoader — fn(domainId) → {core, patterns} or null
|
|
261
|
+
* @returns {{manifest:object, domains:Array, errors:Array}}
|
|
262
|
+
*/
|
|
263
|
+
function loadCluster(clusterManifestPath, domainLoader) {
|
|
264
|
+
const fs = require('fs');
|
|
265
|
+
const manifest = JSON.parse(fs.readFileSync(clusterManifestPath, 'utf8'));
|
|
266
|
+
const domains = [];
|
|
267
|
+
const errors = [];
|
|
268
|
+
|
|
269
|
+
if (!manifest.domains || !Array.isArray(manifest.domains)) {
|
|
270
|
+
return { manifest, domains, errors: ['No domains defined in cluster manifest'] };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const entry of manifest.domains) {
|
|
274
|
+
try {
|
|
275
|
+
const loaded = domainLoader(entry.id);
|
|
276
|
+
if (loaded) {
|
|
277
|
+
domains.push({ id: entry.id, name: loaded.core?.meta?.domain || entry.id, role: entry.role || 'advisor', required: entry.required !== false, core: loaded.core, patterns: loaded.patterns });
|
|
278
|
+
} else {
|
|
279
|
+
errors.push(`Domain ${entry.id}: not found or failed to load`);
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
errors.push(`Domain ${entry.id}: ${e.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { manifest, domains, errors };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Detect conflicts between loaded domains in a cluster.
|
|
291
|
+
*
|
|
292
|
+
* @param {Array<{id:string, core:object, patterns:object}>} domains
|
|
293
|
+
* @returns {Array<{type:string, domains:string[], description:string}>}
|
|
294
|
+
*/
|
|
295
|
+
function detectDomainConflicts(domains) {
|
|
296
|
+
if (!domains || domains.length < 2) return [];
|
|
297
|
+
|
|
298
|
+
const conflicts = [];
|
|
299
|
+
const bannedTermMap = new Map();
|
|
300
|
+
|
|
301
|
+
// Check banned term conflicts — same term banned by one, preferred by another
|
|
302
|
+
for (const d of domains) {
|
|
303
|
+
const terms = d.patterns?.terminology?.banned_terms || [];
|
|
304
|
+
for (const t of terms) {
|
|
305
|
+
const term = typeof t === 'string' ? t : t.term;
|
|
306
|
+
if (bannedTermMap.has(term)) {
|
|
307
|
+
conflicts.push({
|
|
308
|
+
type: 'term_conflict',
|
|
309
|
+
domains: [bannedTermMap.get(term), d.id],
|
|
310
|
+
description: `Banned term "${term}" appears in multiple domains`,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
bannedTermMap.set(term, d.id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check stance conflicts — contradictory stances
|
|
318
|
+
for (let i = 0; i < domains.length; i++) {
|
|
319
|
+
for (let j = i + 1; j < domains.length; j++) {
|
|
320
|
+
const sA = (domains[i].core?.stances || []).map((s) => (typeof s === 'string' ? s : s.stance));
|
|
321
|
+
const sB = (domains[j].core?.stances || []).map((s) => (typeof s === 'string' ? s : s.stance));
|
|
322
|
+
for (const sa of sA) {
|
|
323
|
+
for (const sb of sB) {
|
|
324
|
+
// Simple negation check — can be extended
|
|
325
|
+
if (sa && sb && (sa.includes('not') && sb.toLowerCase().includes(sa.toLowerCase().replace('not ', ''))) ||
|
|
326
|
+
(sb.includes('not') && sa.toLowerCase().includes(sb.toLowerCase().replace('not ', '')))) {
|
|
327
|
+
conflicts.push({
|
|
328
|
+
type: 'stance_conflict',
|
|
329
|
+
domains: [domains[i].id, domains[j].id],
|
|
330
|
+
description: `Potential stance conflict: "${sa.slice(0, 60)}" vs "${sb.slice(0, 60)}"`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return conflicts;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Generate a judgment trace for a cluster operation.
|
|
343
|
+
*
|
|
344
|
+
* @param {object} params
|
|
345
|
+
* @param {string} params.input — the original user input
|
|
346
|
+
* @param {Array} params.loadedDomains — all domains in the cluster
|
|
347
|
+
* @param {Array} params.activeDomains — domains that were activated
|
|
348
|
+
* @param {Array} params.conflicts — detected conflicts
|
|
349
|
+
* @returns {object}
|
|
350
|
+
*/
|
|
351
|
+
function generateClusterTrace({ input, loadedDomains, activeDomains, conflicts }) {
|
|
352
|
+
return {
|
|
353
|
+
input: (input || '').slice(0, 200),
|
|
354
|
+
timestamp: new Date().toISOString(),
|
|
355
|
+
loaded_domains: (loadedDomains || []).map((d) => d.id || d.name || '?'),
|
|
356
|
+
active_domains: (activeDomains || []).map((d) => d.id || d.name || '?'),
|
|
357
|
+
active_count: (activeDomains || []).length,
|
|
358
|
+
domains_excluded: (loadedDomains || []).length - (activeDomains || []).length,
|
|
359
|
+
conflicts: (conflicts || []).map((c) => ({
|
|
360
|
+
type: c.type,
|
|
361
|
+
domains: c.domains,
|
|
362
|
+
description: c.description,
|
|
363
|
+
})),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
composeContext,
|
|
369
|
+
composeContextWithAttribution,
|
|
370
|
+
classifySignals,
|
|
371
|
+
classifySignalsAcrossDomains,
|
|
372
|
+
composeChecks,
|
|
373
|
+
loadAndCompose,
|
|
374
|
+
loadCluster,
|
|
375
|
+
detectDomainConflicts,
|
|
376
|
+
generateClusterTrace,
|
|
377
|
+
};
|
package/src/index.mjs
CHANGED
|
@@ -16,4 +16,4 @@ export { validateDomainSchema, validateCrossFile } from './validate-pure.js';
|
|
|
16
16
|
|
|
17
17
|
export { renderPreviewHTML, escHtml, renderCard } from './render.js';
|
|
18
18
|
|
|
19
|
-
export { composeContext, classifySignals, composeChecks, loadAndCompose } from './compose.js';
|
|
19
|
+
export { composeContext, composeContextWithAttribution, classifySignals, classifySignalsAcrossDomains, composeChecks, loadAndCompose, loadCluster, detectDomainConflicts, generateClusterTrace } from './compose.js';
|
package/src/lint-pure.js
CHANGED
|
@@ -6,6 +6,29 @@
|
|
|
6
6
|
* Output: { errors: string[], warnings: string[] }
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Map of old/informal field names → correct v0.4 spec field names.
|
|
11
|
+
* Used to give helpful error messages when users write from scratch without the template.
|
|
12
|
+
*/
|
|
13
|
+
const OLD_FIELD_HINTS = {
|
|
14
|
+
statement: 'one_sentence or full_statement',
|
|
15
|
+
description: 'one_sentence',
|
|
16
|
+
summary: 'one_sentence',
|
|
17
|
+
claim: 'wrong',
|
|
18
|
+
misreading: 'wrong',
|
|
19
|
+
reality: 'correct',
|
|
20
|
+
definition: 'essence or one_sentence (on ontology entries)',
|
|
21
|
+
brief: 'title or context',
|
|
22
|
+
bad_pattern: 'what_happened',
|
|
23
|
+
master_pattern: 'structural_pattern',
|
|
24
|
+
conclusion: 'one_sentence',
|
|
25
|
+
capability_layers: 'stages',
|
|
26
|
+
name: 'id (on ontology entries — use id instead of name)',
|
|
27
|
+
input: 'from',
|
|
28
|
+
output: 'to',
|
|
29
|
+
judgment: 'via',
|
|
30
|
+
};
|
|
31
|
+
|
|
9
32
|
/**
|
|
10
33
|
* Lint a KDNA domain from a map of parsed JSON objects.
|
|
11
34
|
*
|
|
@@ -23,6 +46,15 @@ function lintDomain(dataMap) {
|
|
|
23
46
|
function req(o, k, loc, hint) {
|
|
24
47
|
if (!has(o, k) || o[k] === '' || o[k] == null) {
|
|
25
48
|
let msg = `${loc}: missing required field "${k}"`;
|
|
49
|
+
// Check for common old field name and suggest the correct one
|
|
50
|
+
if (o && typeof o === 'object') {
|
|
51
|
+
for (const [oldName, newName] of Object.entries(OLD_FIELD_HINTS)) {
|
|
52
|
+
if (has(o, oldName)) {
|
|
53
|
+
msg += `\n → Found field "${oldName}" — this looks like an old/informal field name. Use "${newName}" instead.`;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
26
58
|
if (hint) msg += `\n → ${hint}`;
|
|
27
59
|
errors.push(msg);
|
|
28
60
|
}
|
package/src/loader.js
CHANGED
|
@@ -118,6 +118,55 @@ function classifyInput(text) {
|
|
|
118
118
|
return [...new Set(optional)];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Known field name mapping for detecting old/incorrect field names.
|
|
123
|
+
* When a common old name is found, log a warning and suggest the correct name.
|
|
124
|
+
*/
|
|
125
|
+
const FIELD_ALIASES = {
|
|
126
|
+
statement: 'one_sentence or full_statement',
|
|
127
|
+
description: 'one_sentence',
|
|
128
|
+
summary: 'one_sentence',
|
|
129
|
+
claim: 'wrong',
|
|
130
|
+
misreading: 'wrong',
|
|
131
|
+
reality: 'correct',
|
|
132
|
+
definition: 'essence or one_sentence (on ontology)',
|
|
133
|
+
brief: 'title or context',
|
|
134
|
+
bad_pattern: 'what_happened',
|
|
135
|
+
master_pattern: 'structural_pattern',
|
|
136
|
+
conclusion: 'one_sentence',
|
|
137
|
+
capability_layers: 'stages',
|
|
138
|
+
name: 'id (on ontology entries)',
|
|
139
|
+
input: 'from',
|
|
140
|
+
output: 'to',
|
|
141
|
+
judgment: 'via',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Deep-scan an object tree for known old field names and return warnings.
|
|
146
|
+
* @param {object} obj
|
|
147
|
+
* @param {string} path
|
|
148
|
+
* @param {string[]} [warnings]
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
function detectOldFieldNames(obj, path = '', warnings = []) {
|
|
152
|
+
if (!obj || typeof obj !== 'object') return warnings;
|
|
153
|
+
if (Array.isArray(obj)) {
|
|
154
|
+
obj.forEach((item, i) => detectOldFieldNames(item, `${path}[${i}]`, warnings));
|
|
155
|
+
return warnings;
|
|
156
|
+
}
|
|
157
|
+
for (const key of Object.keys(obj)) {
|
|
158
|
+
if (FIELD_ALIASES[key]) {
|
|
159
|
+
warnings.push(
|
|
160
|
+
`[KDNA LOADER] ${path}.${key}: field "${key}" is not in spec v0.4. Use "${FIELD_ALIASES[key]}" instead. See docs/authoring-guide.md §0.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
164
|
+
detectOldFieldNames(obj[key], `${path}.${key}`, warnings);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return warnings;
|
|
168
|
+
}
|
|
169
|
+
|
|
121
170
|
/**
|
|
122
171
|
* Format a loaded KDNA domain into a context string suitable for
|
|
123
172
|
* inclusion in an agent's system prompt.
|
|
@@ -132,6 +181,17 @@ function formatContext(domain) {
|
|
|
132
181
|
const core = domain.core;
|
|
133
182
|
const pat = domain.patterns;
|
|
134
183
|
|
|
184
|
+
// Scan for old field names and warn
|
|
185
|
+
const warnings = detectOldFieldNames(domain, domain.core?.meta?.domain || 'domain');
|
|
186
|
+
if (warnings.length) {
|
|
187
|
+
parts.push('<!-- KDNA FIELD NAME WARNINGS:');
|
|
188
|
+
for (const w of warnings) parts.push(` ${w}`);
|
|
189
|
+
parts.push(' These fields will be SILENTLY IGNORED by the loader.');
|
|
190
|
+
parts.push(' See: docs/authoring-guide.md §0 (Field Name Reference)');
|
|
191
|
+
parts.push('-->');
|
|
192
|
+
parts.push('');
|
|
193
|
+
}
|
|
194
|
+
|
|
135
195
|
parts.push('## Domain Cognition (KDNA)');
|
|
136
196
|
parts.push(`Domain: ${core.meta.domain}`);
|
|
137
197
|
parts.push('');
|
package/src/validate-pure.js
CHANGED
|
@@ -118,7 +118,7 @@ function validateCrossFile(dataMap) {
|
|
|
118
118
|
if (version === null) {
|
|
119
119
|
version = data.meta.version;
|
|
120
120
|
} else if (data.meta.version !== version) {
|
|
121
|
-
|
|
121
|
+
errors.push(
|
|
122
122
|
`${file}: version "${data.meta.version}" differs from "${version}" in other files`,
|
|
123
123
|
);
|
|
124
124
|
}
|