@aikdna/kdna-cli 0.17.0 → 0.19.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 +120 -101
- package/SECURITY.md +1 -1
- package/package.json +6 -4
- package/skills/kdna-loader/SKILL.md +23 -22
- package/src/agent.js +290 -159
- package/src/cli.js +117 -67
- package/src/cmds/_common.js +40 -18
- package/src/cmds/badge.js +14 -9
- package/src/cmds/changelog.js +32 -12
- package/src/cmds/cluster.js +80 -85
- package/src/cmds/doctor.js +10 -27
- package/src/cmds/domain.js +114 -427
- package/src/cmds/explain.js +119 -0
- package/src/cmds/governance.js +111 -42
- package/src/cmds/legacy.js +8 -9
- package/src/cmds/license.js +491 -26
- package/src/cmds/quality.js +10 -3
- package/src/cmds/registry.js +15 -67
- package/src/cmds/studio.js +99 -47
- package/src/cmds/test.js +9 -6
- package/src/cmds/trace.js +11 -7
- package/src/compare.js +41 -22
- package/src/diff.js +38 -24
- package/src/identity.js +9 -7
- package/src/init.js +2 -2
- package/src/install.js +147 -459
- package/src/loader.js +10 -10
- package/src/package-store.js +232 -0
- package/src/paths.js +44 -0
- package/src/publish.js +150 -51
- package/src/registry.js +81 -9
- package/src/setup.js +19 -20
- package/src/verify.js +293 -140
- package/src/version.js +15 -7
- package/templates/minimal-domain/kdna.json +7 -7
- package/templates/standard-domain/README.md +10 -10
- package/templates/standard-domain/kdna.json +7 -3
- package/validators/kdna-lint.js +45 -11
- 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,232 @@
|
|
|
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('./') ||
|
|
175
|
+
input.startsWith('/') ||
|
|
176
|
+
input.startsWith('~/') ||
|
|
177
|
+
fs.existsSync(expanded));
|
|
178
|
+
if (looksLikeFile) {
|
|
179
|
+
const abs = path.resolve(expanded);
|
|
180
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null;
|
|
181
|
+
const manifest = readAssetManifest(abs);
|
|
182
|
+
const full = manifest.name;
|
|
183
|
+
const parsed = full ? parseName(full) : null;
|
|
184
|
+
return {
|
|
185
|
+
name: full || path.basename(abs, '.kdna'),
|
|
186
|
+
parsed,
|
|
187
|
+
asset_path: abs,
|
|
188
|
+
receipt_path: null,
|
|
189
|
+
version: manifest.version || null,
|
|
190
|
+
judgment_version: manifest.judgment_version || null,
|
|
191
|
+
access: manifest.access || 'open',
|
|
192
|
+
asset_digest: assetDigest(abs),
|
|
193
|
+
content_digest: contentDigest(abs),
|
|
194
|
+
source: { type: 'local-file', path: abs },
|
|
195
|
+
local_file: true,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return getInstalled(input);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function removeInstalled(input) {
|
|
202
|
+
const parsed = parseName(input);
|
|
203
|
+
if (!parsed) return false;
|
|
204
|
+
const index = readIndex();
|
|
205
|
+
const entry = index.packages[parsed.full];
|
|
206
|
+
if (!entry) return false;
|
|
207
|
+
delete index.packages[parsed.full];
|
|
208
|
+
writeIndex(index);
|
|
209
|
+
if (entry.asset_path) {
|
|
210
|
+
const versionDir = path.dirname(entry.asset_path);
|
|
211
|
+
fs.rmSync(versionDir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
readIndex,
|
|
218
|
+
writeIndex,
|
|
219
|
+
sha256File,
|
|
220
|
+
assetDigest,
|
|
221
|
+
contentDigest,
|
|
222
|
+
readContainer,
|
|
223
|
+
readContainerEntry,
|
|
224
|
+
readContainerJson,
|
|
225
|
+
listContainerEntries,
|
|
226
|
+
verifyAsset,
|
|
227
|
+
getInstalled,
|
|
228
|
+
listInstalled,
|
|
229
|
+
installAsset,
|
|
230
|
+
resolveAsset,
|
|
231
|
+
removeInstalled,
|
|
232
|
+
};
|
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 =
|
|
7
|
+
process.env.KDNA_HOME || 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}`);
|
|
@@ -53,17 +53,17 @@ const SLOGAN_PATTERNS = [
|
|
|
53
53
|
// PRIORITIZE or AVOID, not steps to follow.
|
|
54
54
|
|
|
55
55
|
const SOP_PATTERNS = [
|
|
56
|
-
/^Step\s+\d/i,
|
|
56
|
+
/^Step\s+\d/i, // "Step 1: identify the topic"
|
|
57
57
|
/^First,?\s|^Next,?\s|^Then,?\s|^Finally,?\s/i, // "First, do X. Then do Y."
|
|
58
|
-
/^Check\s(for|if|whether)\s/i,
|
|
58
|
+
/^Check\s(for|if|whether)\s/i, // "Check for spelling errors"
|
|
59
59
|
/^Always\s+(use|do|make|include)/i, // "Always use active voice"
|
|
60
|
-
/^Never\s+(use|do|make)/i,
|
|
61
|
-
/^Generate\s/i,
|
|
62
|
-
/^Create\s+(a|the)\s/i,
|
|
63
|
-
/^Make\s+sure\s/i,
|
|
64
|
-
/^Remember\s+to\s/i,
|
|
60
|
+
/^Never\s+(use|do|make)/i, // "Never use passive voice"
|
|
61
|
+
/^Generate\s/i, // "Generate three options"
|
|
62
|
+
/^Create\s+(a|the)\s/i, // "Create a list of..."
|
|
63
|
+
/^Make\s+sure\s/i, // "Make sure to check..."
|
|
64
|
+
/^Remember\s+to\s/i, // "Remember to validate..."
|
|
65
65
|
/^(You|The agent)\s+should\s+(use|do|make|include)/i, // "You should use X"
|
|
66
|
-
/^Avoid\s+(using|doing)/i,
|
|
66
|
+
/^Avoid\s+(using|doing)/i, // "Avoid using X" (too procedural)
|
|
67
67
|
];
|
|
68
68
|
|
|
69
69
|
function isSOP(text) {
|
|
@@ -143,8 +143,6 @@ function isGenericSelfCheck(question) {
|
|
|
143
143
|
|
|
144
144
|
// ─── Human Lock Gate ──────────────────────────────────────────────────
|
|
145
145
|
|
|
146
|
-
const JUDGMENT_CARD_TYPES = ['axiom', 'boundary', 'risk', 'aesthetic'];
|
|
147
|
-
|
|
148
146
|
/**
|
|
149
147
|
* Check whether the domain satisfies Human Lock requirements.
|
|
150
148
|
* Returns { passed, issues[] } — publish should be blocked if !passed.
|
|
@@ -190,13 +188,19 @@ function checkHumanLock(domainPath) {
|
|
|
190
188
|
// Rule 3: Lock must confirm judgment fields were reviewed
|
|
191
189
|
const checked = card.human_lock.checked || {};
|
|
192
190
|
if (!checked.applies_when) {
|
|
193
|
-
issues.push(
|
|
191
|
+
issues.push(
|
|
192
|
+
`${card.type} "${card.id}" Human Lock does not confirm applies_when was reviewed.`,
|
|
193
|
+
);
|
|
194
194
|
}
|
|
195
195
|
if (!checked.does_not_apply_when) {
|
|
196
|
-
issues.push(
|
|
196
|
+
issues.push(
|
|
197
|
+
`${card.type} "${card.id}" Human Lock does not confirm does_not_apply_when was reviewed.`,
|
|
198
|
+
);
|
|
197
199
|
}
|
|
198
200
|
if (!checked.failure_risk) {
|
|
199
|
-
issues.push(
|
|
201
|
+
issues.push(
|
|
202
|
+
`${card.type} "${card.id}" Human Lock does not confirm failure_risk was reviewed.`,
|
|
203
|
+
);
|
|
200
204
|
}
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -384,21 +388,22 @@ function cmdPublishCheck(domainPath, args = []) {
|
|
|
384
388
|
}
|
|
385
389
|
for (let i = 0; i < core.stances.length; i++) {
|
|
386
390
|
const s = core.stances[i];
|
|
387
|
-
|
|
391
|
+
const stanceText = typeof s === 'string' ? s : s && typeof s === 'object' ? s.stance : null;
|
|
392
|
+
if (!stanceText || typeof stanceText !== 'string') {
|
|
388
393
|
fail(
|
|
389
394
|
'KDNA_Core.json',
|
|
390
395
|
`stances[${i}]`,
|
|
391
396
|
JSON.stringify(s),
|
|
392
|
-
'Must be a string
|
|
397
|
+
'Must be a string or an object with a stance string.',
|
|
393
398
|
);
|
|
394
|
-
} else if (isSlogan(
|
|
399
|
+
} else if (isSlogan(stanceText)) {
|
|
395
400
|
fail(
|
|
396
401
|
'KDNA_Core.json',
|
|
397
402
|
`stances[${i}]`,
|
|
398
|
-
|
|
403
|
+
stanceText,
|
|
399
404
|
'Reads like a slogan. Stances must be prescriptive positions that bias agent behavior.',
|
|
400
405
|
);
|
|
401
|
-
} else if (isVague(
|
|
406
|
+
} else if (isVague(stanceText)) {
|
|
402
407
|
warn('KDNA_Core.json', `stances[${i}]`, 'Contains vague language.');
|
|
403
408
|
} else {
|
|
404
409
|
pass('KDNA_Core.json', `stances[${i}]`);
|
|
@@ -442,22 +447,23 @@ function cmdPublishCheck(domainPath, args = []) {
|
|
|
442
447
|
if (patterns.self_check && Array.isArray(patterns.self_check)) {
|
|
443
448
|
for (let i = 0; i < patterns.self_check.length; i++) {
|
|
444
449
|
const sc = patterns.self_check[i];
|
|
445
|
-
|
|
450
|
+
const text = selfCheckText(sc);
|
|
451
|
+
if (!text) {
|
|
446
452
|
fail(
|
|
447
453
|
'KDNA_Patterns.json',
|
|
448
454
|
`self_check[${i}]`,
|
|
449
455
|
JSON.stringify(sc),
|
|
450
|
-
'Must be a string
|
|
456
|
+
'Must be a string or an object with a question string.',
|
|
451
457
|
);
|
|
452
|
-
} else if (isGenericSelfCheck(
|
|
458
|
+
} else if (isGenericSelfCheck(text)) {
|
|
453
459
|
fail(
|
|
454
460
|
'KDNA_Patterns.json',
|
|
455
461
|
`self_check[${i}]`,
|
|
456
|
-
|
|
462
|
+
text,
|
|
457
463
|
'Generic question. Self-checks must be domain-specific, not "is this helpful?".',
|
|
458
464
|
);
|
|
459
|
-
} else if (!sc
|
|
460
|
-
warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should
|
|
465
|
+
} else if (!isYesNoSelfCheck(sc)) {
|
|
466
|
+
warn('KDNA_Patterns.json', `self_check[${i}]`, 'Should be answerable with yes/no.');
|
|
461
467
|
passes++;
|
|
462
468
|
} else {
|
|
463
469
|
pass('KDNA_Patterns.json', `self_check[${i}]`);
|
|
@@ -527,25 +533,24 @@ function identityPaths() {
|
|
|
527
533
|
}
|
|
528
534
|
|
|
529
535
|
/**
|
|
530
|
-
* Canonical signing payload: sorted (filename, sha256) pairs of all
|
|
531
|
-
* inside the .kdna ZIP, joined as `name:hex\n`.
|
|
536
|
+
* Canonical signing payload: sorted (filename, sha256) pairs of all published
|
|
537
|
+
* content entries inside the .kdna ZIP, joined as `name:hex\n`.
|
|
532
538
|
*
|
|
533
539
|
* Excludes the `signature` field from kdna.json itself (computed by removing it
|
|
534
|
-
* before hashing). All other files included as-is.
|
|
540
|
+
* before hashing). Digest self-reference fields are also excluded. All other files included as-is.
|
|
535
541
|
*/
|
|
536
|
-
function canonicalPayload(srcDir) {
|
|
537
|
-
const files =
|
|
538
|
-
.readdirSync(srcDir)
|
|
539
|
-
.filter((f) => f.endsWith('.json'))
|
|
540
|
-
.sort();
|
|
542
|
+
function canonicalPayload(srcDir, opts = {}) {
|
|
543
|
+
const files = listPublishEntries(srcDir);
|
|
541
544
|
const parts = [];
|
|
542
545
|
for (const f of files) {
|
|
543
|
-
const full = path.join(srcDir, f);
|
|
546
|
+
const full = f === 'mimetype' ? null : path.join(srcDir, f);
|
|
544
547
|
let buf;
|
|
545
|
-
if (f === '
|
|
548
|
+
if (f === 'mimetype') {
|
|
549
|
+
buf = Buffer.from('application/vnd.aikdna.kdna+zip');
|
|
550
|
+
} else if (f.endsWith('.json')) {
|
|
546
551
|
const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
547
|
-
|
|
548
|
-
buf = Buffer.from(
|
|
552
|
+
const value = f === 'kdna.json' ? manifestForSigning(obj, opts) : obj;
|
|
553
|
+
buf = Buffer.from(stableStringify(value));
|
|
549
554
|
} else {
|
|
550
555
|
buf = fs.readFileSync(full);
|
|
551
556
|
}
|
|
@@ -555,6 +560,89 @@ function canonicalPayload(srcDir) {
|
|
|
555
560
|
return parts.join('\n');
|
|
556
561
|
}
|
|
557
562
|
|
|
563
|
+
function manifestForSigning(manifest, opts = {}) {
|
|
564
|
+
const copy = { ...(manifest || {}) };
|
|
565
|
+
delete copy.signature;
|
|
566
|
+
delete copy.asset_digest;
|
|
567
|
+
delete copy.container_sha256;
|
|
568
|
+
if (!opts.includeContentDigest) delete copy.content_digest;
|
|
569
|
+
delete copy._source;
|
|
570
|
+
return copy;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function stableStringify(value) {
|
|
574
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
575
|
+
if (value && typeof value === 'object') {
|
|
576
|
+
return `{${Object.keys(value)
|
|
577
|
+
.sort()
|
|
578
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
579
|
+
.join(',')}}`;
|
|
580
|
+
}
|
|
581
|
+
return JSON.stringify(value);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function manifestForContentDigest(manifest) {
|
|
585
|
+
const copy = { ...(manifest || {}) };
|
|
586
|
+
delete copy.signature;
|
|
587
|
+
delete copy.asset_digest;
|
|
588
|
+
delete copy.container_sha256;
|
|
589
|
+
delete copy.content_digest;
|
|
590
|
+
delete copy._source;
|
|
591
|
+
return copy;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function sourceContentDigest(srcDir) {
|
|
595
|
+
const files = listPublishEntries(srcDir);
|
|
596
|
+
const parts = [];
|
|
597
|
+
for (const f of files) {
|
|
598
|
+
let buf;
|
|
599
|
+
if (f === 'mimetype') {
|
|
600
|
+
buf = Buffer.from('application/vnd.aikdna.kdna+zip');
|
|
601
|
+
} else if (f.endsWith('.json')) {
|
|
602
|
+
const obj = JSON.parse(fs.readFileSync(path.join(srcDir, f), 'utf8'));
|
|
603
|
+
const value = f === 'kdna.json' ? manifestForContentDigest(obj) : obj;
|
|
604
|
+
buf = Buffer.from(stableStringify(value));
|
|
605
|
+
} else {
|
|
606
|
+
buf = fs.readFileSync(path.join(srcDir, f));
|
|
607
|
+
}
|
|
608
|
+
parts.push(`${f}:${crypto.createHash('sha256').update(buf).digest('hex')}`);
|
|
609
|
+
}
|
|
610
|
+
return `sha256:${crypto
|
|
611
|
+
.createHash('sha256')
|
|
612
|
+
.update(Buffer.from(parts.join('\n')))
|
|
613
|
+
.digest('hex')}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function listPublishEntries(domainDir) {
|
|
617
|
+
const entries = ['mimetype'];
|
|
618
|
+
const skipDirs = new Set(['.git', 'node_modules', 'dist']);
|
|
619
|
+
function walk(dir, prefix = '') {
|
|
620
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
621
|
+
if (name === 'mimetype') continue;
|
|
622
|
+
if (name === '.DS_Store' || name === 'signature.json') continue;
|
|
623
|
+
const abs = path.join(dir, name);
|
|
624
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
625
|
+
const stat = fs.statSync(abs);
|
|
626
|
+
if (stat.isDirectory()) {
|
|
627
|
+
if (!skipDirs.has(name)) walk(abs, rel);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (
|
|
631
|
+
rel.endsWith('.json') ||
|
|
632
|
+
rel === 'README.md' ||
|
|
633
|
+
rel === 'LICENSE' ||
|
|
634
|
+
rel.startsWith('evals/') ||
|
|
635
|
+
rel.startsWith('examples/') ||
|
|
636
|
+
rel.startsWith('reports/')
|
|
637
|
+
) {
|
|
638
|
+
entries.push(rel);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
walk(domainDir);
|
|
643
|
+
return entries;
|
|
644
|
+
}
|
|
645
|
+
|
|
558
646
|
function signPayload(payload, privateKeyPem) {
|
|
559
647
|
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
|
560
648
|
const sig = crypto.sign(null, Buffer.from(payload), privateKey);
|
|
@@ -579,17 +667,17 @@ function publicKeyToScopeFormat(publicKeyPem) {
|
|
|
579
667
|
}
|
|
580
668
|
|
|
581
669
|
function packToFile(domainDir, outPath) {
|
|
582
|
-
const files =
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (!files.includes('kdna.json')) error('kdna.json required in domain folder for publish.');
|
|
670
|
+
const files = listPublishEntries(domainDir).filter((f) => f !== 'mimetype');
|
|
671
|
+
if (!files.includes('kdna.json'))
|
|
672
|
+
error('kdna.json required in dev source directory for publish.');
|
|
586
673
|
|
|
587
674
|
const script = `import zipfile, os
|
|
588
675
|
src = ${JSON.stringify(domainDir)}
|
|
589
676
|
out = ${JSON.stringify(outPath)}
|
|
590
677
|
files = ${JSON.stringify(files)}
|
|
591
678
|
with zipfile.ZipFile(out, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
592
|
-
|
|
679
|
+
zf.writestr(zipfile.ZipInfo('mimetype'), 'application/vnd.aikdna.kdna+zip', compress_type=zipfile.ZIP_STORED)
|
|
680
|
+
for f in files:
|
|
593
681
|
zf.write(os.path.join(src, f), f)
|
|
594
682
|
`;
|
|
595
683
|
const tmpPy = `/tmp/kdna-publish-pack-${Date.now()}.py`;
|
|
@@ -679,14 +767,17 @@ function cmdPublish(domainPath, args = []) {
|
|
|
679
767
|
|
|
680
768
|
// 2. Write signature
|
|
681
769
|
delete manifest.signature;
|
|
770
|
+
delete manifest.asset_digest;
|
|
771
|
+
delete manifest.container_sha256;
|
|
772
|
+
delete manifest.content_digest;
|
|
682
773
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
683
|
-
|
|
684
|
-
|
|
774
|
+
manifest.content_digest = sourceContentDigest(abs);
|
|
775
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
776
|
+
const signedPayload = canonicalPayload(abs);
|
|
777
|
+
const sig = signPayload(signedPayload, privateKey);
|
|
685
778
|
manifest.signature = 'ed25519:' + sig;
|
|
686
779
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
687
|
-
console.log(
|
|
688
|
-
` ✓ Signed (payload covers ${fs.readdirSync(abs).filter((f) => f.endsWith('.json')).length} json files)`,
|
|
689
|
-
);
|
|
780
|
+
console.log(` ✓ Signed (payload covers ${listPublishEntries(abs).length} content entries)`);
|
|
690
781
|
|
|
691
782
|
// 3. Pack
|
|
692
783
|
const fileName = `${m[2]}-${manifest.version}.kdna`;
|
|
@@ -697,9 +788,10 @@ function cmdPublish(domainPath, args = []) {
|
|
|
697
788
|
const outPath = path.join(outDir, fileName);
|
|
698
789
|
packToFile(abs, outPath);
|
|
699
790
|
const sha256 = sha256File(outPath);
|
|
791
|
+
const assetDigest = `sha256:${sha256}`;
|
|
700
792
|
const size = fs.statSync(outPath).size;
|
|
701
793
|
console.log(` ✓ Packed: ${outPath} (${size} bytes)`);
|
|
702
|
-
console.log(` ✓
|
|
794
|
+
console.log(` ✓ asset_digest: ${assetDigest}`);
|
|
703
795
|
|
|
704
796
|
// 4. Optional upload via gh CLI
|
|
705
797
|
const tagIdx = args.indexOf('--release-tag');
|
|
@@ -726,8 +818,9 @@ function cmdPublish(domainPath, args = []) {
|
|
|
726
818
|
name,
|
|
727
819
|
type: manifest.cluster ? 'cluster' : 'domain',
|
|
728
820
|
version: manifest.version,
|
|
729
|
-
|
|
730
|
-
|
|
821
|
+
asset_url: kdnaUrl,
|
|
822
|
+
asset_digest: assetDigest,
|
|
823
|
+
content_digest: manifest.content_digest || null,
|
|
731
824
|
signature: manifest.signature,
|
|
732
825
|
release_status: kdnaUrl ? 'published_signed' : 'published_signed_local',
|
|
733
826
|
author: { ...manifest.author },
|
|
@@ -744,4 +837,10 @@ function cmdPublish(domainPath, args = []) {
|
|
|
744
837
|
);
|
|
745
838
|
}
|
|
746
839
|
|
|
747
|
-
module.exports = {
|
|
840
|
+
module.exports = {
|
|
841
|
+
cmdPublishCheck,
|
|
842
|
+
cmdPublish,
|
|
843
|
+
checkHumanLock,
|
|
844
|
+
canonicalPayload,
|
|
845
|
+
publicKeyToScopeFormat,
|
|
846
|
+
};
|