@aikdna/kdna-cli 0.16.10 → 0.18.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 +158 -75
- package/package.json +5 -5
- package/skills/kdna-loader/SKILL.md +5 -6
- package/src/agent.js +489 -79
- package/src/cli.js +112 -62
- package/src/cmds/_common.js +32 -16
- package/src/cmds/badge.js +7 -7
- package/src/cmds/changelog.js +1 -1
- package/src/cmds/cluster.js +16 -48
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +213 -443
- package/src/cmds/explain.js +122 -0
- package/src/cmds/legacy.js +8 -8
- package/src/cmds/license.js +483 -26
- package/src/cmds/quality.js +14 -2
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +4 -5
- package/src/cmds/test.js +4 -4
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +28 -22
- package/src/diff.js +11 -13
- package/src/init.js +2 -2
- package/src/install.js +138 -460
- package/src/loader.js +10 -10
- package/src/package-store.js +229 -0
- package/src/paths.js +44 -0
- package/src/publish.js +184 -22
- package/src/registry.js +76 -9
- package/src/setup.js +19 -20
- package/src/verify.js +275 -121
- package/templates/standard-domain/kdna.json +2 -1
- package/validators/kdna-lint.js +37 -3
- package/validators/kdna-validate.js +3 -2
- package/src/cmds/encrypt.js +0 -199
package/src/loader.js
CHANGED
|
@@ -15,8 +15,8 @@ const FILE_MAP = core.FILE_MAP;
|
|
|
15
15
|
* Read and parse a KDNA JSON file.
|
|
16
16
|
* Returns null if the file does not exist.
|
|
17
17
|
*/
|
|
18
|
-
function readFile(
|
|
19
|
-
const filePath = path.join(
|
|
18
|
+
function readFile(sourceDir, filename) {
|
|
19
|
+
const filePath = path.join(sourceDir, filename);
|
|
20
20
|
if (!fs.existsSync(filePath)) return null;
|
|
21
21
|
try {
|
|
22
22
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -29,32 +29,32 @@ function readFile(domainDir, filename) {
|
|
|
29
29
|
* Load the minimum required KDNA files (Core + Patterns).
|
|
30
30
|
* Always load these. They form the cognition baseline.
|
|
31
31
|
*/
|
|
32
|
-
function loadCorePatterns(
|
|
33
|
-
const coreData = readFile(
|
|
34
|
-
const patternsData = readFile(
|
|
32
|
+
function loadCorePatterns(sourceDir) {
|
|
33
|
+
const coreData = readFile(sourceDir, FILE_MAP.core);
|
|
34
|
+
const patternsData = readFile(sourceDir, FILE_MAP.patterns);
|
|
35
35
|
return core.loadCorePatternsFromData(coreData, patternsData);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Load a complete KDNA domain.
|
|
40
40
|
*
|
|
41
|
-
* @param {string}
|
|
41
|
+
* @param {string} sourceDir — path to a dev source directory
|
|
42
42
|
* @param {object} [options]
|
|
43
43
|
* @param {string} [options.input] — user input text for conditional loading
|
|
44
44
|
* @param {'all'|'minimum'|'auto'} [options.mode='auto'] — loading mode
|
|
45
45
|
* @returns {object|null} loaded KDNA files keyed by type, or null if minimum files are missing
|
|
46
46
|
*/
|
|
47
|
-
function loadDomain(
|
|
47
|
+
function loadDomain(sourceDir, options = {}) {
|
|
48
48
|
const dataMap = { core: null, patterns: null };
|
|
49
49
|
|
|
50
|
-
dataMap.core = readFile(
|
|
51
|
-
dataMap.patterns = readFile(
|
|
50
|
+
dataMap.core = readFile(sourceDir, FILE_MAP.core);
|
|
51
|
+
dataMap.patterns = readFile(sourceDir, FILE_MAP.patterns);
|
|
52
52
|
|
|
53
53
|
if (!dataMap.core || !dataMap.patterns) return null;
|
|
54
54
|
|
|
55
55
|
// Also read optional files that might be present
|
|
56
56
|
for (const key of ['scenarios', 'cases', 'reasoning', 'evolution']) {
|
|
57
|
-
const data = readFile(
|
|
57
|
+
const data = readFile(sourceDir, FILE_MAP[key]);
|
|
58
58
|
if (data) dataMap[key] = data;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const PATHS = require('./paths');
|
|
5
|
+
const { parseName } = require('./registry');
|
|
6
|
+
const core = require('@aikdna/kdna-core');
|
|
7
|
+
|
|
8
|
+
if (typeof core.createKdnaAssetReader !== 'function') {
|
|
9
|
+
throw new Error('@aikdna/kdna-core >=0.5.0 is required for direct .kdna asset loading');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const assetReader = core.createKdnaAssetReader();
|
|
13
|
+
|
|
14
|
+
const INDEX_VERSION = 2;
|
|
15
|
+
|
|
16
|
+
function ensureDir(dir) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJsonFile(file) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJsonFile(file, data) {
|
|
29
|
+
ensureDir(path.dirname(file));
|
|
30
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readIndex() {
|
|
34
|
+
const data = readJsonFile(PATHS.packageIndex);
|
|
35
|
+
if (data?.packages && typeof data.packages === 'object') return data;
|
|
36
|
+
return { version: INDEX_VERSION, packages: {} };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeIndex(index) {
|
|
40
|
+
index.version = INDEX_VERSION;
|
|
41
|
+
writeJsonFile(PATHS.packageIndex, index);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sha256File(filePath) {
|
|
45
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assetDigest(filePath) {
|
|
49
|
+
return `sha256:${sha256File(filePath)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function contentDigest(kdnaPath) {
|
|
53
|
+
return assetReader.contentDigestSync(assetReader.openSync(kdnaPath));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assetDir(scope, ident, version) {
|
|
57
|
+
return path.join(PATHS.packages, scope, ident, version || 'unknown');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assetFileName(ident, version) {
|
|
61
|
+
return `${ident}-${version || 'unknown'}.kdna`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readContainerJson(kdnaPath, fileName, options = {}) {
|
|
65
|
+
const asset = assetReader.openSync(kdnaPath);
|
|
66
|
+
return assetReader.readJsonSync(asset, fileName, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readContainerEntry(kdnaPath, fileName) {
|
|
70
|
+
const asset = assetReader.openSync(kdnaPath);
|
|
71
|
+
return assetReader.readEntrySync(asset, fileName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function listContainerEntries(kdnaPath) {
|
|
75
|
+
const asset = assetReader.openSync(kdnaPath);
|
|
76
|
+
return assetReader.listEntriesSync(asset);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readContainer(kdnaPath, options = {}) {
|
|
80
|
+
const asset = assetReader.openSync(kdnaPath);
|
|
81
|
+
return {
|
|
82
|
+
manifest: assetReader.readJsonSync(asset, 'kdna.json'),
|
|
83
|
+
core: assetReader.readJsonSync(asset, 'KDNA_Core.json', options),
|
|
84
|
+
patterns: assetReader.readJsonSync(asset, 'KDNA_Patterns.json', options),
|
|
85
|
+
scenarios: assetReader.readJsonSync(asset, 'KDNA_Scenarios.json', options),
|
|
86
|
+
cases: assetReader.readJsonSync(asset, 'KDNA_Cases.json', options),
|
|
87
|
+
reasoning: assetReader.readJsonSync(asset, 'KDNA_Reasoning.json', options),
|
|
88
|
+
evolution: assetReader.readJsonSync(asset, 'KDNA_Evolution.json', options),
|
|
89
|
+
files: assetReader.listEntriesSync(asset),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function verifyAsset(kdnaPath, options = {}) {
|
|
94
|
+
const asset = assetReader.openSync(kdnaPath);
|
|
95
|
+
return assetReader.verifySync(asset, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getInstalled(input) {
|
|
99
|
+
const parsed = parseName(input);
|
|
100
|
+
if (!parsed) return null;
|
|
101
|
+
const index = readIndex();
|
|
102
|
+
const entry = index.packages[parsed.full];
|
|
103
|
+
if (!entry?.asset_path || !fs.existsSync(entry.asset_path)) return null;
|
|
104
|
+
return { ...entry, parsed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function listInstalled() {
|
|
108
|
+
const index = readIndex();
|
|
109
|
+
return Object.entries(index.packages)
|
|
110
|
+
.map(([full, entry]) => ({ full, ...entry }))
|
|
111
|
+
.filter((entry) => entry.asset_path && fs.existsSync(entry.asset_path))
|
|
112
|
+
.sort((a, b) => a.full.localeCompare(b.full));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readAssetManifest(assetPath) {
|
|
116
|
+
return readContainerJson(assetPath, 'kdna.json') || {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function receiptPathForAsset(assetPath) {
|
|
120
|
+
return path.join(path.dirname(assetPath), 'receipt.json');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function installAsset({ sourcePath, name, version, source = {} }) {
|
|
124
|
+
const parsed = parseName(name);
|
|
125
|
+
if (!parsed) throw new Error(`Invalid scoped domain name: ${name}`);
|
|
126
|
+
const finalVersion = version || 'unknown';
|
|
127
|
+
const destDir = assetDir(parsed.scope, parsed.ident, finalVersion);
|
|
128
|
+
const dest = path.join(destDir, assetFileName(parsed.ident, finalVersion));
|
|
129
|
+
ensureDir(destDir);
|
|
130
|
+
fs.copyFileSync(sourcePath, dest);
|
|
131
|
+
|
|
132
|
+
const manifest = readAssetManifest(dest);
|
|
133
|
+
const installedAt = new Date().toISOString();
|
|
134
|
+
const computedAssetDigest = assetDigest(dest);
|
|
135
|
+
const computedContentDigest = contentDigest(dest);
|
|
136
|
+
const receiptPath = receiptPathForAsset(dest);
|
|
137
|
+
const receipt = {
|
|
138
|
+
version: 1,
|
|
139
|
+
name: parsed.full,
|
|
140
|
+
asset_path: dest,
|
|
141
|
+
asset_digest: computedAssetDigest,
|
|
142
|
+
content_digest: computedContentDigest,
|
|
143
|
+
package_version: finalVersion,
|
|
144
|
+
judgment_version: manifest.judgment_version || null,
|
|
145
|
+
access: manifest.access || 'open',
|
|
146
|
+
signature: manifest.signature || null,
|
|
147
|
+
installed_at: installedAt,
|
|
148
|
+
source,
|
|
149
|
+
};
|
|
150
|
+
writeJsonFile(receiptPath, receipt);
|
|
151
|
+
|
|
152
|
+
const index = readIndex();
|
|
153
|
+
index.packages[parsed.full] = {
|
|
154
|
+
name: parsed.full,
|
|
155
|
+
version: finalVersion,
|
|
156
|
+
asset_path: dest,
|
|
157
|
+
receipt_path: receiptPath,
|
|
158
|
+
asset_digest: computedAssetDigest,
|
|
159
|
+
content_digest: computedContentDigest,
|
|
160
|
+
judgment_version: manifest.judgment_version || null,
|
|
161
|
+
access: manifest.access || 'open',
|
|
162
|
+
signature: manifest.signature || null,
|
|
163
|
+
installed_at: installedAt,
|
|
164
|
+
source,
|
|
165
|
+
};
|
|
166
|
+
writeIndex(index);
|
|
167
|
+
return index.packages[parsed.full];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function resolveAsset(input) {
|
|
171
|
+
const expanded = input.replace(/^~/, process.env.HOME || '');
|
|
172
|
+
const looksLikeFile =
|
|
173
|
+
input.endsWith('.kdna') &&
|
|
174
|
+
(input.startsWith('./') || input.startsWith('/') || input.startsWith('~/') || fs.existsSync(expanded));
|
|
175
|
+
if (looksLikeFile) {
|
|
176
|
+
const abs = path.resolve(expanded);
|
|
177
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
178
|
+
const manifest = readAssetManifest(abs);
|
|
179
|
+
const full = manifest.name;
|
|
180
|
+
const parsed = full ? parseName(full) : null;
|
|
181
|
+
return {
|
|
182
|
+
name: full || path.basename(abs, '.kdna'),
|
|
183
|
+
parsed,
|
|
184
|
+
asset_path: abs,
|
|
185
|
+
receipt_path: null,
|
|
186
|
+
version: manifest.version || null,
|
|
187
|
+
judgment_version: manifest.judgment_version || null,
|
|
188
|
+
access: manifest.access || 'open',
|
|
189
|
+
asset_digest: assetDigest(abs),
|
|
190
|
+
content_digest: contentDigest(abs),
|
|
191
|
+
source: { type: 'local-file', path: abs },
|
|
192
|
+
local_file: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return getInstalled(input);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function removeInstalled(input) {
|
|
199
|
+
const parsed = parseName(input);
|
|
200
|
+
if (!parsed) return false;
|
|
201
|
+
const index = readIndex();
|
|
202
|
+
const entry = index.packages[parsed.full];
|
|
203
|
+
if (!entry) return false;
|
|
204
|
+
delete index.packages[parsed.full];
|
|
205
|
+
writeIndex(index);
|
|
206
|
+
if (entry.asset_path) {
|
|
207
|
+
const versionDir = path.dirname(entry.asset_path);
|
|
208
|
+
fs.rmSync(versionDir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
readIndex,
|
|
215
|
+
writeIndex,
|
|
216
|
+
sha256File,
|
|
217
|
+
assetDigest,
|
|
218
|
+
contentDigest,
|
|
219
|
+
readContainer,
|
|
220
|
+
readContainerEntry,
|
|
221
|
+
readContainerJson,
|
|
222
|
+
listContainerEntries,
|
|
223
|
+
verifyAsset,
|
|
224
|
+
getInstalled,
|
|
225
|
+
listInstalled,
|
|
226
|
+
installAsset,
|
|
227
|
+
resolveAsset,
|
|
228
|
+
removeInstalled,
|
|
229
|
+
};
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// KDNA shared path configuration — canonical source for ~/.kdna structure
|
|
2
|
+
// Spec: docs/local-kdna-home-spec.md
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const KDNA_HOME = process.env.KDNA_HOME
|
|
7
|
+
|| path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
|
|
8
|
+
|
|
9
|
+
const PATHS = {
|
|
10
|
+
root: KDNA_HOME,
|
|
11
|
+
config: path.join(KDNA_HOME, 'config.json'),
|
|
12
|
+
identity: path.join(KDNA_HOME, 'identity'),
|
|
13
|
+
domains: {
|
|
14
|
+
root: path.join(KDNA_HOME, 'domains'),
|
|
15
|
+
official: path.join(KDNA_HOME, 'domains', 'official'),
|
|
16
|
+
local: path.join(KDNA_HOME, 'domains', 'local'),
|
|
17
|
+
private: path.join(KDNA_HOME, 'domains', 'private'),
|
|
18
|
+
// Legacy flat path — used for migration only
|
|
19
|
+
legacy: path.join(KDNA_HOME, 'domains'),
|
|
20
|
+
// All three directories for scanning
|
|
21
|
+
all: [
|
|
22
|
+
path.join(KDNA_HOME, 'domains', 'official'),
|
|
23
|
+
path.join(KDNA_HOME, 'domains', 'local'),
|
|
24
|
+
path.join(KDNA_HOME, 'domains', 'private'),
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
clusters: path.join(KDNA_HOME, 'clusters'),
|
|
28
|
+
packages: path.join(KDNA_HOME, 'packages'),
|
|
29
|
+
packageIndex: path.join(KDNA_HOME, 'index.json'),
|
|
30
|
+
registry: path.join(KDNA_HOME, 'registry'),
|
|
31
|
+
registryCache: path.join(KDNA_HOME, 'registry', 'cache.json'),
|
|
32
|
+
traces: path.join(KDNA_HOME, 'traces'),
|
|
33
|
+
feedback: path.join(KDNA_HOME, 'feedback'),
|
|
34
|
+
evals: path.join(KDNA_HOME, 'evals'),
|
|
35
|
+
cache: path.join(KDNA_HOME, 'cache'),
|
|
36
|
+
licenses: path.join(KDNA_HOME, 'licenses'),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Runtime asset store aliases
|
|
40
|
+
PATHS.USER_KDNA_DIR = KDNA_HOME;
|
|
41
|
+
PATHS.INSTALL_DIR = PATHS.packages;
|
|
42
|
+
|
|
43
|
+
module.exports = PATHS;
|
|
44
|
+
module.exports.KDNA_HOME = KDNA_HOME;
|
package/src/publish.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { EXIT } = require('./cmds/_common');
|
|
10
|
+
const { EXIT, selfCheckText, isYesNoSelfCheck } = require('./cmds/_common');
|
|
11
11
|
|
|
12
12
|
function error(msg, code = EXIT.VALIDATION_FAILED) {
|
|
13
13
|
console.error(`Error: ${msg}`);
|
|
@@ -141,9 +141,69 @@ function isGenericSelfCheck(question) {
|
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// ─── Human Lock Gate ──────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check whether the domain satisfies Human Lock requirements.
|
|
148
|
+
* Returns { passed, issues[] } — publish should be blocked if !passed.
|
|
149
|
+
*/
|
|
150
|
+
function checkHumanLock(domainPath) {
|
|
151
|
+
const core = readJson(path.join(domainPath, 'KDNA_Core.json'));
|
|
152
|
+
if (!core) return { passed: false, issues: ['KDNA_Core.json not found'] };
|
|
153
|
+
|
|
154
|
+
const issues = [];
|
|
155
|
+
const cards = [];
|
|
156
|
+
|
|
157
|
+
// Collect judgment-class cards from axioms, boundaries, risks
|
|
158
|
+
if (core.axioms) {
|
|
159
|
+
for (const a of core.axioms) {
|
|
160
|
+
cards.push({ type: 'axiom', id: a.id || '?', status: a.status, human_lock: a.human_lock });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (core.boundaries) {
|
|
164
|
+
for (const b of core.boundaries) {
|
|
165
|
+
cards.push({ type: 'boundary', id: b.id || '?', status: b.status, human_lock: b.human_lock });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (core.risks || core.risk_model) {
|
|
169
|
+
const risks = core.risks || core.risk_model || [];
|
|
170
|
+
for (const r of risks) {
|
|
171
|
+
cards.push({ type: 'risk', id: r.id || '?', status: r.status, human_lock: r.human_lock });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cards.length === 0) return { passed: true, issues: [] };
|
|
176
|
+
|
|
177
|
+
for (const card of cards) {
|
|
178
|
+
// Rule 1: Must be locked
|
|
179
|
+
if (!card.status || !['locked', 'tested', 'published'].includes(card.status)) {
|
|
180
|
+
issues.push(`${card.type} "${card.id}" is not locked. Human Lock required before publish.`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Rule 2: Must have human_lock record
|
|
184
|
+
if (!card.human_lock || !card.human_lock.by || !card.human_lock.statement) {
|
|
185
|
+
issues.push(`${card.type} "${card.id}" is locked but has no valid Human Lock record.`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Rule 3: Lock must confirm judgment fields were reviewed
|
|
189
|
+
const checked = card.human_lock.checked || {};
|
|
190
|
+
if (!checked.applies_when) {
|
|
191
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`);
|
|
192
|
+
}
|
|
193
|
+
if (!checked.does_not_apply_when) {
|
|
194
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`);
|
|
195
|
+
}
|
|
196
|
+
if (!checked.failure_risk) {
|
|
197
|
+
issues.push(`${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { passed: issues.length === 0, issues };
|
|
202
|
+
}
|
|
203
|
+
|
|
144
204
|
// ─── Main check function ──────────────────────────────────────────────
|
|
145
205
|
|
|
146
|
-
function cmdPublishCheck(domainPath) {
|
|
206
|
+
function cmdPublishCheck(domainPath, args = []) {
|
|
147
207
|
const abs = path.resolve(domainPath);
|
|
148
208
|
if (!fs.existsSync(abs)) error(`Domain not found: ${abs}`);
|
|
149
209
|
|
|
@@ -152,6 +212,35 @@ function cmdPublishCheck(domainPath) {
|
|
|
152
212
|
console.log('═'.repeat(60));
|
|
153
213
|
console.log('');
|
|
154
214
|
|
|
215
|
+
// ─── Human Lock Gate (must pass before any other checks) ──────────
|
|
216
|
+
const hl = checkHumanLock(abs);
|
|
217
|
+
if (!hl.passed) {
|
|
218
|
+
if (args.includes('--force')) {
|
|
219
|
+
console.warn(' ⚠ Human Lock Gate: OVERRIDDEN (--force). Proceeding with checks.');
|
|
220
|
+
console.warn(` ${hl.issues.length} unresolved Human Lock issue(s):`);
|
|
221
|
+
for (const issue of hl.issues) {
|
|
222
|
+
console.warn(` ${issue}`);
|
|
223
|
+
}
|
|
224
|
+
console.warn('');
|
|
225
|
+
} else {
|
|
226
|
+
console.error(' Human Lock Gate: BLOCKED');
|
|
227
|
+
console.error(` ${hl.issues.length} issue(s) found:`);
|
|
228
|
+
for (const issue of hl.issues) {
|
|
229
|
+
console.error(` ✗ ${issue}`);
|
|
230
|
+
}
|
|
231
|
+
console.error('');
|
|
232
|
+
console.error(' Judgment-class cards (axiom, boundary, risk, aesthetic)');
|
|
233
|
+
console.error(' must be locked with a valid Human Lock record before publishing.');
|
|
234
|
+
console.error(' Use kdna-studio or manually add human_lock to each card.');
|
|
235
|
+
console.error(' Use --force for emergency override (audited).');
|
|
236
|
+
console.error('');
|
|
237
|
+
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
console.log(' ✓ Human Lock Gate: passed');
|
|
241
|
+
console.log('');
|
|
242
|
+
}
|
|
243
|
+
|
|
155
244
|
let errors = 0;
|
|
156
245
|
let warnings = 0;
|
|
157
246
|
let passes = 0;
|
|
@@ -293,21 +382,22 @@ function cmdPublishCheck(domainPath) {
|
|
|
293
382
|
}
|
|
294
383
|
for (let i = 0; i < core.stances.length; i++) {
|
|
295
384
|
const s = core.stances[i];
|
|
296
|
-
|
|
385
|
+
const stanceText = typeof s === 'string' ? s : s && typeof s === 'object' ? s.stance : null;
|
|
386
|
+
if (!stanceText || typeof stanceText !== 'string') {
|
|
297
387
|
fail(
|
|
298
388
|
'KDNA_Core.json',
|
|
299
389
|
`stances[${i}]`,
|
|
300
390
|
JSON.stringify(s),
|
|
301
|
-
'Must be a string
|
|
391
|
+
'Must be a string or an object with a stance string.',
|
|
302
392
|
);
|
|
303
|
-
} else if (isSlogan(
|
|
393
|
+
} else if (isSlogan(stanceText)) {
|
|
304
394
|
fail(
|
|
305
395
|
'KDNA_Core.json',
|
|
306
396
|
`stances[${i}]`,
|
|
307
|
-
|
|
397
|
+
stanceText,
|
|
308
398
|
'Reads like a slogan. Stances must be prescriptive positions that bias agent behavior.',
|
|
309
399
|
);
|
|
310
|
-
} else if (isVague(
|
|
400
|
+
} else if (isVague(stanceText)) {
|
|
311
401
|
warn('KDNA_Core.json', `stances[${i}]`, 'Contains vague language.');
|
|
312
402
|
} else {
|
|
313
403
|
pass('KDNA_Core.json', `stances[${i}]`);
|
|
@@ -351,22 +441,23 @@ function cmdPublishCheck(domainPath) {
|
|
|
351
441
|
if (patterns.self_check && Array.isArray(patterns.self_check)) {
|
|
352
442
|
for (let i = 0; i < patterns.self_check.length; i++) {
|
|
353
443
|
const sc = patterns.self_check[i];
|
|
354
|
-
|
|
444
|
+
const text = selfCheckText(sc);
|
|
445
|
+
if (!text) {
|
|
355
446
|
fail(
|
|
356
447
|
'KDNA_Patterns.json',
|
|
357
448
|
`self_check[${i}]`,
|
|
358
449
|
JSON.stringify(sc),
|
|
359
|
-
'Must be a string
|
|
450
|
+
'Must be a string or an object with a question string.',
|
|
360
451
|
);
|
|
361
|
-
} else if (isGenericSelfCheck(
|
|
452
|
+
} else if (isGenericSelfCheck(text)) {
|
|
362
453
|
fail(
|
|
363
454
|
'KDNA_Patterns.json',
|
|
364
455
|
`self_check[${i}]`,
|
|
365
|
-
|
|
456
|
+
text,
|
|
366
457
|
'Generic question. Self-checks must be domain-specific, not "is this helpful?".',
|
|
367
458
|
);
|
|
368
|
-
} else if (!sc
|
|
369
|
-
warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should
|
|
459
|
+
} else if (!isYesNoSelfCheck(sc)) {
|
|
460
|
+
warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should be answerable with yes/no.');
|
|
370
461
|
passes++;
|
|
371
462
|
} else {
|
|
372
463
|
pass('KDNA_Patterns.json', `self_check[${i}]`);
|
|
@@ -440,9 +531,9 @@ function identityPaths() {
|
|
|
440
531
|
* inside the .kdna ZIP, joined as `name:hex\n`. This is what the signature covers.
|
|
441
532
|
*
|
|
442
533
|
* Excludes the `signature` field from kdna.json itself (computed by removing it
|
|
443
|
-
* before hashing). All other files included as-is.
|
|
534
|
+
* before hashing). Digest self-reference fields are also excluded. All other files included as-is.
|
|
444
535
|
*/
|
|
445
|
-
function canonicalPayload(srcDir) {
|
|
536
|
+
function canonicalPayload(srcDir, opts = {}) {
|
|
446
537
|
const files = fs
|
|
447
538
|
.readdirSync(srcDir)
|
|
448
539
|
.filter((f) => f.endsWith('.json'))
|
|
@@ -454,6 +545,10 @@ function canonicalPayload(srcDir) {
|
|
|
454
545
|
if (f === 'kdna.json') {
|
|
455
546
|
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
456
547
|
delete obj.signature;
|
|
548
|
+
delete obj.asset_digest;
|
|
549
|
+
delete obj.container_sha256;
|
|
550
|
+
if (!opts.includeContentDigest) delete obj.content_digest;
|
|
551
|
+
delete obj._source;
|
|
457
552
|
buf = Buffer.from(JSON.stringify(obj));
|
|
458
553
|
} else {
|
|
459
554
|
buf = fs.readFileSync(full);
|
|
@@ -464,6 +559,46 @@ function canonicalPayload(srcDir) {
|
|
|
464
559
|
return parts.join('\n');
|
|
465
560
|
}
|
|
466
561
|
|
|
562
|
+
function stableStringify(value) {
|
|
563
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
564
|
+
if (value && typeof value === 'object') {
|
|
565
|
+
return `{${Object.keys(value)
|
|
566
|
+
.sort()
|
|
567
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
568
|
+
.join(',')}}`;
|
|
569
|
+
}
|
|
570
|
+
return JSON.stringify(value);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function manifestForContentDigest(manifest) {
|
|
574
|
+
const copy = { ...(manifest || {}) };
|
|
575
|
+
delete copy.signature;
|
|
576
|
+
delete copy.asset_digest;
|
|
577
|
+
delete copy.container_sha256;
|
|
578
|
+
delete copy.content_digest;
|
|
579
|
+
delete copy._source;
|
|
580
|
+
return copy;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function sourceContentDigest(srcDir) {
|
|
584
|
+
const files = fs
|
|
585
|
+
.readdirSync(srcDir)
|
|
586
|
+
.filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE')
|
|
587
|
+
.sort();
|
|
588
|
+
const parts = [];
|
|
589
|
+
for (const f of files) {
|
|
590
|
+
let buf;
|
|
591
|
+
if (f === 'kdna.json') {
|
|
592
|
+
const obj = JSON.parse(fs.readFileSync(path.join(srcDir, f), 'utf8'));
|
|
593
|
+
buf = Buffer.from(stableStringify(manifestForContentDigest(obj)));
|
|
594
|
+
} else {
|
|
595
|
+
buf = fs.readFileSync(path.join(srcDir, f));
|
|
596
|
+
}
|
|
597
|
+
parts.push(`${f}:${crypto.createHash('sha256').update(buf).digest('hex')}`);
|
|
598
|
+
}
|
|
599
|
+
return `sha256:${crypto.createHash('sha256').update(Buffer.from(parts.join('\n'))).digest('hex')}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
467
602
|
function signPayload(payload, privateKeyPem) {
|
|
468
603
|
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
|
469
604
|
const sig = crypto.sign(null, Buffer.from(payload), privateKey);
|
|
@@ -491,7 +626,7 @@ function packToFile(domainDir, outPath) {
|
|
|
491
626
|
const files = fs
|
|
492
627
|
.readdirSync(domainDir)
|
|
493
628
|
.filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE');
|
|
494
|
-
if (!files.includes('kdna.json')) error('kdna.json required in
|
|
629
|
+
if (!files.includes('kdna.json')) error('kdna.json required in dev source directory for publish.');
|
|
495
630
|
|
|
496
631
|
const script = `import zipfile, os
|
|
497
632
|
src = ${JSON.stringify(domainDir)}
|
|
@@ -551,6 +686,26 @@ function cmdPublish(domainPath, args = []) {
|
|
|
551
686
|
console.log('═'.repeat(60));
|
|
552
687
|
console.log(` Publishing ${name}@${manifest.version}`);
|
|
553
688
|
console.log('═'.repeat(60));
|
|
689
|
+
|
|
690
|
+
// ─── Human Lock Gate ──────────────────────────────────────────────
|
|
691
|
+
const hl = checkHumanLock(abs);
|
|
692
|
+
if (!hl.passed) {
|
|
693
|
+
console.error('');
|
|
694
|
+
console.error(' Human Lock Gate: BLOCKED');
|
|
695
|
+
for (const issue of hl.issues) {
|
|
696
|
+
console.error(` ✗ ${issue}`);
|
|
697
|
+
}
|
|
698
|
+
console.error('');
|
|
699
|
+
console.error(' Use kdna publish --check for details, or --force to override.');
|
|
700
|
+
if (!args.includes('--force')) {
|
|
701
|
+
process.exit(EXIT.HUMAN_LOCK_REQUIRED);
|
|
702
|
+
}
|
|
703
|
+
console.warn(' ⚠ --force override: publishing without Human Lock (emergency only)');
|
|
704
|
+
} else {
|
|
705
|
+
console.log(` ✓ Human Lock Gate: passed`);
|
|
706
|
+
}
|
|
707
|
+
console.log('');
|
|
708
|
+
|
|
554
709
|
console.log(` Identity fingerprint: ${fingerprint(publicKey)}`);
|
|
555
710
|
console.log(` Scope trust key: ${scopeKey.slice(0, 28)}…`);
|
|
556
711
|
console.log('');
|
|
@@ -568,9 +723,14 @@ function cmdPublish(domainPath, args = []) {
|
|
|
568
723
|
|
|
569
724
|
// 2. Write signature
|
|
570
725
|
delete manifest.signature;
|
|
726
|
+
delete manifest.asset_digest;
|
|
727
|
+
delete manifest.container_sha256;
|
|
728
|
+
delete manifest.content_digest;
|
|
729
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
730
|
+
manifest.content_digest = sourceContentDigest(abs);
|
|
571
731
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
572
|
-
const
|
|
573
|
-
const sig = signPayload(
|
|
732
|
+
const signedPayload = canonicalPayload(abs, { includeContentDigest: true });
|
|
733
|
+
const sig = signPayload(signedPayload, privateKey);
|
|
574
734
|
manifest.signature = 'ed25519:' + sig;
|
|
575
735
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
576
736
|
console.log(
|
|
@@ -586,9 +746,10 @@ function cmdPublish(domainPath, args = []) {
|
|
|
586
746
|
const outPath = path.join(outDir, fileName);
|
|
587
747
|
packToFile(abs, outPath);
|
|
588
748
|
const sha256 = sha256File(outPath);
|
|
749
|
+
const assetDigest = `sha256:${sha256}`;
|
|
589
750
|
const size = fs.statSync(outPath).size;
|
|
590
751
|
console.log(` ✓ Packed: ${outPath} (${size} bytes)`);
|
|
591
|
-
console.log(` ✓
|
|
752
|
+
console.log(` ✓ asset_digest: ${assetDigest}`);
|
|
592
753
|
|
|
593
754
|
// 4. Optional upload via gh CLI
|
|
594
755
|
const tagIdx = args.indexOf('--release-tag');
|
|
@@ -615,8 +776,9 @@ function cmdPublish(domainPath, args = []) {
|
|
|
615
776
|
name,
|
|
616
777
|
type: manifest.cluster ? 'cluster' : 'domain',
|
|
617
778
|
version: manifest.version,
|
|
618
|
-
|
|
619
|
-
|
|
779
|
+
asset_url: kdnaUrl,
|
|
780
|
+
asset_digest: assetDigest,
|
|
781
|
+
content_digest: manifest.content_digest || null,
|
|
620
782
|
signature: manifest.signature,
|
|
621
783
|
release_status: kdnaUrl ? 'published_signed' : 'published_signed_local',
|
|
622
784
|
author: { ...manifest.author },
|
|
@@ -633,4 +795,4 @@ function cmdPublish(domainPath, args = []) {
|
|
|
633
795
|
);
|
|
634
796
|
}
|
|
635
797
|
|
|
636
|
-
module.exports = { cmdPublishCheck, cmdPublish, canonicalPayload, publicKeyToScopeFormat };
|
|
798
|
+
module.exports = { cmdPublishCheck, cmdPublish, checkHumanLock, canonicalPayload, publicKeyToScopeFormat };
|