@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 +1 -1
- package/package.json +2 -2
- package/src/cards/index.js +1 -1
- package/src/compile/index.js +53 -4
- package/src/compile/source-authority-gate.js +156 -0
- package/src/compile/truth-charter-gate.js +157 -0
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
|
|
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.
|
|
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.
|
|
27
|
+
"@aikdna/kdna-core": "^0.9.1",
|
|
28
28
|
"cbor-x": "^1.6.4"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
package/src/cards/index.js
CHANGED
|
@@ -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'],
|
package/src/compile/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compile locked cards into KDNA domain JSON files — SPEC-compatible output.
|
|
3
3
|
*
|
|
4
|
-
* KDNA Container
|
|
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
|
-
//
|
|
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 };
|