@aikdna/kdna-studio-core 1.5.0 → 1.5.1

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 CHANGED
@@ -205,7 +205,7 @@ KDNA Studio Core is open source. Official KDNA Studio App, hosted collaboration,
205
205
 
206
206
  ## Related
207
207
 
208
- - [KDNA Protocol](https://github.com/aikdna/kdna) — Specification
208
+ - [KDNA Core](https://github.com/aikdna/kdna) — Official format
209
209
  - [kdna-cli](https://github.com/aikdna/kdna-cli) — CLI tools
210
210
  - [kdna-core-swift](https://github.com/aikdna/kdna-core-swift) — Swift runtime for macOS/iOS
211
211
  - [aikdna.com](https://aikdna.com) — Website
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-studio-core",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Studio-compatible authoring kernel for Human Lock, compile, and export of trusted .kdna assets.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -24,7 +24,7 @@
24
24
  "validate"
25
25
  ],
26
26
  "dependencies": {
27
- "@aikdna/kdna-core": "^0.8.0",
27
+ "@aikdna/kdna-core": "^0.9.1",
28
28
  "cbor-x": "^1.6.4"
29
29
  },
30
30
  "engines": {
@@ -12,7 +12,7 @@
12
12
  const { cardJudgmentFingerprint } = require('../judgment-fields');
13
13
 
14
14
  const VALID_STATES = ['draft', 'revised', 'locked', 'tested', 'published', 'deprecated'];
15
- const CARD_TYPES = ['axiom', 'ontology', 'misunderstanding', 'boundary', 'self_check', 'risk', 'aesthetic', 'scenario', 'case'];
15
+ const CARD_TYPES = ['axiom', 'ontology', 'misunderstanding', 'boundary', 'self_check', 'risk', 'aesthetic', 'scenario', 'case', 'stance', 'framework', 'term', 'banned_term', 'reasoning', 'evolution_stage'];
16
16
 
17
17
  const TRANSITIONS = {
18
18
  draft: ['revised', 'deprecated'],
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Compile locked cards into KDNA domain JSON files — SPEC-compatible output.
3
3
  *
4
- * KDNA Container v2:
4
+ * KDNA Container:
5
5
  * - Judgment content is encoded as CBOR payload (payload.kdnab)
6
6
  * - Individual KDNA_Core.json etc. are NOT exposed as ZIP entries
7
7
  * - kdna.json manifest contains metadata only, no judgment content
@@ -415,8 +415,28 @@ function buildReports(project, files, identity, provenance, stats) {
415
415
  };
416
416
  }
417
417
 
418
- function compileDomain(project) {
418
+ function compileDomain(project, options = {}) {
419
419
  const cards = project.cards || [];
420
+
421
+ // ── Empty-domain gate (PR-2) ────────────────────────────────────
422
+ // A KDNA domain with no locked judgment content of any kind is not a
423
+ // domain — it is a content-shaped empty file. Refuse to compile so the
424
+ // downstream Registry / Lab / Studio export never advertises an empty
425
+ // judgment asset as "successfully compiled".
426
+ const lockedCards = cards.filter(c => c.locked);
427
+ const hasJudgmentContent = lockedCards.some(c =>
428
+ ['axiom', 'misunderstanding', 'scenario', 'case', 'self_check', 'boundary', 'risk', 'ontology', 'aesthetic'].includes(c.type)
429
+ );
430
+ if (!hasJudgmentContent) {
431
+ const err = new Error(
432
+ 'refusing to compile empty KDNA domain: no locked judgment content ' +
433
+ `(axiom / misunderstanding / scenario / case / self_check / boundary / risk / ontology / aesthetic). ` +
434
+ `Found ${lockedCards.length} locked card(s) and ${cards.length} total card(s).`
435
+ );
436
+ err.code = 'EMPTY_DOMAIN';
437
+ throw err;
438
+ }
439
+
420
440
  const core = compileCore(cards, project);
421
441
  const patterns = compilePatterns(cards, project);
422
442
  const scenarios = compileScenarios(cards, project);
@@ -424,9 +444,37 @@ function compileDomain(project) {
424
444
  const reasoning = compileReasoning(cards, project);
425
445
  const evolution = compileEvolution(cards, project);
426
446
 
447
+ // ── RFC-0013 §3.1/§3.2 Compile Gates (PR-3) ───────────────────
448
+ // Run the Source Authority Graph gate and the Truth Charter gate
449
+ // BEFORE packaging. PR-2: strictAuthority now defaults to true so
450
+ // missing/unstable SAG/TC are surfaced as gate errors that block
451
+ // compilation. Pass options.strictAuthority = false to opt out
452
+ // (legacy workspaces only).
453
+ const strictAuthority = options.strictAuthority !== false;
454
+ const { runSagGate } = require('./source-authority-gate');
455
+ const { runTcGate } = require('./truth-charter-gate');
456
+ const sag = runSagGate(options.sourceAuthority, { strict: strictAuthority });
457
+ const tc = runTcGate(options.truthCharter, {
458
+ strict: strictAuthority,
459
+ sourceAuthority: options.sourceAuthority || null,
460
+ patterns: patterns || null,
461
+ });
462
+ const gates = { sag, tc, strict_authority: strictAuthority };
463
+ // Gate policy: strictAuthority=true + any gate.status === 'fail' => throw.
464
+ if (strictAuthority && (sag.status === 'fail' || tc.status === 'fail')) {
465
+ const allErrors = [...sag.errors, ...tc.errors];
466
+ const err = new Error(
467
+ `Strict-authority compile failed. ${allErrors.length} gate error(s):\n` +
468
+ allErrors.map((e) => ` - ${e}`).join('\n'),
469
+ );
470
+ err.code = 'GATE_FAIL';
471
+ err.gates = gates;
472
+ throw err;
473
+ }
474
+
427
475
  const files = {};
428
476
 
429
- // ── KDNA Container v2: encode judgment as single CBOR payload ──
477
+ // Encode judgment as CBOR payload
430
478
  const payload = {
431
479
  kind: 'kdna.payload',
432
480
  payload_version: '2.0',
@@ -474,6 +522,7 @@ function compileDomain(project) {
474
522
  files,
475
523
  stats,
476
524
  identity,
525
+ gates,
477
526
  };
478
527
  }
479
528
 
@@ -556,4 +605,4 @@ function generateReadme(project, options = {}) {
556
605
  return lines.join('\n');
557
606
  }
558
607
 
559
- module.exports = { compileDomain, compileCore, compilePatterns, compileScenarios, compileCases, compileReasoning, compileEvolution, compileManifest, generateReadme, buildAssetIdentity, computeContentDigest };
608
+ module.exports = { compileDomain, compileCore, compilePatterns, compileScenarios, compileCases, compileReasoning, compileEvolution, compileManifest, generateReadme, buildAssetIdentity, computeContentDigest, runSagGate: require('./source-authority-gate').runSagGate, runTcGate: require('./truth-charter-gate').runTcGate };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Source Authority Graph (SAG) compile gate for kdna-studio-core.
3
+ *
4
+ * Implements the SAG gate from RFC-0013 §3.1 / §9 #3.
5
+ *
6
+ * Behavior:
7
+ * - Default mode: all rules are reported as WARNINGS (soft).
8
+ * - Strict-authority mode: hard-rule violations become ERRORS.
9
+ * - If no sourceAuthority provided, gate skips silently
10
+ * (backwards-compatible: legacy workspaces without SAG pass through).
11
+ *
12
+ * Rules checked:
13
+ * R1. precedence_order entries must all reference an existing source id.
14
+ * R2. At least one source must have authority: "current_highest"
15
+ * (when SAG is present; soft warning in default mode).
16
+ * R3. authority/status consistency:
17
+ * - authority: "deprecated" requires status: "deprecated"
18
+ * - authority: "current_highest" requires status: "active"
19
+ * - deprecated sources must not appear in precedence_order.
20
+ * R4. In precedence_order (highest precedence first), the first
21
+ * current_highest must not be preceded by any lower-authority
22
+ * source.
23
+ * R5. sources_contain_pii=true without author_consent_on_file is
24
+ * a soft warning (we are not the consent authority).
25
+ *
26
+ * Output contract:
27
+ * {
28
+ * gate: 'source_authority',
29
+ * status: 'skipped' | 'pass' | 'warn' | 'fail',
30
+ * errors: [string, ...], // strict-only; empty in default mode
31
+ * warnings: [string, ...], // always populated for any rule
32
+ * source_authority: object | null,
33
+ * strict_authority: boolean
34
+ * }
35
+ */
36
+
37
+ const AUTHORITY_RANK = {
38
+ current_highest: 4,
39
+ thought_mine: 3,
40
+ historical_baseline: 2,
41
+ exemplar_case: 1,
42
+ deprecated: 0,
43
+ };
44
+
45
+ function isPlainObject(v) {
46
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
47
+ }
48
+
49
+ function runSagGate(sourceAuthority, opts = {}) {
50
+ const strict = !!opts.strict;
51
+ const result = {
52
+ gate: 'source_authority',
53
+ status: 'skipped',
54
+ errors: [],
55
+ warnings: [],
56
+ source_authority: sourceAuthority || null,
57
+ strict_authority: strict,
58
+ };
59
+
60
+ if (!isPlainObject(sourceAuthority)) {
61
+ result.status = 'skipped';
62
+ result.warnings.push('No source_authority.json provided; SAG gate skipped.');
63
+ return result;
64
+ }
65
+
66
+ // R1. precedence_order references must be valid source ids.
67
+ const sources = Array.isArray(sourceAuthority.sources) ? sourceAuthority.sources : [];
68
+ const sourceIds = new Set();
69
+ for (const s of sources) {
70
+ if (isPlainObject(s) && typeof s.id === 'string') {
71
+ sourceIds.add(s.id);
72
+ }
73
+ }
74
+ const order = Array.isArray(sourceAuthority.precedence_order) ? sourceAuthority.precedence_order : [];
75
+ const missing = order.filter((id) => !sourceIds.has(id));
76
+ if (missing.length > 0) {
77
+ const msg = `source_authority.json: precedence_order references unknown source id(s): ${missing.join(', ')}.`;
78
+ if (strict) result.errors.push(msg);
79
+ else result.warnings.push(msg);
80
+ }
81
+
82
+ // R2. At least one current_highest source.
83
+ const highest = sources.filter(
84
+ (s) => isPlainObject(s) && s.authority === 'current_highest',
85
+ );
86
+ if (highest.length === 0) {
87
+ const msg = 'source_authority.json: no source has authority "current_highest"; at least one current_highest source is required to establish current authority.';
88
+ if (strict) result.errors.push(msg);
89
+ else result.warnings.push(msg);
90
+ }
91
+
92
+ // R3. authority/status consistency.
93
+ for (const s of sources) {
94
+ if (!isPlainObject(s)) continue;
95
+ if (s.authority === 'deprecated' && s.status !== 'deprecated') {
96
+ const msg = `source_authority.json: source "${s.id}" has authority "deprecated" but status is "${s.status}"; status MUST be "deprecated" when authority is "deprecated".`;
97
+ if (strict) result.errors.push(msg);
98
+ else result.warnings.push(msg);
99
+ }
100
+ if (s.authority === 'current_highest' && s.status !== 'active') {
101
+ const msg = `source_authority.json: source "${s.id}" has authority "current_highest" but status is "${s.status}"; status MUST be "active" for current_highest sources.`;
102
+ if (strict) result.errors.push(msg);
103
+ else result.warnings.push(msg);
104
+ }
105
+ if (s.authority === 'deprecated' && order.includes(s.id)) {
106
+ const msg = `source_authority.json: deprecated source "${s.id}" appears in precedence_order; deprecated sources cannot be authoritative precursors.`;
107
+ if (strict) result.errors.push(msg);
108
+ else result.warnings.push(msg);
109
+ }
110
+ }
111
+
112
+ // R4. In precedence_order, the first current_highest must not be
113
+ // preceded by any lower-authority source.
114
+ if (order.length > 0 && highest.length > 0) {
115
+ const firstHighestIdx = Math.min(
116
+ ...order
117
+ .map((id, i) => ({ id, i, isHighest: highest.some((h) => h.id === id) }))
118
+ .filter((e) => e.isHighest)
119
+ .map((e) => e.i),
120
+ );
121
+ if (Number.isFinite(firstHighestIdx)) {
122
+ for (let i = 0; i < firstHighestIdx; i++) {
123
+ const id = order[i];
124
+ const src = sources.find((s) => isPlainObject(s) && s.id === id);
125
+ if (!src) continue;
126
+ if (
127
+ AUTHORITY_RANK[src.authority] !== undefined &&
128
+ AUTHORITY_RANK[src.authority] < AUTHORITY_RANK.current_highest
129
+ ) {
130
+ const msg = `source_authority.json: lower-authority source "${src.id}" (${src.authority}) appears before current_highest in precedence_order; current_highest must override.`;
131
+ if (strict) result.errors.push(msg);
132
+ else result.warnings.push(msg);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // R5. PII without consent is a soft warning only.
139
+ const sensitivity = sourceAuthority.sensitivity || {};
140
+ if (sensitivity.sources_contain_pii === true && sensitivity.author_consent_on_file !== true) {
141
+ result.warnings.push(
142
+ 'source_authority.json: sensitivity.sources_contain_pii is true but author_consent_on_file is not true; recording author consent is recommended before publishing.',
143
+ );
144
+ }
145
+
146
+ if (result.errors.length > 0) {
147
+ result.status = 'fail';
148
+ } else if (result.warnings.length > 0) {
149
+ result.status = 'warn';
150
+ } else {
151
+ result.status = 'pass';
152
+ }
153
+ return result;
154
+ }
155
+
156
+ module.exports = { runSagGate, AUTHORITY_RANK };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Truth Charter (TC) compile gate for kdna-studio-core.
3
+ *
4
+ * Implements the TC gate from RFC-0013 §3.2 / §9 #3.
5
+ *
6
+ * Behavior:
7
+ * - Default mode: WARNING only. Errors are downgraded to warnings.
8
+ * - Strict-authority mode: ERROR for synthesized/draft/deprecated
9
+ * tc_status; PASS only for tc_status: "locked".
10
+ * - If no truthCharter provided, gate skips silently
11
+ * (backwards-compatible: legacy workspaces without TC pass through).
12
+ *
13
+ * Rules checked:
14
+ * R1. tc_status must be one of: draft / synthesized / locked / deprecated.
15
+ * R2. tc_status: "synthesized" + strict-authority -> ERROR
16
+ * (synthesized means no real author-locked truth; cannot be
17
+ * officially published without explicit lock).
18
+ * R3. tc_status: "deprecated" + strict-authority -> ERROR
19
+ * (deprecated charters cannot govern new compilations).
20
+ * R4. tc_status: "locked" requires locked_at and locked_by fields.
21
+ * R5. renamed_terms: if renamed_terms are present and a
22
+ * patterns.terminology is supplied, check that each old term is
23
+ * either in banned_terms or its replacement is in standard_terms.
24
+ * (Soft check; mismatches are warnings, not errors.)
25
+ * R6. forbidden_simplifications: presence is recorded; we do NOT
26
+ * perform LLM-based semantic verification (would require a
27
+ * judgment call outside the gate's scope). Always PASS.
28
+ *
29
+ * Cross-file consistency (when both SAG and TC are supplied):
30
+ * If sourceAuthority has any current_highest source of type
31
+ * "human_locked_charter" and TC exists, TC.judgment_authority_holder
32
+ * must be present and non-empty.
33
+ */
34
+
35
+ const VALID_TC_STATUS = ['draft', 'synthesized', 'locked', 'deprecated'];
36
+
37
+ function isPlainObject(v) {
38
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
39
+ }
40
+
41
+ function runTcGate(truthCharter, opts = {}) {
42
+ const strict = !!opts.strict;
43
+ const sourceAuthority = opts.sourceAuthority || null;
44
+ const patterns = opts.patterns || null; // KDNA_Patterns.json content
45
+ const result = {
46
+ gate: 'truth_charter',
47
+ status: 'skipped',
48
+ errors: [],
49
+ warnings: [],
50
+ truth_charter: truthCharter || null,
51
+ strict_authority: strict,
52
+ };
53
+
54
+ if (!isPlainObject(truthCharter)) {
55
+ result.status = 'skipped';
56
+ result.warnings.push('No truth_charter.json provided; TC gate skipped.');
57
+ return result;
58
+ }
59
+
60
+ // R1. tc_status must be valid.
61
+ const tcStatus = truthCharter.tc_status;
62
+ if (!VALID_TC_STATUS.includes(tcStatus)) {
63
+ result.errors.push(
64
+ `truth_charter.json: tc_status "${tcStatus}" is not one of [${VALID_TC_STATUS.join(', ')}].`,
65
+ );
66
+ result.status = 'fail';
67
+ return result;
68
+ }
69
+
70
+ // R2. synthesized + strict -> ERROR.
71
+ if (tcStatus === 'synthesized') {
72
+ const msg = 'truth_charter.json: tc_status is "synthesized" (no author-locked truth); strict-authority requires "locked".';
73
+ if (strict) result.errors.push(msg);
74
+ else result.warnings.push(msg);
75
+ }
76
+
77
+ // R3. deprecated + strict -> ERROR.
78
+ if (tcStatus === 'deprecated') {
79
+ const msg = 'truth_charter.json: tc_status is "deprecated"; deprecated charters cannot govern new compilations under strict-authority.';
80
+ if (strict) result.errors.push(msg);
81
+ else result.warnings.push(msg);
82
+ }
83
+
84
+ // R4. locked requires locked_at and locked_by.
85
+ if (tcStatus === 'locked') {
86
+ if (!truthCharter.locked_at || !truthCharter.locked_by) {
87
+ result.errors.push(
88
+ 'truth_charter.json: tc_status is "locked" but locked_at or locked_by is missing.',
89
+ );
90
+ }
91
+ }
92
+
93
+ // R5. renamed_terms soft check against patterns.terminology.
94
+ // Only fires when the project's terminology actually has content; an
95
+ // empty terminology (from a card-only project) is treated as
96
+ // "not yet declared" and the soft check is skipped.
97
+ if (
98
+ Array.isArray(truthCharter.renamed_terms) &&
99
+ isPlainObject(patterns) &&
100
+ isPlainObject(patterns.terminology) &&
101
+ (Array.isArray(patterns.terminology.standard_terms) && patterns.terminology.standard_terms.length > 0 ||
102
+ Array.isArray(patterns.terminology.banned_terms) && patterns.terminology.banned_terms.length > 0)
103
+ ) {
104
+ const term = patterns.terminology;
105
+ const banned = new Set(
106
+ Array.isArray(term.banned_terms) ? term.banned_terms.map((t) => t && t.term).filter(Boolean) : [],
107
+ );
108
+ const standard = new Set(
109
+ Array.isArray(term.standard_terms) ? term.standard_terms.map((t) => t && t.term).filter(Boolean) : [],
110
+ );
111
+ for (const r of truthCharter.renamed_terms) {
112
+ if (!isPlainObject(r)) continue;
113
+ const oldName = r.old;
114
+ const newName = r.new;
115
+ const oldBanned = oldName && banned.has(oldName);
116
+ const newStandard = newName && standard.has(newName);
117
+ if (oldName && !oldBanned) {
118
+ result.warnings.push(
119
+ `truth_charter.json renamed_terms: old term "${oldName}" is not in KDNA_Patterns.json.terminology.banned_terms; consider adding it to make the rename enforceable.`,
120
+ );
121
+ }
122
+ if (newName && !newStandard) {
123
+ result.warnings.push(
124
+ `truth_charter.json renamed_terms: new term "${newName}" is not in KDNA_Patterns.json.terminology.standard_terms; consider adding it.`,
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ // Cross-file consistency: SAG has human_locked_charter current_highest
131
+ // => TC.judgment_authority_holder must be present and non-empty.
132
+ if (sourceAuthority && isPlainObject(sourceAuthority)) {
133
+ const sources = Array.isArray(sourceAuthority.sources) ? sourceAuthority.sources : [];
134
+ const hasHumanLockedCharter = sources.some(
135
+ (s) => isPlainObject(s) && s.authority === 'current_highest' && s.type === 'human_locked_charter',
136
+ );
137
+ if (hasHumanLockedCharter) {
138
+ const holder = truthCharter.judgment_authority_holder;
139
+ if (!holder || (typeof holder === 'string' && holder.trim() === '')) {
140
+ const msg = 'truth_charter.json: SAG has a current_highest source of type human_locked_charter, but TC.judgment_authority_holder is missing or empty; cross-file consistency requires both.';
141
+ if (strict) result.errors.push(msg);
142
+ else result.warnings.push(msg);
143
+ }
144
+ }
145
+ }
146
+
147
+ if (result.errors.length > 0) {
148
+ result.status = 'fail';
149
+ } else if (result.warnings.length > 0) {
150
+ result.status = 'warn';
151
+ } else {
152
+ result.status = 'pass';
153
+ }
154
+ return result;
155
+ }
156
+
157
+ module.exports = { runTcGate, VALID_TC_STATUS };