@aikdna/kdna-studio-core 1.4.2 → 1.5.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 +53 -10
- package/package.json +3 -2
- package/src/cards/index.js +6 -1
- package/src/compile/index.js +35 -16
- package/src/creator-identity.js +134 -11
- package/src/distillation/index.js +268 -0
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# KDNA Studio Core
|
|
2
2
|
|
|
3
|
-
**KDNA Studio turns
|
|
3
|
+
**KDNA Studio Core is the judgment asset refinery.** It turns scattered notes, documents, works, and feedback into loadable .kdna judgment assets — not by compressing content, but by distilling the stable judgment patterns embedded within it.
|
|
4
4
|
|
|
5
|
-
AI may propose judgment candidates. Humans must confirm judgment. Only human-locked judgment can compile into KDNA.
|
|
5
|
+
AI may propose judgment candidates from content analysis. Humans must confirm judgment. Only human-locked judgment can compile into KDNA.
|
|
6
6
|
|
|
7
|
-
Open-source Studio-compatible authoring kernel for creating trusted `.kdna` assets — JS/npm package.
|
|
7
|
+
Open-source Studio-compatible authoring kernel for creating trusted `.kdna` assets — JS/npm package. Supports two authoring paths: interview-first (expert self-expression) and distillation-first (pattern extraction from existing content). Both end with Human Judgment Lock.
|
|
8
8
|
|
|
9
9
|
**KDNA Studio Core is the JS authoring kernel.** It is not a UI tool and not a CLI package. It is a pure-logic engine for creating KDNA judgment cards, Human Locks, compiler output, and authoring provenance from JavaScript applications and Studio-compatible tools.
|
|
10
10
|
|
|
@@ -13,6 +13,11 @@ Studio-compatible authoring pipeline that performs human confirmation,
|
|
|
13
13
|
validation, canonicalization, identity generation, digest computation, signing,
|
|
14
14
|
optional encryption, and provenance recording.
|
|
15
15
|
|
|
16
|
+
**Hard boundary:** Optional encryption, when supported, MUST be represented as
|
|
17
|
+
protected entries inside the `.kdna` container (RFC-0008). App-private encrypted
|
|
18
|
+
envelopes or transfer wrappers that cannot be opened by KDNA Core are NOT
|
|
19
|
+
conforming KDNA runtime assets.
|
|
20
|
+
|
|
16
21
|
| Library | Language | Role |
|
|
17
22
|
|---------|----------|------|
|
|
18
23
|
| `@aikdna/kdna-cli` | JS/npm | **Operate** KDNA — install, verify, load, compare, publish |
|
|
@@ -24,6 +29,9 @@ optional encryption, and provenance recording.
|
|
|
24
29
|
|
|
25
30
|
- **Project Model** — `studio.project.json` with full metadata, provenance tracking
|
|
26
31
|
- **Evidence Room** — import raw material (text, markdown, interviews, cases)
|
|
32
|
+
- **Distillation Target** — declare domain category, owner scope, granularity, task scope, include/exclude areas, and load condition before extraction
|
|
33
|
+
- **Evidence Relevance** — classify source material as relevant, weakly relevant, out-of-scope, or split-domain before distillation
|
|
34
|
+
- **Scope Gate** — mark candidates with `scope_fit`, relevance score, and suggested split domain before they can become cards
|
|
27
35
|
- **Judgment Cards** — 8 card types: axiom, ontology, stance, framework, misunderstanding, self_check, banned_term, terminology
|
|
28
36
|
- **Human Lock** — AI proposes, human confirms. Only locked cards compile.
|
|
29
37
|
- **Authoring Provenance** — every compiled manifest records Studio-compatible
|
|
@@ -49,6 +57,15 @@ optional encryption, and provenance recording.
|
|
|
49
57
|
Evidence Room → Judgment Cards → Human Lock → Quality Gate → Compile → Validate → Export
|
|
50
58
|
```
|
|
51
59
|
|
|
60
|
+
For distillation-first authoring, the flow starts with an explicit target:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Declare Domain + Scope → Import Evidence → Classify Relevance → Distill Candidates
|
|
64
|
+
→ Scope Gate → Human Review → Promote to Cards → Human Lock → Compile → Export
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A single `.kdna` asset should stay scoped to one domain and loading condition. If a task needs several judgment domains, create multiple domain assets and compose them through a KDNA Cluster rather than making one broad file.
|
|
68
|
+
|
|
52
69
|
## Install
|
|
53
70
|
|
|
54
71
|
```bash
|
|
@@ -63,6 +80,17 @@ The command-line authoring entry is a separate package:
|
|
|
63
80
|
npm install -g @aikdna/kdna-studio-cli
|
|
64
81
|
kdna-studio create my_domain --name @yourscope/my_domain
|
|
65
82
|
kdna-studio import my_domain ./notes.md
|
|
83
|
+
kdna-studio target declare my_domain \
|
|
84
|
+
--category expression_writing \
|
|
85
|
+
--scope personal \
|
|
86
|
+
--granularity core_principles \
|
|
87
|
+
--task "longform article review" \
|
|
88
|
+
--include "argument structure,tone,revision" \
|
|
89
|
+
--exclude "life habits,food preference"
|
|
90
|
+
kdna-studio source classify my_domain
|
|
91
|
+
kdna-studio distill my_domain --candidates candidates.json
|
|
92
|
+
kdna-studio candidate accept my_domain <candidate-id>
|
|
93
|
+
kdna-studio candidate promote my_domain
|
|
66
94
|
kdna-studio card add my_domain axiom \
|
|
67
95
|
--field one_sentence="Judgment principle" \
|
|
68
96
|
--field full_statement="What the agent should do differently" \
|
|
@@ -79,15 +107,31 @@ kdna publish dist/my_domain.kdna
|
|
|
79
107
|
## Quick Start
|
|
80
108
|
|
|
81
109
|
```js
|
|
82
|
-
const {
|
|
110
|
+
const {
|
|
111
|
+
project: projectApi,
|
|
112
|
+
cards: cardApi,
|
|
113
|
+
distillation
|
|
114
|
+
} = require('@aikdna/kdna-studio-core');
|
|
83
115
|
|
|
84
116
|
// 1. Create a project
|
|
85
|
-
const project = createProject('writing_judgment', 'domain', {
|
|
117
|
+
const project = projectApi.createProject('writing_judgment', 'domain', {
|
|
86
118
|
author: { name: 'Writing Expert', id: 'writer_001' }
|
|
87
119
|
});
|
|
88
120
|
|
|
121
|
+
// Optional: declare a distillation target before extracting from evidence.
|
|
122
|
+
const target = distillation.createDistillationTarget({
|
|
123
|
+
domainName: 'writing_judgment',
|
|
124
|
+
domainCategory: 'expression_writing',
|
|
125
|
+
ownerScope: 'personal',
|
|
126
|
+
granularity: 'core_principles',
|
|
127
|
+
taskScope: 'longform article diagnosis and revision',
|
|
128
|
+
includeAreas: ['argument structure', 'reader framing', 'evidence density'],
|
|
129
|
+
excludeAreas: ['life habits', 'food preference']
|
|
130
|
+
});
|
|
131
|
+
project.distillation_target = target;
|
|
132
|
+
|
|
89
133
|
// 2. Create judgment cards
|
|
90
|
-
const card = createCard('axiom', {
|
|
134
|
+
const card = cardApi.createCard('axiom', {
|
|
91
135
|
one_sentence: 'Most writing problems are structural, not language-level.',
|
|
92
136
|
full_statement: 'When reviewing content, diagnose structure before language.',
|
|
93
137
|
why: 'Surface polishing on structurally weak content wastes effort.',
|
|
@@ -98,18 +142,17 @@ const card = createCard('axiom', {
|
|
|
98
142
|
project.cards.push(card);
|
|
99
143
|
|
|
100
144
|
// 3. Human Lock
|
|
101
|
-
const locked = lockCard(card, {
|
|
145
|
+
const locked = cardApi.lockCard(card, {
|
|
102
146
|
by: 'writer_001',
|
|
103
147
|
statement: 'This represents my professional writing judgment.',
|
|
104
148
|
checked: { applies_when: true, does_not_apply_when: true, failure_risk: true }
|
|
105
149
|
});
|
|
106
150
|
|
|
107
151
|
// 4. Check readiness
|
|
108
|
-
const
|
|
109
|
-
const gate = checkHumanLockGate(project);
|
|
152
|
+
const gate = projectApi.checkHumanLockGate(project);
|
|
110
153
|
if (!gate.blocked) {
|
|
111
154
|
// 5. Export
|
|
112
|
-
const json = exportProject(project);
|
|
155
|
+
const json = projectApi.exportProject(project);
|
|
113
156
|
console.log('Ready to publish');
|
|
114
157
|
}
|
|
115
158
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aikdna/kdna-studio-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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,8 @@
|
|
|
24
24
|
"validate"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@aikdna/kdna-core": "^0.
|
|
27
|
+
"@aikdna/kdna-core": "^0.8.0",
|
|
28
|
+
"cbor-x": "^1.6.4"
|
|
28
29
|
},
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=18"
|
package/src/cards/index.js
CHANGED
|
@@ -66,7 +66,7 @@ function lockCard(card, lockPayload) {
|
|
|
66
66
|
if (!lockPayload.checked?.does_not_apply_when) throw new Error('Must confirm does_not_apply_when reviewed');
|
|
67
67
|
if (!lockPayload.checked?.failure_risk) throw new Error('Must confirm failure_risk reviewed');
|
|
68
68
|
|
|
69
|
-
// Schema gate
|
|
69
|
+
// Schema gate per KDNA SPEC
|
|
70
70
|
if (card.type === 'axiom') {
|
|
71
71
|
if (!card.fields?.full_statement || card.fields.full_statement.length < 20) {
|
|
72
72
|
throw new Error(`Axiom ${card.id} cannot be locked: missing or too-short full_statement. SPEC requires a complete, testable explanation.`);
|
|
@@ -75,6 +75,11 @@ function lockCard(card, lockPayload) {
|
|
|
75
75
|
throw new Error(`Axiom ${card.id} cannot be locked: missing or too-short why. SPEC requires an explanation of failure mode.`);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
if (card.type === 'misunderstanding') {
|
|
79
|
+
if (!card.fields?.key_distinction || card.fields.key_distinction.length < 20) {
|
|
80
|
+
throw new Error(`Misunderstanding ${card.id} cannot be locked: missing or too-short key_distinction. SPEC requires a clear conceptual boundary.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
78
83
|
|
|
79
84
|
const lockedCard = { ...card, fields: { ...card.fields } };
|
|
80
85
|
lockedCard.human_lock = {
|
package/src/compile/index.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compile locked cards into KDNA domain JSON files — SPEC-compatible output.
|
|
3
3
|
*
|
|
4
|
-
* KDNA
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - KDNA_Scenarios.json: { meta, scenes[] }
|
|
9
|
-
* - KDNA_Reasoning.json: { meta, reasoning_chains[] }
|
|
10
|
-
* - KDNA_Evolution.json: { meta, stages[], capability_layers[], measurements[] }
|
|
4
|
+
* KDNA Container v2:
|
|
5
|
+
* - Judgment content is encoded as CBOR payload (payload.kdnab)
|
|
6
|
+
* - Individual KDNA_Core.json etc. are NOT exposed as ZIP entries
|
|
7
|
+
* - kdna.json manifest contains metadata only, no judgment content
|
|
11
8
|
*
|
|
12
9
|
* Only locked cards enter compilation. Draft/Revised excluded silently.
|
|
13
10
|
*/
|
|
11
|
+
|
|
12
|
+
const cbor = require('cbor-x');
|
|
14
13
|
const crypto = require('crypto');
|
|
15
14
|
|
|
16
15
|
function uuidv7() {
|
|
@@ -240,8 +239,8 @@ function compileManifest(project, files, identity = null) {
|
|
|
240
239
|
.digest('hex');
|
|
241
240
|
const manifest = {
|
|
242
241
|
format: 'kdna',
|
|
243
|
-
format_version: '
|
|
244
|
-
spec_version: '
|
|
242
|
+
format_version: '2.0',
|
|
243
|
+
spec_version: '2.0',
|
|
245
244
|
name: project.name,
|
|
246
245
|
domain_id: assetIdentity.domain_id,
|
|
247
246
|
asset_uid: assetIdentity.asset_uid,
|
|
@@ -259,6 +258,17 @@ function compileManifest(project, files, identity = null) {
|
|
|
259
258
|
license: project.license || { type: 'CC-BY-4.0' },
|
|
260
259
|
description: project.release?.description || project.name,
|
|
261
260
|
file_count: kdnaFileCount,
|
|
261
|
+
container: {
|
|
262
|
+
type: 'kdna-container-v2',
|
|
263
|
+
payload: 'payload.kdnab',
|
|
264
|
+
payload_encoding: 'cbor',
|
|
265
|
+
payload_schema: 'kdna-payload-v2',
|
|
266
|
+
payload_digest: `sha256:${crypto.createHash('sha256').update(files['payload.kdnab']).digest('hex')}`,
|
|
267
|
+
},
|
|
268
|
+
runtime: {
|
|
269
|
+
min_runtime_version: '0.3.0',
|
|
270
|
+
load_contract: 'context-capsule-v1',
|
|
271
|
+
},
|
|
262
272
|
creator: project.creator_identity ? {
|
|
263
273
|
creator_id: project.creator_identity.creator_id,
|
|
264
274
|
display_name: project.creator_identity.display_name,
|
|
@@ -415,12 +425,21 @@ function compileDomain(project) {
|
|
|
415
425
|
const evolution = compileEvolution(cards, project);
|
|
416
426
|
|
|
417
427
|
const files = {};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
428
|
+
|
|
429
|
+
// ── KDNA Container v2: encode judgment as single CBOR payload ──
|
|
430
|
+
const payload = {
|
|
431
|
+
kind: 'kdna.payload',
|
|
432
|
+
payload_version: '2.0',
|
|
433
|
+
domain: { name: project.name, version: (project.release && project.release.version) || '0.1.0' },
|
|
434
|
+
judgment: { core, patterns },
|
|
435
|
+
profiles: {},
|
|
436
|
+
integrity: {},
|
|
437
|
+
};
|
|
438
|
+
if (scenarios) payload.judgment.scenarios = scenarios;
|
|
439
|
+
if (cases) payload.judgment.cases = cases;
|
|
440
|
+
if (reasoning) payload.judgment.reasoning = reasoning;
|
|
441
|
+
if (evolution) payload.judgment.evolution = evolution;
|
|
442
|
+
files['payload.kdnab'] = cbor.encode(payload);
|
|
424
443
|
|
|
425
444
|
// ── KDNA Card (governance metadata) ─────────────────────────────
|
|
426
445
|
// Must be added BEFORE digest computation so it is included in content_digest.
|
|
@@ -520,7 +539,7 @@ function generateReadme(project, options = {}) {
|
|
|
520
539
|
|
|
521
540
|
if (lockedSelfChecks.length > 0) {
|
|
522
541
|
lines.push('## Eval Score'); lines.push('');
|
|
523
|
-
lines.push(`- quality_badge: ${tests.filter(t => t.result === 'with_kdna_better').length >=
|
|
542
|
+
lines.push(`- quality_badge: ${tests.filter(t => t.result === 'with_kdna_better').length >= 3 ? 'tested' : 'untested'}`);
|
|
524
543
|
lines.push(`- eval cases: ${tests.length}`);
|
|
525
544
|
lines.push('');
|
|
526
545
|
}
|
package/src/creator-identity.js
CHANGED
|
@@ -34,9 +34,10 @@ function creatorFingerprint(publicKeyPem) {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Generate a new Ed25519 keypair and persist to identityDir.
|
|
37
|
-
*
|
|
37
|
+
* If passphrase is provided, the private key is encrypted with AES-256-GCM
|
|
38
|
+
* (key derived via PBKDF2-SHA256). Without passphrase, plaintext with 0o600.
|
|
38
39
|
*/
|
|
39
|
-
function initIdentity(displayName, identityDir = null) {
|
|
40
|
+
function initIdentity(displayName, identityDir = null, passphrase = null) {
|
|
40
41
|
const dir = identityDir || defaultIdentityDir();
|
|
41
42
|
fs.mkdirSync(dir, { recursive: true });
|
|
42
43
|
|
|
@@ -55,7 +56,11 @@ function initIdentity(displayName, identityDir = null) {
|
|
|
55
56
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
const privateKeyData = passphrase
|
|
60
|
+
? encryptPrivateKey(privateKey, passphrase)
|
|
61
|
+
: privateKey;
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(privateKeyPath, privateKeyData, { mode: 0o600 });
|
|
59
64
|
fs.writeFileSync(publicKeyPath, publicKey, { mode: 0o644 });
|
|
60
65
|
|
|
61
66
|
const creatorId = creatorFingerprint(publicKey);
|
|
@@ -67,6 +72,7 @@ function initIdentity(displayName, identityDir = null) {
|
|
|
67
72
|
identity_dir: dir,
|
|
68
73
|
created_at: new Date().toISOString(),
|
|
69
74
|
verified: false,
|
|
75
|
+
encrypted: !!passphrase,
|
|
70
76
|
};
|
|
71
77
|
|
|
72
78
|
fs.writeFileSync(identityJsonPath, JSON.stringify(identity, null, 2), { mode: 0o644 });
|
|
@@ -106,8 +112,9 @@ function loadIdentity(identityDir = null) {
|
|
|
106
112
|
/**
|
|
107
113
|
* Sign a payload with the creator's Ed25519 private key.
|
|
108
114
|
* Returns the signature in "ed25519:<hex>" format.
|
|
115
|
+
* If the key is encrypted, passphrase is required.
|
|
109
116
|
*/
|
|
110
|
-
function signPayload(payload, identityDir = null) {
|
|
117
|
+
function signPayload(payload, identityDir = null, passphrase = null) {
|
|
111
118
|
const dir = identityDir || defaultIdentityDir();
|
|
112
119
|
const privateKeyPath = path.join(dir, PRIVATE_KEY_FILE);
|
|
113
120
|
|
|
@@ -117,21 +124,134 @@ function signPayload(payload, identityDir = null) {
|
|
|
117
124
|
);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
|
|
127
|
+
let privateKeyPem = fs.readFileSync(privateKeyPath, 'utf8');
|
|
128
|
+
if (isEncryptedKey(privateKeyPem)) {
|
|
129
|
+
if (!passphrase) throw new Error('Private key is encrypted. Provide --passphrase to sign.');
|
|
130
|
+
privateKeyPem = decryptPrivateKey(privateKeyPem, passphrase);
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
const data = Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload));
|
|
122
134
|
const sig = crypto.sign(null, data, privateKeyPem);
|
|
123
135
|
return `ed25519:${sig.toString('hex')}`;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
/**
|
|
127
|
-
* Sign a Human Lock payload
|
|
128
|
-
* the
|
|
129
|
-
*
|
|
130
|
-
* Payload: `${cardId}\n${statement}\n${judgmentFingerprint}`
|
|
139
|
+
* Sign a Human Lock payload.
|
|
140
|
+
* If the key is encrypted, passphrase is required.
|
|
131
141
|
*/
|
|
132
|
-
function signHumanLock(cardId, statement, judgmentFingerprint, identityDir = null) {
|
|
142
|
+
function signHumanLock(cardId, statement, judgmentFingerprint, identityDir = null, passphrase = null) {
|
|
133
143
|
const lockPayload = [cardId, statement, judgmentFingerprint].join('\n');
|
|
134
|
-
return signPayload(lockPayload, identityDir);
|
|
144
|
+
return signPayload(lockPayload, identityDir, passphrase);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Private Key Encryption ────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
function encryptPrivateKey(pem, passphrase) {
|
|
150
|
+
const salt = crypto.randomBytes(16);
|
|
151
|
+
const key = crypto.pbkdf2Sync(passphrase, salt, 100000, 32, 'sha256');
|
|
152
|
+
const iv = crypto.randomBytes(12);
|
|
153
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
154
|
+
const ciphertext = Buffer.concat([cipher.update(Buffer.from(pem)), cipher.final()]);
|
|
155
|
+
return JSON.stringify({
|
|
156
|
+
encrypted: true,
|
|
157
|
+
kdf: 'pbkdf2-sha256',
|
|
158
|
+
iterations: 100000,
|
|
159
|
+
salt: salt.toString('base64'),
|
|
160
|
+
iv: iv.toString('base64'),
|
|
161
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
162
|
+
ciphertext: ciphertext.toString('base64'),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function decryptPrivateKey(envelope, passphrase) {
|
|
167
|
+
const env = typeof envelope === 'string' ? JSON.parse(envelope) : envelope;
|
|
168
|
+
if (!env.encrypted) throw new Error('Private key is not encrypted');
|
|
169
|
+
const key = crypto.pbkdf2Sync(passphrase, Buffer.from(env.salt, 'base64'), env.iterations, 32, 'sha256');
|
|
170
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(env.iv, 'base64'));
|
|
171
|
+
decipher.setAuthTag(Buffer.from(env.tag, 'base64'));
|
|
172
|
+
try {
|
|
173
|
+
return Buffer.concat([decipher.update(Buffer.from(env.ciphertext, 'base64')), decipher.final()]).toString('utf8');
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e.code === 'ERR_OSSL_EVP_BAD_DECRYPT' || e.message.includes('bad decrypt')) {
|
|
176
|
+
throw new Error('Wrong passphrase — cannot decrypt private key.');
|
|
177
|
+
}
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isEncryptedKey(content) {
|
|
183
|
+
try { const o = JSON.parse(content); return o.encrypted === true; }
|
|
184
|
+
catch { return false; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Key Rotation ────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Rotate the creator's private key. Generates a new Ed25519 keypair,
|
|
191
|
+
* signs the rotation event with the old key, and saves both.
|
|
192
|
+
* The old public key is recorded in creator.json under previous_keys.
|
|
193
|
+
*/
|
|
194
|
+
function rotateIdentity(passphrase = null, identityDir = null) {
|
|
195
|
+
const identity = loadIdentity(identityDir);
|
|
196
|
+
if (!identity) throw new Error('No identity found. Run identity init first.');
|
|
197
|
+
|
|
198
|
+
const dir = identityDir || defaultIdentityDir();
|
|
199
|
+
const oldPrivPath = path.join(dir, PRIVATE_KEY_FILE);
|
|
200
|
+
const oldPubPath = path.join(dir, PUBLIC_KEY_FILE);
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(oldPrivPath) || !fs.existsSync(oldPubPath)) {
|
|
203
|
+
throw new Error('Key files not found for rotation.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Decrypt old key if needed
|
|
207
|
+
let oldPrivatePem = fs.readFileSync(oldPrivPath, 'utf8');
|
|
208
|
+
if (isEncryptedKey(oldPrivatePem)) {
|
|
209
|
+
if (!passphrase) throw new Error('Private key is encrypted. Provide passphrase to rotate.');
|
|
210
|
+
oldPrivatePem = decryptPrivateKey(oldPrivatePem, passphrase);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Generate new keypair
|
|
214
|
+
const { publicKey: newPub, privateKey: newPriv } = crypto.generateKeyPairSync('ed25519', {
|
|
215
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
216
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Sign rotation event with old key
|
|
220
|
+
const rotationPayload = [
|
|
221
|
+
identity.creator_id,
|
|
222
|
+
creatorFingerprint(newPub),
|
|
223
|
+
new Date().toISOString(),
|
|
224
|
+
].join('\n');
|
|
225
|
+
const rotationSig = `ed25519:${crypto.sign(null, Buffer.from(rotationPayload), oldPrivatePem).toString('hex')}`;
|
|
226
|
+
|
|
227
|
+
// Save new keypair
|
|
228
|
+
const newPrivData = passphrase ? encryptPrivateKey(newPriv, passphrase) : newPriv;
|
|
229
|
+
fs.writeFileSync(oldPrivPath, newPrivData, { mode: 0o600 });
|
|
230
|
+
fs.writeFileSync(oldPubPath, newPub, { mode: 0o644 });
|
|
231
|
+
|
|
232
|
+
// Update identity record
|
|
233
|
+
const newCreatorId = creatorFingerprint(newPub);
|
|
234
|
+
const identityJson = path.join(dir, IDENTITY_JSON_FILE);
|
|
235
|
+
let idData = {};
|
|
236
|
+
try { idData = JSON.parse(fs.readFileSync(identityJson, 'utf8')); } catch {}
|
|
237
|
+
|
|
238
|
+
const previousKeys = idData.previous_keys || [];
|
|
239
|
+
previousKeys.push({
|
|
240
|
+
creator_id: identity.creator_id,
|
|
241
|
+
public_key: identity.public_key,
|
|
242
|
+
rotated_at: new Date().toISOString(),
|
|
243
|
+
rotation_signature: rotationSig,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
idData.creator_id = newCreatorId;
|
|
247
|
+
idData.public_key = newPub;
|
|
248
|
+
idData.rotated_at = new Date().toISOString();
|
|
249
|
+
idData.encrypted = !!passphrase;
|
|
250
|
+
idData.previous_keys = previousKeys;
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(identityJson, JSON.stringify(idData, null, 2), { mode: 0o644 });
|
|
253
|
+
|
|
254
|
+
return { ...idData, creator_id: newCreatorId };
|
|
135
255
|
}
|
|
136
256
|
|
|
137
257
|
/**
|
|
@@ -160,5 +280,8 @@ module.exports = {
|
|
|
160
280
|
loadPublicKey,
|
|
161
281
|
privateKeyPath,
|
|
162
282
|
defaultIdentityDir,
|
|
283
|
+
encryptPrivateKey,
|
|
284
|
+
decryptPrivateKey,
|
|
285
|
+
isEncryptedKey,
|
|
163
286
|
CREATOR_ID_PREFIX,
|
|
164
287
|
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Domain-First Distillation — Core API
|
|
2
|
+
// Aligned with SOURCE_DISTILLATION_CONTRACT.md v0.2
|
|
3
|
+
|
|
4
|
+
// ─── Domain Taxonomy ───────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const DOMAIN_CATEGORIES = Object.freeze({
|
|
7
|
+
expression_writing: {
|
|
8
|
+
id: 'expression_writing',
|
|
9
|
+
displayName: 'Expression & Writing',
|
|
10
|
+
examples: ['writing_style', 'blog_voice', 'social_media_tone', 'article_structure'],
|
|
11
|
+
},
|
|
12
|
+
aesthetic_creation: {
|
|
13
|
+
id: 'aesthetic_creation',
|
|
14
|
+
displayName: 'Aesthetic & Creation',
|
|
15
|
+
examples: ['visual_design', 'video_rhythm', 'cover_art', 'brand_aesthetics'],
|
|
16
|
+
},
|
|
17
|
+
professional_field: {
|
|
18
|
+
id: 'professional_field',
|
|
19
|
+
displayName: 'Professional Field',
|
|
20
|
+
examples: ['legal_judgment', 'medical_diagnosis', 'education_standards'],
|
|
21
|
+
},
|
|
22
|
+
decision_preference: {
|
|
23
|
+
id: 'decision_preference',
|
|
24
|
+
displayName: 'Decision Preference',
|
|
25
|
+
examples: ['product_decisions', 'investment_criteria', 'prioritization_methods'],
|
|
26
|
+
},
|
|
27
|
+
communication_style: {
|
|
28
|
+
id: 'communication_style',
|
|
29
|
+
displayName: 'Communication Style',
|
|
30
|
+
examples: ['client_communication', 'team_management', 'conflict_handling'],
|
|
31
|
+
},
|
|
32
|
+
workflow_process: {
|
|
33
|
+
id: 'workflow_process',
|
|
34
|
+
displayName: 'Workflow & Process',
|
|
35
|
+
examples: ['project_reviews', 'meeting_standards', 'sales_followups'],
|
|
36
|
+
},
|
|
37
|
+
life_preference: {
|
|
38
|
+
id: 'life_preference',
|
|
39
|
+
displayName: 'Life Preference',
|
|
40
|
+
examples: ['schedule_preferences', 'learning_habits', 'consumption_choices'],
|
|
41
|
+
},
|
|
42
|
+
team_organization: {
|
|
43
|
+
id: 'team_organization',
|
|
44
|
+
displayName: 'Team & Organization',
|
|
45
|
+
examples: ['team_brand', 'hiring_criteria', 'service_standards'],
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const OWNER_SCOPES = Object.freeze({
|
|
50
|
+
personal: {
|
|
51
|
+
id: 'personal',
|
|
52
|
+
displayName: 'Personal',
|
|
53
|
+
description: 'One person\'s individual standards. Extracts personal preferences, boundaries, taste.',
|
|
54
|
+
},
|
|
55
|
+
team: {
|
|
56
|
+
id: 'team',
|
|
57
|
+
displayName: 'Team',
|
|
58
|
+
description: 'Shared team conventions. Extracts team-wide standards, agreed practices.',
|
|
59
|
+
},
|
|
60
|
+
organization: {
|
|
61
|
+
id: 'organization',
|
|
62
|
+
displayName: 'Organization',
|
|
63
|
+
description: 'Company/organization policies. Extracts organizational values, compliance boundaries.',
|
|
64
|
+
},
|
|
65
|
+
field: {
|
|
66
|
+
id: 'field',
|
|
67
|
+
displayName: 'Industry / Field',
|
|
68
|
+
description: 'Industry/profession-wide. Extracts domain expertise beyond any single practitioner.',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const GRANULARITY_LEVELS = Object.freeze({
|
|
73
|
+
core_principles: {
|
|
74
|
+
id: 'core_principles',
|
|
75
|
+
displayName: 'Core Principles',
|
|
76
|
+
description: 'High-level axioms and boundaries. Foundational beliefs, what the person consistently prioritizes and rejects.',
|
|
77
|
+
},
|
|
78
|
+
concrete_patterns: {
|
|
79
|
+
id: 'concrete_patterns',
|
|
80
|
+
displayName: 'Concrete Patterns',
|
|
81
|
+
description: 'Recurring decision patterns. Specific rules and detectable habits.',
|
|
82
|
+
},
|
|
83
|
+
specific_scenarios: {
|
|
84
|
+
id: 'specific_scenarios',
|
|
85
|
+
displayName: 'Specific Scenarios',
|
|
86
|
+
description: 'Scenario-level triggers. Context-specific judgment triggers and responses.',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const EVIDENCE_RELEVANCE = Object.freeze({
|
|
91
|
+
relevant: { id: 'relevant', label: 'Relevant' },
|
|
92
|
+
weakly_relevant: { id: 'weakly_relevant', label: 'Weakly Relevant' },
|
|
93
|
+
out_of_scope: { id: 'out_of_scope', label: 'Out of Scope' },
|
|
94
|
+
split_domain: { id: 'split_domain', label: 'Split Domain' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── Distillation Target ───────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function createDistillationTarget({
|
|
100
|
+
domainName,
|
|
101
|
+
domainCategory = 'expression_writing',
|
|
102
|
+
ownerScope = 'personal',
|
|
103
|
+
granularity = 'core_principles',
|
|
104
|
+
taskScope = '',
|
|
105
|
+
includeAreas = [],
|
|
106
|
+
excludeAreas = [],
|
|
107
|
+
loadCondition = '',
|
|
108
|
+
}) {
|
|
109
|
+
if (!domainName || typeof domainName !== 'string') {
|
|
110
|
+
throw new Error('domainName is required');
|
|
111
|
+
}
|
|
112
|
+
if (!DOMAIN_CATEGORIES[domainCategory]) {
|
|
113
|
+
throw new Error(`Invalid domainCategory: ${domainCategory}. Must be one of: ${Object.keys(DOMAIN_CATEGORIES).join(', ')}`);
|
|
114
|
+
}
|
|
115
|
+
if (!OWNER_SCOPES[ownerScope]) {
|
|
116
|
+
throw new Error(`Invalid ownerScope: ${ownerScope}`);
|
|
117
|
+
}
|
|
118
|
+
if (!GRANULARITY_LEVELS[granularity]) {
|
|
119
|
+
throw new Error(`Invalid granularity: ${granularity}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: `tgt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
124
|
+
domain_name: domainName,
|
|
125
|
+
domain_category: domainCategory,
|
|
126
|
+
owner_scope: ownerScope,
|
|
127
|
+
granularity,
|
|
128
|
+
task_scope: taskScope,
|
|
129
|
+
include_areas: includeAreas,
|
|
130
|
+
exclude_areas: excludeAreas,
|
|
131
|
+
load_condition: loadCondition || `Load when the task involves ${DOMAIN_CATEGORIES[domainCategory].displayName.toLowerCase()} judgment.`,
|
|
132
|
+
declared_at: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateDistillationTarget(target) {
|
|
137
|
+
const errors = [];
|
|
138
|
+
if (!target || !target.domain_name) errors.push('domain_name is required');
|
|
139
|
+
if (!DOMAIN_CATEGORIES[target.domain_category]) errors.push(`invalid domain_category: ${target.domain_category}`);
|
|
140
|
+
if (!OWNER_SCOPES[target.owner_scope]) errors.push(`invalid owner_scope: ${target.owner_scope}`);
|
|
141
|
+
if (!GRANULARITY_LEVELS[target.granularity]) errors.push(`invalid granularity: ${target.granularity}`);
|
|
142
|
+
if (!target.task_scope || target.task_scope.trim().length === 0) errors.push('task_scope is required');
|
|
143
|
+
return { valid: errors.length === 0, errors };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function targetScopeDescription(target) {
|
|
147
|
+
const cat = DOMAIN_CATEGORIES[target.domain_category];
|
|
148
|
+
const scope = OWNER_SCOPES[target.owner_scope];
|
|
149
|
+
const gran = GRANULARITY_LEVELS[target.granularity];
|
|
150
|
+
return `Domain: ${cat.displayName}. Scope: ${scope.displayName}. Granularity: ${gran.displayName}. Task: ${target.task_scope}. ${target.load_condition}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Scope Gate ────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function applyScopeGate(candidate, target) {
|
|
156
|
+
const text = `${candidate.one_sentence || ''} ${candidate.full_statement || ''}`.toLowerCase();
|
|
157
|
+
const domainWords = [target.domain_category, ...(target.include_areas || [])];
|
|
158
|
+
|
|
159
|
+
let scopeFit = true;
|
|
160
|
+
let relevanceScore = 50;
|
|
161
|
+
let relevanceEvidence = null;
|
|
162
|
+
let suggestedSplitDomain = null;
|
|
163
|
+
|
|
164
|
+
const domainMatch = domainWords.some(w => text.includes(w.toLowerCase()));
|
|
165
|
+
const excludeMatch = (target.exclude_areas || []).some(w => text.includes(w.toLowerCase()));
|
|
166
|
+
|
|
167
|
+
if (domainMatch && !excludeMatch) {
|
|
168
|
+
scopeFit = true;
|
|
169
|
+
relevanceScore = 80;
|
|
170
|
+
} else if (!domainMatch && (target.include_areas || []).length > 0) {
|
|
171
|
+
scopeFit = false;
|
|
172
|
+
relevanceScore = 20;
|
|
173
|
+
relevanceEvidence = 'No match with declared include areas';
|
|
174
|
+
} else if (excludeMatch) {
|
|
175
|
+
scopeFit = false;
|
|
176
|
+
relevanceScore = 10;
|
|
177
|
+
relevanceEvidence = 'Matched exclude area';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!scopeFit) {
|
|
181
|
+
for (const [catId, cat] of Object.entries(DOMAIN_CATEGORIES)) {
|
|
182
|
+
if (catId !== target.domain_category) {
|
|
183
|
+
if (text.includes(catId) || text.includes(cat.displayName.toLowerCase())) {
|
|
184
|
+
suggestedSplitDomain = cat.displayName;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
...candidate,
|
|
193
|
+
scope_fit: scopeFit,
|
|
194
|
+
domain_relevance_score: relevanceScore,
|
|
195
|
+
relevance_evidence: relevanceEvidence,
|
|
196
|
+
suggested_split_domain: suggestedSplitDomain,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function candidateStatusSummary(candidates) {
|
|
201
|
+
let proposed = 0, accepted = 0, rejected = 0, modified = 0, blocked = 0, outOfScope = 0;
|
|
202
|
+
for (const c of candidates) {
|
|
203
|
+
if (c.sensitive_content_flag) { blocked++; continue; }
|
|
204
|
+
if (!c.scope_fit) { outOfScope++; }
|
|
205
|
+
switch (c.status || c.candidate_status) {
|
|
206
|
+
case 'proposed': proposed++; break;
|
|
207
|
+
case 'accepted': accepted++; break;
|
|
208
|
+
case 'rejected': rejected++; break;
|
|
209
|
+
case 'modified': modified++; break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { proposed, accepted, rejected, modified, blocked, outOfScope };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Sensitive Inference Filter ────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
const SENSITIVE_KEYWORDS = {
|
|
218
|
+
identity: [
|
|
219
|
+
'gender identity', 'sexual orientation', 'ethnicity', 'race', 'racial',
|
|
220
|
+
'性别认同', '性取向', '种族', '民族',
|
|
221
|
+
],
|
|
222
|
+
health: [
|
|
223
|
+
'medical condition', 'mental health', 'disability', 'diagnosis', 'medication', 'therapy', 'treatment',
|
|
224
|
+
'疾病', '病史', '诊断', '药物', '治疗', '心理疾病', '精神健康', '残疾', '残障',
|
|
225
|
+
],
|
|
226
|
+
political: [
|
|
227
|
+
'political affiliation', 'voting', 'activist', 'party member',
|
|
228
|
+
'政治立场', '党派', '党员', '政治倾向',
|
|
229
|
+
],
|
|
230
|
+
religious: [
|
|
231
|
+
'religious belief', 'faith', 'church', 'prayer', 'worship', 'spiritual practice',
|
|
232
|
+
'宗教信仰', '教会', '祈祷', '礼拜', '信教',
|
|
233
|
+
],
|
|
234
|
+
financial: [
|
|
235
|
+
'income', 'net worth', 'salary', 'debt', 'bank account', 'savings amount',
|
|
236
|
+
'收入', '工资', '存款', '负债', '资产净值', '银行卡号', '账户余额',
|
|
237
|
+
],
|
|
238
|
+
intimate: [
|
|
239
|
+
'relationship status', 'marriage', 'divorce', 'family structure',
|
|
240
|
+
'婚姻状况', '离婚', '家庭结构', '感情状况', '亲密关系',
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
function checkSensitiveContent(text) {
|
|
245
|
+
const lower = text.toLowerCase();
|
|
246
|
+
for (const [domain, keywords] of Object.entries(SENSITIVE_KEYWORDS)) {
|
|
247
|
+
for (const keyword of keywords) {
|
|
248
|
+
if (lower.includes(keyword.toLowerCase())) {
|
|
249
|
+
return { flagged: true, reason: `May involve sensitive domain: ${domain}` };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { flagged: false, reason: null };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
DOMAIN_CATEGORIES,
|
|
258
|
+
OWNER_SCOPES,
|
|
259
|
+
GRANULARITY_LEVELS,
|
|
260
|
+
EVIDENCE_RELEVANCE,
|
|
261
|
+
SENSITIVE_KEYWORDS,
|
|
262
|
+
createDistillationTarget,
|
|
263
|
+
validateDistillationTarget,
|
|
264
|
+
targetScopeDescription,
|
|
265
|
+
applyScopeGate,
|
|
266
|
+
candidateStatusSummary,
|
|
267
|
+
checkSensitiveContent,
|
|
268
|
+
};
|
package/src/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const provenance = require('./provenance');
|
|
|
30
30
|
const quality = require('./quality');
|
|
31
31
|
const testlab = require('./testlab');
|
|
32
32
|
const versioning = require('./versioning');
|
|
33
|
+
const distillation = require('./distillation');
|
|
33
34
|
const feynman = require('./cards/feynman');
|
|
34
35
|
const contradiction = require('./quality/contradiction');
|
|
35
36
|
const validateCards = require('./quality/validate-cards');
|
|
@@ -46,6 +47,7 @@ module.exports = {
|
|
|
46
47
|
governance,
|
|
47
48
|
i18n,
|
|
48
49
|
creator: creatorIdentity,
|
|
50
|
+
distillation,
|
|
49
51
|
|
|
50
52
|
// Experimental
|
|
51
53
|
evidence,
|