@aikdna/kdna-core 0.2.3 → 0.4.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/README.md +30 -0
- package/package.json +6 -4
- 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 +191 -7
- package/schema/KDNA_Core.strict.schema.json +290 -0
- package/schema/KDNA_Evolution.schema.json +56 -3
- package/schema/KDNA_Patterns.schema.json +273 -6
- package/schema/KDNA_Patterns.strict.schema.json +342 -0
- package/schema/KDNA_Reasoning.schema.json +38 -18
- package/schema/KDNA_Scenarios.schema.json +37 -6
- package/schema/KDNA_Scenarios.strict.schema.json +101 -0
- package/schema/eval.schema.json +58 -0
- package/src/compose.js +255 -1
- package/src/index.mjs +1 -1
- package/src/lint-pure.js +191 -4
- package/src/loader.js +90 -20
- 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 && (/\bnot\b/.test(sa) && sb.toLowerCase().includes(sa.toLowerCase().replace('not ', ''))) ||
|
|
326
|
+
(/\bnot\b/.test(sb) && 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,38 @@
|
|
|
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
|
+
|
|
32
|
+
const KDNA_DOMAIN_FILES = new Set([
|
|
33
|
+
'KDNA_Core.json',
|
|
34
|
+
'KDNA_Patterns.json',
|
|
35
|
+
'KDNA_Scenarios.json',
|
|
36
|
+
'KDNA_Cases.json',
|
|
37
|
+
'KDNA_Reasoning.json',
|
|
38
|
+
'KDNA_Evolution.json',
|
|
39
|
+
]);
|
|
40
|
+
|
|
9
41
|
/**
|
|
10
42
|
* Lint a KDNA domain from a map of parsed JSON objects.
|
|
11
43
|
*
|
|
@@ -23,6 +55,15 @@ function lintDomain(dataMap) {
|
|
|
23
55
|
function req(o, k, loc, hint) {
|
|
24
56
|
if (!has(o, k) || o[k] === '' || o[k] == null) {
|
|
25
57
|
let msg = `${loc}: missing required field "${k}"`;
|
|
58
|
+
// Check for common old field name and suggest the correct one
|
|
59
|
+
if (o && typeof o === 'object') {
|
|
60
|
+
for (const [oldName, newName] of Object.entries(OLD_FIELD_HINTS)) {
|
|
61
|
+
if (has(o, oldName)) {
|
|
62
|
+
msg += `\n → Found field "${oldName}" — this looks like an old/informal field name. Use "${newName}" instead.`;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
26
67
|
if (hint) msg += `\n → ${hint}`;
|
|
27
68
|
errors.push(msg);
|
|
28
69
|
}
|
|
@@ -72,9 +113,7 @@ function lintDomain(dataMap) {
|
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
// Check file count
|
|
75
|
-
const kdnaFiles = Object.keys(dataMap).filter(
|
|
76
|
-
(f) => f.endsWith('.json') && f !== 'kdna.json',
|
|
77
|
-
);
|
|
116
|
+
const kdnaFiles = Object.keys(dataMap).filter((f) => KDNA_DOMAIN_FILES.has(f));
|
|
78
117
|
if (kdnaFiles.length > 6) errors.push(`Domain has ${kdnaFiles.length} JSON files; KDNA allows at most 6.`);
|
|
79
118
|
|
|
80
119
|
// Validate meta on all files
|
|
@@ -213,4 +252,152 @@ function lintDomain(dataMap) {
|
|
|
213
252
|
return { errors, warnings };
|
|
214
253
|
}
|
|
215
254
|
|
|
216
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Canonical enum tables for manifest validation.
|
|
257
|
+
* Single source of truth — keep in sync with schema/kdna-manifest-v1rc.json and specs/enum-tables.md.
|
|
258
|
+
*/
|
|
259
|
+
const VALID_STATUS = new Set(['draft', 'experimental', 'stable', 'deprecated', 'staging']);
|
|
260
|
+
const VALID_BADGE = new Set(['untested', 'tested', 'validated', 'expert_reviewed', 'production_ready']);
|
|
261
|
+
const VALID_ACCESS = new Set(['open', 'licensed', 'runtime']);
|
|
262
|
+
const VALID_RISK = new Set(['R0', 'R1', 'R2', 'R3']);
|
|
263
|
+
const VALID_I18N = new Set(['L0', 'L1', 'L2', 'L3']);
|
|
264
|
+
|
|
265
|
+
const MANIFEST_REQUIRED = [
|
|
266
|
+
'kdna_spec', 'name', 'version', 'judgment_version',
|
|
267
|
+
'description', 'author', 'license', 'status',
|
|
268
|
+
'quality_badge', 'access', 'language',
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Validate a kdna.json manifest against the canonical v1.0-rc schema.
|
|
273
|
+
*
|
|
274
|
+
* @param {Object} manifest — parsed kdna.json
|
|
275
|
+
* @returns {{ errors: string[], warnings: string[] }}
|
|
276
|
+
*/
|
|
277
|
+
function validateManifest(manifest) {
|
|
278
|
+
const errors = [];
|
|
279
|
+
const warnings = [];
|
|
280
|
+
|
|
281
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
282
|
+
errors.push('kdna.json: missing or empty manifest');
|
|
283
|
+
return { errors, warnings };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 1. Check spec_version is NOT in domain manifest (use kdna_spec only)
|
|
287
|
+
if ('spec_version' in manifest) {
|
|
288
|
+
errors.push(
|
|
289
|
+
'kdna.json: spec_version is deprecated in domain manifests. Use kdna_spec. ' +
|
|
290
|
+
'(spec_version is reserved for .kdna container manifests only.)',
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 2. Check required fields
|
|
295
|
+
for (const field of MANIFEST_REQUIRED) {
|
|
296
|
+
if (!(field in manifest) || manifest[field] === undefined || manifest[field] === '') {
|
|
297
|
+
errors.push(`kdna.json: missing required field "${field}"`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 3. Validate name format
|
|
302
|
+
if (manifest.name && !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(manifest.name)) {
|
|
303
|
+
errors.push(`kdna.json.name: invalid format "${manifest.name}". Expected @scope/name.`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 4. Validate enum fields
|
|
307
|
+
if (manifest.status && !VALID_STATUS.has(manifest.status)) {
|
|
308
|
+
errors.push(
|
|
309
|
+
`kdna.json.status: invalid value "${manifest.status}". ` +
|
|
310
|
+
`Valid: ${[...VALID_STATUS].join(', ')}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (manifest.quality_badge && !VALID_BADGE.has(manifest.quality_badge)) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`kdna.json.quality_badge: invalid value "${manifest.quality_badge}". ` +
|
|
316
|
+
`Valid: ${[...VALID_BADGE].join(', ')}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (manifest.access && !VALID_ACCESS.has(manifest.access)) {
|
|
320
|
+
errors.push(
|
|
321
|
+
`kdna.json.access: invalid value "${manifest.access}". ` +
|
|
322
|
+
`Valid: ${[...VALID_ACCESS].join(', ')}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (manifest.risk_level && !VALID_RISK.has(manifest.risk_level)) {
|
|
326
|
+
errors.push(
|
|
327
|
+
`kdna.json.risk_level: invalid value "${manifest.risk_level}". ` +
|
|
328
|
+
`Valid: ${[...VALID_RISK].join(', ')}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (manifest.i18n_level && !VALID_I18N.has(manifest.i18n_level)) {
|
|
332
|
+
warnings.push(
|
|
333
|
+
`kdna.json.i18n_level: non-standard value "${manifest.i18n_level}". ` +
|
|
334
|
+
`Valid: ${[...VALID_I18N].join(', ')}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 5. Deprecated status must have replaced_by
|
|
339
|
+
if (manifest.status === 'deprecated' && !manifest.replaced_by) {
|
|
340
|
+
errors.push('kdna.json: status is "deprecated" but replaced_by is missing');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 6. Tested+ badge must have signature
|
|
344
|
+
const needsSig = ['tested', 'validated', 'expert_reviewed', 'production_ready'];
|
|
345
|
+
if (needsSig.includes(manifest.quality_badge) && !manifest.signature) {
|
|
346
|
+
warnings.push(
|
|
347
|
+
`kdna.json: quality_badge "${manifest.quality_badge}" should have a signature`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 7. Validate author
|
|
352
|
+
if (manifest.author) {
|
|
353
|
+
if (!manifest.author.name) errors.push('kdna.json.author: missing "name"');
|
|
354
|
+
if (!manifest.author.id) errors.push('kdna.json.author: missing "id"');
|
|
355
|
+
if (manifest.author.pubkey && !/^ed25519:[0-9a-f]{64}$/.test(manifest.author.pubkey)) {
|
|
356
|
+
warnings.push('kdna.json.author.pubkey: non-standard format. Expected ed25519:<64 hex chars>.');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 8. Validate license
|
|
361
|
+
if (manifest.license && !manifest.license.type) {
|
|
362
|
+
errors.push('kdna.json.license: missing "type"');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 9. Validate kdna_spec value
|
|
366
|
+
if (manifest.kdna_spec && manifest.kdna_spec !== '1.0-rc') {
|
|
367
|
+
warnings.push(
|
|
368
|
+
`kdna.json.kdna_spec: non-standard value "${manifest.kdna_spec}". Expected "1.0-rc".`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 10. Validate version format
|
|
373
|
+
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
|
374
|
+
warnings.push(
|
|
375
|
+
`kdna.json.version: non-semver format "${manifest.version}". Expected MAJOR.MINOR.PATCH.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 11. Check for removed fields
|
|
380
|
+
const removedFields = ['release_status', 'domain_field', 'judgment_patterns', 'files', 'registry'];
|
|
381
|
+
for (const field of removedFields) {
|
|
382
|
+
if (field in manifest) {
|
|
383
|
+
warnings.push(
|
|
384
|
+
`kdna.json: field "${field}" is not in the canonical domain manifest and should be removed`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 12. Check removed license sub-fields
|
|
390
|
+
if (manifest.license && typeof manifest.license === 'object') {
|
|
391
|
+
for (const field of ['commercial', 'allow_agent_use', 'allow_redistribution', 'allow_training']) {
|
|
392
|
+
if (field in manifest.license) {
|
|
393
|
+
warnings.push(
|
|
394
|
+
`kdna.json.license.${field}: license-type-specific field, not universal. Consider removing.`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { errors, warnings };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = { lintDomain, validateManifest };
|
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.
|
|
@@ -125,6 +174,16 @@ function classifyInput(text) {
|
|
|
125
174
|
* @param {object} domain — result from loadDomainFromData() or loadDomainFromFiles()
|
|
126
175
|
* @returns {string}
|
|
127
176
|
*/
|
|
177
|
+
function sanitize(str) {
|
|
178
|
+
if (typeof str !== 'string') return str;
|
|
179
|
+
return str
|
|
180
|
+
.replace(/^#{1,6}\s/gm, '\\# ') // Escape leading # to prevent fake headers
|
|
181
|
+
.replace(/```/g, '\\`\\`\\`') // Escape code blocks
|
|
182
|
+
.replace(/<\|/g, '<|') // Escape special tokens
|
|
183
|
+
.replace(/\b(ignore|forget|disregard)\s+(all\s+)?(previous|prior|above)\s+(instructions?|directives?|rules?|constraints?)\b/gi,
|
|
184
|
+
'[filtered: $&]'); // Filter prompt injection patterns
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
function formatContext(domain) {
|
|
129
188
|
if (!domain || !domain.core || !domain.patterns) return '';
|
|
130
189
|
|
|
@@ -132,14 +191,25 @@ function formatContext(domain) {
|
|
|
132
191
|
const core = domain.core;
|
|
133
192
|
const pat = domain.patterns;
|
|
134
193
|
|
|
194
|
+
// Scan for old field names and warn
|
|
195
|
+
const warnings = detectOldFieldNames(domain, domain.core?.meta?.domain || 'domain');
|
|
196
|
+
if (warnings.length) {
|
|
197
|
+
parts.push('<!-- KDNA FIELD NAME WARNINGS:');
|
|
198
|
+
for (const w of warnings) parts.push(` ${w}`);
|
|
199
|
+
parts.push(' These fields will be SILENTLY IGNORED by the loader.');
|
|
200
|
+
parts.push(' See: docs/authoring-guide.md §0 (Field Name Reference)');
|
|
201
|
+
parts.push('-->');
|
|
202
|
+
parts.push('');
|
|
203
|
+
}
|
|
204
|
+
|
|
135
205
|
parts.push('## Domain Cognition (KDNA)');
|
|
136
|
-
parts.push(`Domain: ${core.meta.domain}`);
|
|
206
|
+
parts.push(`Domain: ${sanitize(core.meta.domain)}`);
|
|
137
207
|
parts.push('');
|
|
138
208
|
|
|
139
209
|
if (core.stances && core.stances.length) {
|
|
140
210
|
parts.push('### Stances');
|
|
141
211
|
for (const s of core.stances) {
|
|
142
|
-
parts.push(`- ${s}`);
|
|
212
|
+
parts.push(`- ${sanitize(s)}`);
|
|
143
213
|
}
|
|
144
214
|
parts.push('');
|
|
145
215
|
}
|
|
@@ -147,8 +217,8 @@ function formatContext(domain) {
|
|
|
147
217
|
if (core.axioms && core.axioms.length) {
|
|
148
218
|
parts.push('### Axioms');
|
|
149
219
|
for (const a of core.axioms) {
|
|
150
|
-
parts.push(`- **${a.one_sentence}** ${a.full_statement}`);
|
|
151
|
-
parts.push(` *Why:* ${a.why}`);
|
|
220
|
+
parts.push(`- **${sanitize(a.one_sentence)}** ${sanitize(a.full_statement)}`);
|
|
221
|
+
parts.push(` *Why:* ${sanitize(a.why)}`);
|
|
152
222
|
}
|
|
153
223
|
parts.push('');
|
|
154
224
|
}
|
|
@@ -156,8 +226,8 @@ function formatContext(domain) {
|
|
|
156
226
|
if (core.ontology && core.ontology.length) {
|
|
157
227
|
parts.push('### Key Concepts');
|
|
158
228
|
for (const c of core.ontology) {
|
|
159
|
-
parts.push(`- **${c.id.replace(/_/g, ' ')}** — ${c.one_sentence}`);
|
|
160
|
-
parts.push(` Boundary: ${c.boundary}`);
|
|
229
|
+
parts.push(`- **${sanitize(c.id.replace(/_/g, ' '))}** — ${sanitize(c.one_sentence)}`);
|
|
230
|
+
parts.push(` Boundary: ${sanitize(c.boundary)}`);
|
|
161
231
|
}
|
|
162
232
|
parts.push('');
|
|
163
233
|
}
|
|
@@ -165,7 +235,7 @@ function formatContext(domain) {
|
|
|
165
235
|
if (core.frameworks && core.frameworks.length) {
|
|
166
236
|
parts.push('### Frameworks');
|
|
167
237
|
for (const fw of core.frameworks) {
|
|
168
|
-
parts.push(`- **${fw.name}**: ${fw.when_to_use}`);
|
|
238
|
+
parts.push(`- **${sanitize(fw.name)}**: ${sanitize(fw.when_to_use)}`);
|
|
169
239
|
}
|
|
170
240
|
parts.push('');
|
|
171
241
|
}
|
|
@@ -173,7 +243,7 @@ function formatContext(domain) {
|
|
|
173
243
|
if (pat.terminology && pat.terminology.banned_terms && pat.terminology.banned_terms.length) {
|
|
174
244
|
parts.push('### Avoid These Terms');
|
|
175
245
|
for (const b of pat.terminology.banned_terms) {
|
|
176
|
-
parts.push(`- Avoid "${b.term}". ${b.why} Use "${b.replace_with}" instead.`);
|
|
246
|
+
parts.push(`- Avoid "${sanitize(b.term)}". ${sanitize(b.why)} Use "${sanitize(b.replace_with)}" instead.`);
|
|
177
247
|
}
|
|
178
248
|
parts.push('');
|
|
179
249
|
}
|
|
@@ -181,8 +251,8 @@ function formatContext(domain) {
|
|
|
181
251
|
if (pat.misunderstandings && pat.misunderstandings.length) {
|
|
182
252
|
parts.push('### Watch For These Misunderstandings');
|
|
183
253
|
for (const m of pat.misunderstandings) {
|
|
184
|
-
parts.push(`- **Wrong:** ${m.wrong}`);
|
|
185
|
-
parts.push(` **Correct:** ${m.correct}`);
|
|
254
|
+
parts.push(`- **Wrong:** ${sanitize(m.wrong)}`);
|
|
255
|
+
parts.push(` **Correct:** ${sanitize(m.correct)}`);
|
|
186
256
|
}
|
|
187
257
|
parts.push('');
|
|
188
258
|
}
|
|
@@ -190,7 +260,7 @@ function formatContext(domain) {
|
|
|
190
260
|
if (pat.self_check && pat.self_check.length) {
|
|
191
261
|
parts.push('### Before Responding, Check');
|
|
192
262
|
for (const s of pat.self_check) {
|
|
193
|
-
parts.push(`- [ ] ${s}`);
|
|
263
|
+
parts.push(`- [ ] ${sanitize(s)}`);
|
|
194
264
|
}
|
|
195
265
|
parts.push('');
|
|
196
266
|
}
|
|
@@ -198,7 +268,7 @@ function formatContext(domain) {
|
|
|
198
268
|
if (domain.scenarios && domain.scenarios.scenes) {
|
|
199
269
|
parts.push('### Relevant Scenarios');
|
|
200
270
|
for (const scene of domain.scenarios.scenes) {
|
|
201
|
-
parts.push(`- **${scene.name}**: ${scene.trigger_signal}`);
|
|
271
|
+
parts.push(`- **${sanitize(scene.name)}**: ${sanitize(scene.trigger_signal)}`);
|
|
202
272
|
}
|
|
203
273
|
parts.push('');
|
|
204
274
|
}
|
|
@@ -206,7 +276,7 @@ function formatContext(domain) {
|
|
|
206
276
|
if (domain.reasoning && domain.reasoning.reasoning_chains) {
|
|
207
277
|
parts.push('### Reasoning Chains');
|
|
208
278
|
for (const r of domain.reasoning.reasoning_chains) {
|
|
209
|
-
parts.push(`- **${r.one_sentence}** → ${r.so_what}`);
|
|
279
|
+
parts.push(`- **${sanitize(r.one_sentence)}** → ${sanitize(r.so_what)}`);
|
|
210
280
|
}
|
|
211
281
|
parts.push('');
|
|
212
282
|
}
|
|
@@ -214,11 +284,11 @@ function formatContext(domain) {
|
|
|
214
284
|
if (domain.cases && domain.cases.cases && domain.cases.cases.length) {
|
|
215
285
|
parts.push('### Cases');
|
|
216
286
|
for (const c of domain.cases.cases) {
|
|
217
|
-
parts.push(`- **${c.title}**`);
|
|
218
|
-
parts.push(` Context: ${c.context}`);
|
|
219
|
-
parts.push(` What happened: ${c.what_happened}`);
|
|
220
|
-
parts.push(` Learned: ${c.what_was_learned}`);
|
|
221
|
-
parts.push(` Pattern: ${c.structural_pattern}`);
|
|
287
|
+
parts.push(`- **${sanitize(c.title)}**`);
|
|
288
|
+
parts.push(` Context: ${sanitize(c.context)}`);
|
|
289
|
+
parts.push(` What happened: ${sanitize(c.what_happened)}`);
|
|
290
|
+
parts.push(` Learned: ${sanitize(c.what_was_learned)}`);
|
|
291
|
+
parts.push(` Pattern: ${sanitize(c.structural_pattern)}`);
|
|
222
292
|
}
|
|
223
293
|
parts.push('');
|
|
224
294
|
}
|
|
@@ -228,7 +298,7 @@ function formatContext(domain) {
|
|
|
228
298
|
if (evo.stages && evo.stages.length) {
|
|
229
299
|
parts.push('### Growth Stages');
|
|
230
300
|
for (const stage of evo.stages) {
|
|
231
|
-
parts.push(`- **${stage.name}**: ${stage.description}`);
|
|
301
|
+
parts.push(`- **${sanitize(stage.name)}**: ${sanitize(stage.description)}`);
|
|
232
302
|
}
|
|
233
303
|
parts.push('');
|
|
234
304
|
}
|
|
@@ -236,7 +306,7 @@ function formatContext(domain) {
|
|
|
236
306
|
parts.push('### Capability Layers');
|
|
237
307
|
for (const layer of evo.evolution_layers) {
|
|
238
308
|
parts.push(
|
|
239
|
-
`- **${layer.name}**: ${layer.capability} (${layer.from_stage} → ${layer.to_stage})`,
|
|
309
|
+
`- **${sanitize(layer.name)}**: ${sanitize(layer.capability)} (${sanitize(layer.from_stage)} → ${sanitize(layer.to_stage)})`,
|
|
240
310
|
);
|
|
241
311
|
}
|
|
242
312
|
parts.push('');
|