@aikdna/kdna-cli 0.9.0 → 0.11.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 +109 -31
- package/package.json +1 -1
- package/src/agent.js +410 -2
- package/src/cli.js +267 -38
- package/src/cmds/_common.js +65 -3
- package/src/cmds/badge.js +244 -0
- package/src/cmds/changelog.js +226 -0
- package/src/cmds/cluster.js +214 -3
- package/src/cmds/doctor.js +160 -0
- package/src/cmds/domain.js +110 -33
- package/src/cmds/governance.js +471 -0
- package/src/cmds/identity.js +5 -4
- package/src/cmds/quality.js +34 -9
- package/src/cmds/registry.js +62 -22
- package/src/cmds/studio.js +577 -0
- package/src/cmds/test.js +177 -0
- package/src/compare.js +46 -32
- package/src/diff.js +136 -38
- package/src/identity.js +20 -4
- package/src/install.js +181 -91
- package/src/publish.js +4 -3
- package/src/search.js +33 -2
- package/src/verify.js +76 -13
- package/src/version.js +110 -3
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KDNA Quality Badge & Registry commands — Phase 7.
|
|
3
|
+
*
|
|
4
|
+
* kdna badge compute <domain> [--json]
|
|
5
|
+
* kdna registry audit --scope <scope> [--json]
|
|
6
|
+
* kdna package <domain> --format=kdna
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { error, readJson, EXIT } = require('./_common');
|
|
12
|
+
const { RegistryResolver } = require('../registry');
|
|
13
|
+
|
|
14
|
+
// ─── Badge Compute ─────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function cmdBadgeCompute(domainPath, args = []) {
|
|
17
|
+
const jsonMode = args.includes('--json');
|
|
18
|
+
const abs = path.resolve(domainPath);
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(abs)) error(`Domain not found: ${abs}`, EXIT.INPUT_ERROR);
|
|
21
|
+
|
|
22
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
23
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
24
|
+
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
25
|
+
|
|
26
|
+
if (!core) error(`No KDNA_Core.json in ${abs}`, EXIT.INPUT_ERROR);
|
|
27
|
+
|
|
28
|
+
const axiomCount = (core.axioms || []).length;
|
|
29
|
+
const lockedAxioms = (core.axioms || []).filter(
|
|
30
|
+
(a) => a.applies_when?.length && a.does_not_apply_when?.length && a.failure_risk,
|
|
31
|
+
).length;
|
|
32
|
+
const misCount = (pat?.misunderstandings || []).length;
|
|
33
|
+
const selfCheckCount = (pat?.self_check || []).length;
|
|
34
|
+
|
|
35
|
+
// Count eval cases
|
|
36
|
+
const evalsDir = path.join(abs, 'evals');
|
|
37
|
+
let evalCount = 0;
|
|
38
|
+
let humanPassCount = 0;
|
|
39
|
+
if (fs.existsSync(evalsDir)) {
|
|
40
|
+
for (const f of fs.readdirSync(evalsDir)) {
|
|
41
|
+
if (!f.endsWith('.json')) continue;
|
|
42
|
+
const evalData = readJson(path.join(evalsDir, f));
|
|
43
|
+
if (evalData?.cases) {
|
|
44
|
+
evalCount += evalData.cases.length;
|
|
45
|
+
humanPassCount += evalData.cases.filter((c) => c.human_pass === true).length;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const humanPassRate = evalCount > 0 ? humanPassCount / evalCount : 0;
|
|
50
|
+
|
|
51
|
+
// Regression check
|
|
52
|
+
const regressionPassed = fs.existsSync(path.join(abs, 'evals')); // simplified
|
|
53
|
+
|
|
54
|
+
// Determine badge level
|
|
55
|
+
let badge = 'draft';
|
|
56
|
+
const criteria = [];
|
|
57
|
+
|
|
58
|
+
if (axiomCount >= 3 && lockedAxioms >= 3 && misCount >= 2 && selfCheckCount >= 3) {
|
|
59
|
+
badge = 'declared';
|
|
60
|
+
criteria.push('minimum content: 3 axioms, 2 misunderstandings, 3 self-checks');
|
|
61
|
+
}
|
|
62
|
+
if (lockedAxioms >= axiomCount && evalCount >= 5 && humanPassRate >= 0.6) {
|
|
63
|
+
badge = 'tested';
|
|
64
|
+
criteria.push(`${lockedAxioms}/${axiomCount} axioms governed`);
|
|
65
|
+
criteria.push(`${evalCount} eval cases`);
|
|
66
|
+
criteria.push(`human pass rate: ${Math.round(humanPassRate * 100)}%`);
|
|
67
|
+
}
|
|
68
|
+
if (lockedAxioms >= axiomCount && evalCount >= 10 && humanPassRate >= 0.8 && regressionPassed) {
|
|
69
|
+
badge = 'trusted';
|
|
70
|
+
criteria.push(`${evalCount} eval cases (≥10)`);
|
|
71
|
+
criteria.push(`human pass rate: ${Math.round(humanPassRate * 100)}% (≥80%)`);
|
|
72
|
+
criteria.push('regression test passed');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = {
|
|
76
|
+
quality_badge: badge,
|
|
77
|
+
evidence: {
|
|
78
|
+
axioms: { total: axiomCount, locked: lockedAxioms },
|
|
79
|
+
misunderstandings: misCount,
|
|
80
|
+
self_checks: selfCheckCount,
|
|
81
|
+
eval_count: evalCount,
|
|
82
|
+
human_pass_rate: Math.round(humanPassRate * 100) / 100,
|
|
83
|
+
regression_passed: regressionPassed,
|
|
84
|
+
},
|
|
85
|
+
criteria,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (jsonMode) {
|
|
89
|
+
console.log(JSON.stringify(result, null, 2));
|
|
90
|
+
process.exit(badge === 'draft' ? EXIT.JUDGMENT_QUALITY_FAILED : EXIT.OK);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Quality Badge: ${badge.toUpperCase()}`);
|
|
94
|
+
console.log(` Domain: ${manifest?.name || path.basename(abs)}`);
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(' Evidence:');
|
|
97
|
+
console.log(` Axioms: ${axiomCount} total, ${lockedAxioms} governed`);
|
|
98
|
+
console.log(` Misunderstandings: ${misCount}`);
|
|
99
|
+
console.log(` Self-checks: ${selfCheckCount}`);
|
|
100
|
+
console.log(` Eval cases: ${evalCount}`);
|
|
101
|
+
console.log(` Human pass rate: ${Math.round(humanPassRate * 100)}%`);
|
|
102
|
+
console.log(` Regression: ${regressionPassed ? '✓ passed' : '— not run'}`);
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(' Criteria met:');
|
|
105
|
+
criteria.forEach((c) => console.log(` ✓ ${c}`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Registry Audit ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function cmdRegistryAudit(args = []) {
|
|
111
|
+
const jsonMode = args.includes('--json');
|
|
112
|
+
const scopeIdx = args.indexOf('--scope');
|
|
113
|
+
const scope = scopeIdx >= 0 ? args[scopeIdx + 1] : null;
|
|
114
|
+
|
|
115
|
+
if (!scope) {
|
|
116
|
+
error('Usage: kdna registry audit --scope <@scope> [--json]', EXIT.INPUT_ERROR);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
120
|
+
const allDomains = resolver.listAllDomains() || [];
|
|
121
|
+
const scopeDomains = allDomains.filter((d) => {
|
|
122
|
+
const name = d.name || d.id || '';
|
|
123
|
+
return name.startsWith(scope);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!scopeDomains.length) {
|
|
127
|
+
if (jsonMode) {
|
|
128
|
+
console.log(JSON.stringify({ scope, domains: [], note: 'No domains found in this scope' }));
|
|
129
|
+
process.exit(EXIT.OK);
|
|
130
|
+
}
|
|
131
|
+
console.log(`No domains found in scope: ${scope}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const audit = {
|
|
136
|
+
scope,
|
|
137
|
+
total: scopeDomains.length,
|
|
138
|
+
domains: scopeDomains.map((d) => ({
|
|
139
|
+
name: d.name || d.id,
|
|
140
|
+
version: d.version || null,
|
|
141
|
+
type: d.type || 'domain',
|
|
142
|
+
status: d.status || 'experimental',
|
|
143
|
+
yanked: d.yanked || false,
|
|
144
|
+
deprecated: d.deprecated || false,
|
|
145
|
+
has_kdna_url: !!d.kdna_url,
|
|
146
|
+
has_signature: !!d.signature,
|
|
147
|
+
has_sha256: !!d.sha256,
|
|
148
|
+
})),
|
|
149
|
+
issues: [],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Detect issues
|
|
153
|
+
const yanked = scopeDomains.filter((d) => d.yanked);
|
|
154
|
+
const deprecated = scopeDomains.filter((d) => d.deprecated);
|
|
155
|
+
const noPackage = scopeDomains.filter((d) => !d.kdna_url);
|
|
156
|
+
const noSignature = scopeDomains.filter((d) => !d.signature);
|
|
157
|
+
|
|
158
|
+
if (yanked.length) audit.issues.push(`${yanked.length} yanked domain(s)`);
|
|
159
|
+
if (deprecated.length) audit.issues.push(`${deprecated.length} deprecated domain(s)`);
|
|
160
|
+
if (noPackage.length) audit.issues.push(`${noPackage.length} domain(s) without .kdna package`);
|
|
161
|
+
if (noSignature.length) audit.issues.push(`${noSignature.length} domain(s) without signature`);
|
|
162
|
+
|
|
163
|
+
audit.healthy = audit.issues.length === 0;
|
|
164
|
+
|
|
165
|
+
if (jsonMode) {
|
|
166
|
+
console.log(JSON.stringify(audit, null, 2));
|
|
167
|
+
process.exit(audit.healthy ? EXIT.OK : EXIT.VALIDATION_FAILED);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`Registry audit: ${scope}`);
|
|
171
|
+
console.log(` Total domains: ${audit.total}`);
|
|
172
|
+
console.log(` Healthy: ${audit.healthy ? '✓ yes' : '✗ no'}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
|
|
175
|
+
if (audit.issues.length) {
|
|
176
|
+
console.log(' Issues:');
|
|
177
|
+
audit.issues.forEach((i) => console.log(` ⚠ ${i}`));
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(' Domains:');
|
|
182
|
+
for (const d of audit.domains) {
|
|
183
|
+
const flags = [];
|
|
184
|
+
if (d.yanked) flags.push('yanked');
|
|
185
|
+
if (d.deprecated) flags.push('deprecated');
|
|
186
|
+
if (!d.has_kdna_url) flags.push('no-package');
|
|
187
|
+
console.log(` ${d.name.padEnd(36)} v${d.version || '?'} ${flags.length ? `[${flags.join(', ')}]` : '✓'}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Package ────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function cmdPackage(domainPath, args = []) {
|
|
194
|
+
const abs = path.resolve(domainPath);
|
|
195
|
+
|
|
196
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
|
197
|
+
error(`Not a directory: ${abs}`, EXIT.INPUT_ERROR);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const formatIdx = args.indexOf('--format');
|
|
201
|
+
const format = formatIdx >= 0 ? args[formatIdx + 1] : 'kdna';
|
|
202
|
+
|
|
203
|
+
if (!['kdna'].includes(format)) {
|
|
204
|
+
error(`Unsupported format: ${format}. Use --format=kdna`, EXIT.INPUT_ERROR);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const manifest = readJson(path.join(abs, 'kdna.json'));
|
|
208
|
+
if (!manifest) error(`No kdna.json found in ${abs}. Run: kdna pack`, EXIT.INPUT_ERROR);
|
|
209
|
+
|
|
210
|
+
const domainName = manifest.name?.split('/')?.[1] || path.basename(abs);
|
|
211
|
+
const outFile = path.join(abs, 'dist', `${domainName}-${manifest.version || '0.1.0'}.kdna`);
|
|
212
|
+
|
|
213
|
+
// Reuse pack logic from domain.js
|
|
214
|
+
const { cmdPack } = require('./domain');
|
|
215
|
+
const outDir = path.join(abs, 'dist');
|
|
216
|
+
|
|
217
|
+
// Build package summary
|
|
218
|
+
const core = readJson(path.join(abs, 'KDNA_Core.json'));
|
|
219
|
+
const pat = readJson(path.join(abs, 'KDNA_Patterns.json'));
|
|
220
|
+
|
|
221
|
+
const pkg = {
|
|
222
|
+
name: manifest.name || domainName,
|
|
223
|
+
version: manifest.version || '0.1.0',
|
|
224
|
+
format,
|
|
225
|
+
assets: {
|
|
226
|
+
axioms: (core?.axioms || []).length,
|
|
227
|
+
ontology: (core?.ontology || []).length,
|
|
228
|
+
misunderstandings: (pat?.misunderstandings || []).length,
|
|
229
|
+
self_checks: (pat?.self_check || []).length,
|
|
230
|
+
scenarios: readJson(path.join(abs, 'KDNA_Scenarios.json'))?.scenes?.length || 0,
|
|
231
|
+
cases: readJson(path.join(abs, 'KDNA_Cases.json'))?.cases?.length || 0,
|
|
232
|
+
},
|
|
233
|
+
files: fs.readdirSync(abs).filter((f) => f.endsWith('.json') || f === 'README.md' || f === 'LICENSE'),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Actually pack
|
|
237
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
238
|
+
cmdPack(abs, outDir);
|
|
239
|
+
|
|
240
|
+
console.log(JSON.stringify(pkg, null, 2));
|
|
241
|
+
console.log(`\nPackage built: ${outFile}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { cmdBadgeCompute, cmdRegistryAudit, cmdPackage };
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kdna changelog <domain> --from <v1> --to <v2>
|
|
3
|
+
* Generate a judgment changelog between two versions.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the diff engine from src/diff.js to compute changes,
|
|
6
|
+
* then renders a human-readable markdown changelog.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { error, EXIT } = require('./_common');
|
|
12
|
+
const { loadJudgment } = require('../diff');
|
|
13
|
+
const { RegistryResolver } = require('../registry');
|
|
14
|
+
|
|
15
|
+
const TMP_DIR = '/tmp';
|
|
16
|
+
|
|
17
|
+
function downloadVersion(entry, version, destDir) {
|
|
18
|
+
const { execSync, execFileSync } = require('child_process');
|
|
19
|
+
const tmpFile = `${destDir}.kdna.tmp`;
|
|
20
|
+
try {
|
|
21
|
+
execFileSync('curl', ['-fsSL', '--retry', '2', '-o', tmpFile, entry.kdna_url], {
|
|
22
|
+
timeout: 60000,
|
|
23
|
+
stdio: 'pipe',
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const stderr = e.stderr?.toString().trim() || e.message;
|
|
27
|
+
error(`Failed to download: ${stderr}`, EXIT.PROVIDER_ERROR);
|
|
28
|
+
}
|
|
29
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
30
|
+
try {
|
|
31
|
+
execSync(`unzip -q -o "${tmpFile}" -d "${destDir}"`, { stdio: 'pipe' });
|
|
32
|
+
} catch {
|
|
33
|
+
const script = `import zipfile\nzf = zipfile.ZipFile(${JSON.stringify(tmpFile)}, 'r')\nzf.extractall(${JSON.stringify(destDir)})`;
|
|
34
|
+
execSync(`python3 -c ${JSON.stringify(script)}`, { stdio: 'pipe' });
|
|
35
|
+
}
|
|
36
|
+
fs.unlinkSync(tmpFile);
|
|
37
|
+
return destDir;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cmdChangelog(args = []) {
|
|
41
|
+
const jsonMode = args.includes('--json');
|
|
42
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
43
|
+
const domainInput = positional[1];
|
|
44
|
+
const fromIdx = args.indexOf('--from');
|
|
45
|
+
const toIdx = args.indexOf('--to');
|
|
46
|
+
const fromVersion = fromIdx >= 0 ? args[fromIdx + 1] : null;
|
|
47
|
+
const toVersion = toIdx >= 0 ? args[toIdx + 1] : null;
|
|
48
|
+
|
|
49
|
+
if (!domainInput || !fromVersion || !toVersion) {
|
|
50
|
+
error(
|
|
51
|
+
'Usage:\n' +
|
|
52
|
+
' kdna changelog <domain> --from <version> --to <version> [--json]\n' +
|
|
53
|
+
'\n' +
|
|
54
|
+
'Generates a judgment changelog between two domain versions.\n' +
|
|
55
|
+
'Versions are fetched from the registry.',
|
|
56
|
+
EXIT.INPUT_ERROR,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Resolve domain from registry
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
const { parseName } = require('../registry');
|
|
64
|
+
parsed = parseName(domainInput);
|
|
65
|
+
} catch {
|
|
66
|
+
error(`Invalid domain name: ${domainInput}`, EXIT.INPUT_ERROR);
|
|
67
|
+
}
|
|
68
|
+
if (!parsed) error(`Cannot parse "${domainInput}"`, EXIT.INPUT_ERROR);
|
|
69
|
+
|
|
70
|
+
const resolver = new RegistryResolver({ allowNetwork: true });
|
|
71
|
+
let entry;
|
|
72
|
+
try {
|
|
73
|
+
({ entry } = resolver.resolve(parsed.full));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
error(e.message, EXIT.REGISTRY_ERROR);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Download both versions
|
|
79
|
+
const tmpOld = path.join(TMP_DIR, `kdna-changelog-${Date.now()}-old`);
|
|
80
|
+
const tmpNew = path.join(TMP_DIR, `kdna-changelog-${Date.now()}-new`);
|
|
81
|
+
|
|
82
|
+
if (!jsonMode) console.log(`Fetching ${parsed.full}@${fromVersion}...`);
|
|
83
|
+
downloadVersion(entry, fromVersion, tmpOld);
|
|
84
|
+
if (!jsonMode) console.log(`Fetching ${parsed.full}@${toVersion}...`);
|
|
85
|
+
downloadVersion(entry, toVersion, tmpNew);
|
|
86
|
+
|
|
87
|
+
const oldJ = loadJudgment(tmpOld);
|
|
88
|
+
const newJ = loadJudgment(tmpNew);
|
|
89
|
+
|
|
90
|
+
// Diff maps
|
|
91
|
+
const axioms = diffSummary(oldJ.axioms || {}, newJ.axioms || {}, 'one_sentence');
|
|
92
|
+
const ontology = diffSummary(oldJ.ontology || {}, newJ.ontology || {}, 'concept');
|
|
93
|
+
const misunderstandings = diffSummary(oldJ.misunderstandings || {}, newJ.misunderstandings || {}, 'wrong');
|
|
94
|
+
const bannedTerms = diffList(Object.keys(oldJ.banned_terms || {}), Object.keys(newJ.banned_terms || {}));
|
|
95
|
+
const stances = diffList(oldJ.stances || [], newJ.stances || []);
|
|
96
|
+
|
|
97
|
+
// Version bump suggestion
|
|
98
|
+
const hasRemoved = Object.values(axioms).some((a) => a.status === 'removed') ||
|
|
99
|
+
Object.values(misunderstandings).some((m) => m.status === 'removed');
|
|
100
|
+
const hasAdded = Object.values(axioms).some((a) => a.status === 'added') ||
|
|
101
|
+
Object.values(misunderstandings).some((m) => m.status === 'added');
|
|
102
|
+
const hasChanged = Object.values(axioms).some((a) => a.status === 'changed') ||
|
|
103
|
+
Object.values(misunderstandings).some((m) => m.status === 'changed');
|
|
104
|
+
let recommendedBump = 'none';
|
|
105
|
+
if (hasRemoved) recommendedBump = 'major';
|
|
106
|
+
else if (hasAdded || hasChanged) recommendedBump = 'minor';
|
|
107
|
+
else if (stances.added.length || stances.removed.length) recommendedBump = 'patch';
|
|
108
|
+
|
|
109
|
+
// Cleanup
|
|
110
|
+
try { fs.rmSync(tmpOld, { recursive: true, force: true }); } catch { /* cleanup */ }
|
|
111
|
+
try { fs.rmSync(tmpNew, { recursive: true, force: true }); } catch { /* cleanup */ }
|
|
112
|
+
|
|
113
|
+
// Output
|
|
114
|
+
const changelog = {
|
|
115
|
+
domain: parsed.full,
|
|
116
|
+
from: fromVersion,
|
|
117
|
+
to: toVersion,
|
|
118
|
+
judgment_version: {
|
|
119
|
+
before: oldJ.judgment_version || null,
|
|
120
|
+
after: newJ.judgment_version || null,
|
|
121
|
+
},
|
|
122
|
+
changes: {
|
|
123
|
+
axioms,
|
|
124
|
+
ontology,
|
|
125
|
+
misunderstandings,
|
|
126
|
+
banned_terms: bannedTerms,
|
|
127
|
+
stances,
|
|
128
|
+
},
|
|
129
|
+
recommended_version_bump: recommendedBump,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (jsonMode) {
|
|
133
|
+
console.log(JSON.stringify(changelog, null, 2));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Human-readable markdown
|
|
138
|
+
console.log(`# ${parsed.full} changelog`);
|
|
139
|
+
console.log(`## ${fromVersion} → ${toVersion}`);
|
|
140
|
+
console.log('');
|
|
141
|
+
if (oldJ.judgment_version || newJ.judgment_version) {
|
|
142
|
+
console.log(`Judgment version: ${oldJ.judgment_version || '(none)'} → ${newJ.judgment_version || '(none)'}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
renderSection('Axioms', axioms);
|
|
147
|
+
renderSection('Ontology', ontology);
|
|
148
|
+
renderSection('Misunderstandings', misunderstandings);
|
|
149
|
+
if (bannedTerms.added.length || bannedTerms.removed.length) {
|
|
150
|
+
console.log('### Banned Terms');
|
|
151
|
+
for (const t of bannedTerms.added) console.log(`- **Added:** "${t}"`);
|
|
152
|
+
for (const t of bannedTerms.removed) console.log(`- **Removed:** "${t}"`);
|
|
153
|
+
console.log('');
|
|
154
|
+
}
|
|
155
|
+
if (stances.added.length || stances.removed.length) {
|
|
156
|
+
console.log('### Stances');
|
|
157
|
+
for (const s of stances.added) console.log(`- **Added:** "${s}"`);
|
|
158
|
+
for (const s of stances.removed) console.log(`- **Removed:** "${s}"`);
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const changeCount = Object.values(changelog.changes).reduce((sum, v) => {
|
|
163
|
+
if (Array.isArray(v)) return sum + (v.added?.length || 0) + (v.removed?.length || 0);
|
|
164
|
+
return sum + Object.values(v || {}).filter((x) => x.status !== 'unchanged').length;
|
|
165
|
+
}, 0);
|
|
166
|
+
|
|
167
|
+
console.log(`---`);
|
|
168
|
+
if (changeCount === 0) {
|
|
169
|
+
console.log(`No judgment changes detected.`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`**Recommended version bump: \`${recommendedBump}\`**`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function diffSummary(oldMap, newMap, labelField) {
|
|
176
|
+
const result = {};
|
|
177
|
+
const oldIds = new Set(Object.keys(oldMap));
|
|
178
|
+
const newIds = new Set(Object.keys(newMap));
|
|
179
|
+
|
|
180
|
+
for (const id of newIds) {
|
|
181
|
+
if (!oldIds.has(id)) {
|
|
182
|
+
const item = newMap[id];
|
|
183
|
+
result[id] = { status: 'added', label: item[labelField] || id };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const id of oldIds) {
|
|
187
|
+
if (!newIds.has(id)) {
|
|
188
|
+
const item = oldMap[id];
|
|
189
|
+
result[id] = { status: 'removed', label: item[labelField] || id };
|
|
190
|
+
} else {
|
|
191
|
+
const oldItem = oldMap[id];
|
|
192
|
+
const newItem = newMap[id];
|
|
193
|
+
if (JSON.stringify(oldItem) !== JSON.stringify(newItem)) {
|
|
194
|
+
result[id] = { status: 'changed', label: newItem[labelField] || id };
|
|
195
|
+
} else {
|
|
196
|
+
result[id] = { status: 'unchanged', label: newItem[labelField] || id };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function diffList(oldList, newList) {
|
|
204
|
+
const oldSet = new Set(oldList);
|
|
205
|
+
const newSet = new Set(newList);
|
|
206
|
+
return {
|
|
207
|
+
added: newList.filter((s) => !oldSet.has(s)),
|
|
208
|
+
removed: oldList.filter((s) => !newSet.has(s)),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function renderSection(title, items) {
|
|
213
|
+
const added = Object.entries(items).filter(([, v]) => v.status === 'added');
|
|
214
|
+
const removed = Object.entries(items).filter(([, v]) => v.status === 'removed');
|
|
215
|
+
const changed = Object.entries(items).filter(([, v]) => v.status === 'changed');
|
|
216
|
+
|
|
217
|
+
if (!added.length && !removed.length && !changed.length) return;
|
|
218
|
+
|
|
219
|
+
console.log(`### ${title}`);
|
|
220
|
+
for (const [, v] of added) console.log(`- **Added:** ${v.label}`);
|
|
221
|
+
for (const [, v] of removed) console.log(`- **Removed:** ${v.label}`);
|
|
222
|
+
for (const [, v] of changed) console.log(`- **Changed:** ${v.label}`);
|
|
223
|
+
console.log('');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { cmdChangelog };
|