@knolo/cli 5.5.0 → 6.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @knolo/cli
2
2
 
3
- The CLI preserves the Knolo V4 pack workflow and adds read-only V5 Knowledge
4
- Image inspection. Normal builds emit V4; the ICP legacy build path explicitly
3
+ The CLI preserves the Knolo V4 pack workflow and adds explicit V5 Knowledge
4
+ Image inspection and querying. Normal builds emit V4; the ICP legacy build path explicitly
5
5
  emits V3 until the canister gains a V4 profile. V5 Studio output is a verified
6
6
  management snapshot, not a mutation interface.
7
7
 
@@ -134,6 +134,22 @@ knolo explain receipt.json --pack dist/knowledge.knolo
134
134
  knolo diff old.knolo new.knolo
135
135
  ```
136
136
 
137
+ `knolo build` produces a V4 pack, so its natural-language query and receipt
138
+ contract applies to that artifact. V5 images are verified Knowledge Images and
139
+ use bounded EQL with roots-bound query results:
140
+
141
+ ```bash
142
+ knolo query 'FROM chunk SEARCH "billing" LIMIT 5' \
143
+ --pack ./dist/knowledge.v5 --json --receipt ./query-result.json
144
+ knolo v5 query ./dist/knowledge.v5 \
145
+ 'FROM chunk SEARCH "billing" LIMIT 5' --json
146
+ knolo explain ./query-result.json --pack ./dist/knowledge.v5
147
+ ```
148
+
149
+ V5 images are never mounted through the V4 pack query API. Semantic sidecars
150
+ currently target V4 packs and are rejected for V5 images with a contract-specific
151
+ message.
152
+
137
153
  V5 runtime diagnostics:
138
154
 
139
155
  ```bash
@@ -0,0 +1,323 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readBoundedJson } from './kar-limits.mjs';
4
+
5
+ const USAGE = `Usage: knolo kar benchmark <annotate|adjudicate|agreement> [options]
6
+
7
+ Blind human gold tooling. Semantics stay kar-1-research-1.
8
+ This command does not read producer output, Domain Pack hits, or KAR results.
9
+
10
+ knolo kar benchmark annotate --packet packet.json --decisions decisions.json --out labels.json --role human --reviewer-id <id>
11
+ knolo kar benchmark adjudicate --packet packet.json --a pass-a.json --b pass-b.json --decisions decisions.json --out gold.json --role human-adjudicator --reviewer-id <id>
12
+ knolo kar benchmark agreement --a pass-a.json --b pass-b.json [--json]
13
+
14
+ Roles: human, domain-expert, human-adjudicator, model-assistant.
15
+ A model id cannot use a human role.
16
+ `;
17
+
18
+ const ROLES = ['human', 'domain-expert', 'human-adjudicator', 'model-assistant'];
19
+ const HUMAN_ROLES = new Set(['human', 'domain-expert', 'human-adjudicator']);
20
+ const MODEL_ID = /llama|gpt|openai|claude|grok|openchat|mistral|qwen|ollama|model/i;
21
+
22
+ const RELATIONS = [
23
+ 'supports', 'permits', 'requires', 'prohibits', 'contradicts', 'qualifies', 'excepts',
24
+ 'overrides', 'supersedes', 'applies_to', 'valid_before', 'valid_after',
25
+ ];
26
+
27
+ const KINDS = {
28
+ contracts: ['agreement', 'party', 'right', 'obligation', 'restriction', 'payment', 'termination', 'renewal', 'exception', 'amendment', 'scope', 'condition', 'effective-date'],
29
+ policy: ['policy', 'role', 'requirement', 'prohibition', 'control', 'exception', 'scope', 'condition', 'effective-date'],
30
+ operations: ['procedure', 'role', 'step', 'precondition', 'prohibition', 'exception', 'equipment', 'scope', 'condition', 'effective-date'],
31
+ };
32
+
33
+ const TOKENS = [
34
+ 'termination-right', 'termination-barred', 'renewal-duty', 'payment-duty', 'stated-obligation',
35
+ 'stated-restriction', 'stated-exception', 'amendment-effect', 'party-scope', 'requirement-duty',
36
+ 'control-duty', 'prohibition-bar', 'stated-permission', 'role-scope', 'stated-scope',
37
+ 'required-step', 'precondition', 'prohibited-state', 'equipment-used', 'temporal-bound',
38
+ 'stated-replacement', 'stated-support',
39
+ ];
40
+
41
+ const FORBIDDEN_FLAGS = ['proposals', 'gold', 'graph', 'domain', 'pack', 'other', 'kar', 'result'];
42
+
43
+ export function runKarBenchmark(args, flags) {
44
+ for (const name of FORBIDDEN_FLAGS) {
45
+ if (flags[name]) throw new Error(`Refusing --${name}. Independent annotation cannot see producer output or another answer key.`);
46
+ }
47
+ const sub = args[0];
48
+ if (!sub || sub === 'help' || flags.help) {
49
+ console.log(USAGE.trim());
50
+ return;
51
+ }
52
+ if (sub === 'annotate') return annotate(flags);
53
+ if (sub === 'adjudicate') return adjudicate(flags);
54
+ if (sub === 'agreement') return agreementCommand(flags);
55
+ throw new Error(`Unknown benchmark command "${sub}".\n${USAGE}`);
56
+ }
57
+
58
+ function annotate(flags) {
59
+ const packet = readPacket(required(flags, 'packet'));
60
+ const decisions = readJson(required(flags, 'decisions'));
61
+ const role = roleOf(flags);
62
+ const reviewerId = required(flags, 'reviewer-id');
63
+ assertRole(role, reviewerId);
64
+ const items = [];
65
+ const incomplete = [];
66
+ const decisionItems = Array.isArray(decisions.items) ? decisions.items : [];
67
+ for (const decision of decisionItems) {
68
+ const block = blockOf(packet, decision.evidenceId);
69
+ if (decision.action === 'skip') continue;
70
+ if (decision.action === 'incomplete') {
71
+ const quote = quoteOf(decision, block);
72
+ incomplete.push({ evidenceId: block.evidenceId, quote, code: 'INCOMPLETE_REFERENCE' });
73
+ continue;
74
+ }
75
+ if (decision.action !== 'label') throw new Error(`Unknown annotation action "${decision.action ?? ''}".`);
76
+ items.push(labelOf(packet.domain, decision, block));
77
+ }
78
+ const record = {
79
+ format: 'kar-annotation-pass-1',
80
+ role,
81
+ reviewerId,
82
+ human: HUMAN_ROLES.has(role),
83
+ blind: true,
84
+ sawProducerOutput: false,
85
+ documentId: packet.documentId,
86
+ domain: packet.domain,
87
+ items,
88
+ incomplete,
89
+ };
90
+ writeJson(required(flags, 'out'), record);
91
+ if (flags.json) console.log(JSON.stringify(record, null, 2));
92
+ else console.log(`ANNOTATION PASS\nrole ${role}\nitems ${items.length}\nincomplete ${incomplete.length}\nhuman ${record.human}`);
93
+ }
94
+
95
+ function adjudicate(flags) {
96
+ const packet = readPacket(required(flags, 'packet'));
97
+ const left = readPass(required(flags, 'a'));
98
+ const right = readPass(required(flags, 'b'));
99
+ if (left.documentId !== packet.documentId || right.documentId !== packet.documentId) {
100
+ throw new Error('Annotation passes and the packet must name the same document.');
101
+ }
102
+ const role = roleOf(flags);
103
+ const reviewerId = required(flags, 'reviewer-id');
104
+ assertRole(role, reviewerId);
105
+ if (role !== 'human-adjudicator' && role !== 'domain-expert' && role !== 'model-assistant') {
106
+ throw new Error('Adjudication requires role human-adjudicator, domain-expert, or model-assistant.');
107
+ }
108
+ const decisions = readJson(required(flags, 'decisions'));
109
+ const items = [];
110
+ for (const decision of decisions.items ?? []) {
111
+ const block = blockOf(packet, decision.evidenceId);
112
+ if (decision.action === 'incomplete') continue;
113
+ items.push(labelOf(packet.domain, { ...decision, action: 'label' }, block));
114
+ }
115
+ const record = {
116
+ format: 'kar-adjudicated-gold-1',
117
+ role,
118
+ reviewerId,
119
+ humanAdjudicator: role === 'human-adjudicator' || role === 'domain-expert',
120
+ releaseGold: role === 'human-adjudicator' || role === 'domain-expert',
121
+ sawProducerOutput: false,
122
+ documentId: packet.documentId,
123
+ domain: packet.domain,
124
+ passA: { role: left.role, reviewerId: left.reviewerId, human: left.human === true },
125
+ passB: { role: right.role, reviewerId: right.reviewerId, human: right.human === true },
126
+ items,
127
+ grounding: items.length === 0 ? null : 1,
128
+ };
129
+ writeJson(required(flags, 'out'), record);
130
+ if (flags.json) console.log(JSON.stringify(record, null, 2));
131
+ else console.log(`ADJUDICATION\nrole ${role}\nitems ${items.length}\nrelease gold ${record.releaseGold}`);
132
+ }
133
+
134
+ function agreementCommand(flags) {
135
+ const left = readPass(required(flags, 'a'));
136
+ const right = readPass(required(flags, 'b'));
137
+ const report = comparePasses(left, right);
138
+ if (flags.json) console.log(JSON.stringify(report, null, 2));
139
+ else {
140
+ const lines = [
141
+ 'ANNOTATION AGREEMENT',
142
+ `documents ${report.documents}`,
143
+ `evidence ${formatMetric(report.evidence)}`,
144
+ `relation ${formatMetric(report.relation)}`,
145
+ `concept kind ${formatMetric(report.conceptKind)}`,
146
+ `strict endpoint ${formatMetric(report.strictEndpoint)}`,
147
+ `requirements ${formatMetric(report.requirements)}`,
148
+ `binding ${formatMetric(report.binding)}`,
149
+ `stable ${report.gates.stable}`,
150
+ ];
151
+ console.log(lines.join('\n'));
152
+ }
153
+ }
154
+
155
+ export function comparePasses(left, right) {
156
+ const relation = metric(left.items, right.items, ['evidenceId', 'relation']);
157
+ const conceptKind = metric(left.items, right.items, ['evidenceId', 'relation', 'fromKind', 'toKind']);
158
+ const stable = relation.agreement !== null
159
+ && conceptKind.agreement !== null
160
+ && relation.agreement >= 0.9
161
+ && conceptKind.agreement >= 0.9;
162
+ return {
163
+ format: 'kar-annotation-agreement-3',
164
+ documents: left.documentId && left.documentId === right.documentId ? 1 : 0,
165
+ sameModelFamily: false,
166
+ humanA: left.human === true && HUMAN_ROLES.has(left.role),
167
+ humanB: right.human === true && HUMAN_ROLES.has(right.role),
168
+ evidence: metric(left.items, right.items, ['evidenceId']),
169
+ relation,
170
+ conceptKind,
171
+ strictEndpoint: metric(left.items, right.items, ['evidenceId', 'relation', 'from', 'to']),
172
+ requirements: metric(left.items, right.items, ['evidenceId', 'relation', 'requirements']),
173
+ binding: metric(left.items, right.items, ['evidenceId', 'relation', 'from', 'to', 'requirements']),
174
+ gates: {
175
+ evidenceRelationAgreement: 0.9,
176
+ conceptKindAgreement: 0.9,
177
+ stable,
178
+ },
179
+ };
180
+ }
181
+
182
+ function metric(leftItems, rightItems, fields) {
183
+ const left = new Set((leftItems ?? []).map((item) => keyOf(item, fields)));
184
+ const right = new Set((rightItems ?? []).map((item) => keyOf(item, fields)));
185
+ let hit = 0;
186
+ for (const key of left) if (right.has(key)) hit += 1;
187
+ const union = new Set([...left, ...right]).size;
188
+ return {
189
+ hit,
190
+ left: left.size,
191
+ right: right.size,
192
+ agreement: union === 0 ? null : hit / union,
193
+ precision: right.size === 0 ? null : hit / right.size,
194
+ recall: left.size === 0 ? null : hit / left.size,
195
+ };
196
+ }
197
+
198
+ function keyOf(item, fields) {
199
+ return fields.map((field) => {
200
+ if (field === 'requirements') return [...(item.requirements ?? [])].sort().join(',');
201
+ return String(item[field] ?? '');
202
+ }).join('\t');
203
+ }
204
+
205
+ function labelOf(domain, decision, block) {
206
+ const quote = quoteOf(decision, block);
207
+ if (!RELATIONS.includes(decision.relation)) throw new Error(`Relation "${decision.relation ?? ''}" is not in Annotation Guide v3.`);
208
+ const kinds = KINDS[domain];
209
+ if (!kinds) throw new Error(`Domain "${domain}" is not in Annotation Guide v3.`);
210
+ if (!kinds.includes(decision.fromKind) || !kinds.includes(decision.toKind)) {
211
+ throw new Error('Concept kind is not in the domain taxonomy.');
212
+ }
213
+ if (!isToken(decision.from) || !isToken(decision.to)) throw new Error('Concept ids must be short identifiers.');
214
+ const requirements = Array.isArray(decision.requirements) ? [...decision.requirements] : [];
215
+ if (requirements.length !== 1 || !TOKENS.includes(requirements[0])) {
216
+ throw new Error('A label needs one requirement token from Annotation Guide v3.');
217
+ }
218
+ const item = {
219
+ evidenceId: block.evidenceId,
220
+ quote,
221
+ relation: decision.relation,
222
+ from: decision.from,
223
+ fromKind: decision.fromKind,
224
+ to: decision.to,
225
+ toKind: decision.toKind,
226
+ requirements,
227
+ };
228
+ if (decision.validFrom !== undefined) {
229
+ if (!isoInQuote(decision.validFrom, quote)) throw new Error('validFrom must be an ISO date written in the quote.');
230
+ item.validFrom = decision.validFrom;
231
+ }
232
+ if (decision.validUntil !== undefined) {
233
+ if (!isoInQuote(decision.validUntil, quote)) throw new Error('validUntil must be an ISO date written in the quote.');
234
+ item.validUntil = decision.validUntil;
235
+ }
236
+ if (decision.authority !== undefined) {
237
+ if (!authorityInQuote(decision.authority, quote)) throw new Error('Numeric authority must be an integer labeled as authority in the quote.');
238
+ item.authority = decision.authority;
239
+ }
240
+ return item;
241
+ }
242
+
243
+ function quoteOf(decision, block) {
244
+ if (typeof decision.quote !== 'string' || decision.quote.length === 0) throw new Error('Each decision needs an exact quote.');
245
+ if (!block.text.includes(decision.quote)) throw new Error(`Quote is not an exact substring of evidence ${block.evidenceId}.`);
246
+ return decision.quote;
247
+ }
248
+
249
+ function isoInQuote(value, quote) {
250
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) && quote.includes(value);
251
+ }
252
+
253
+ function authorityInQuote(value, quote) {
254
+ return Number.isInteger(value) && value >= 0 && new RegExp(`authority\\s+${value}(?!\\d)`, 'i').test(quote);
255
+ }
256
+
257
+ function isToken(value) {
258
+ return typeof value === 'string' && /^[a-z][a-z0-9-]{0,80}$/.test(value);
259
+ }
260
+
261
+ function readPacket(file) {
262
+ const packet = readJson(file);
263
+ if (packet.format !== 'kar-annotation-packet-1') throw new Error('Packet format must be kar-annotation-packet-1.');
264
+ if (packet.proposals || packet.gold || packet.producer || packet.kar) {
265
+ throw new Error('Packet contains producer or answer-key fields.');
266
+ }
267
+ if (!isToken(packet.documentId) || !KINDS[packet.domain]) throw new Error('Packet document id or domain is invalid.');
268
+ if (!Array.isArray(packet.blocks) || packet.blocks.length === 0) throw new Error('Packet has no evidence blocks.');
269
+ const seen = new Set();
270
+ for (const block of packet.blocks) {
271
+ if (!isToken(block.evidenceId) || typeof block.text !== 'string' || block.text.length === 0) {
272
+ throw new Error('Each evidence block needs an id and text.');
273
+ }
274
+ if (seen.has(block.evidenceId)) throw new Error(`Duplicate evidence id ${block.evidenceId}.`);
275
+ seen.add(block.evidenceId);
276
+ }
277
+ return packet;
278
+ }
279
+
280
+ function blockOf(packet, evidenceId) {
281
+ const block = packet.blocks.find((item) => item.evidenceId === evidenceId);
282
+ if (!block) throw new Error(`Evidence ${evidenceId ?? ''} is not in the packet.`);
283
+ return block;
284
+ }
285
+
286
+ function readPass(file) {
287
+ const pass = readJson(file);
288
+ if (pass.format !== 'kar-annotation-pass-1' && pass.format !== 'kar-adjudicated-gold-1') {
289
+ throw new Error('Annotation file format is not recognized.');
290
+ }
291
+ if (!Array.isArray(pass.items)) throw new Error('Annotation file has no items.');
292
+ return pass;
293
+ }
294
+
295
+ function roleOf(flags) {
296
+ const role = required(flags, 'role');
297
+ if (!ROLES.includes(role)) throw new Error(`Role must be one of ${ROLES.join(', ')}.`);
298
+ return role;
299
+ }
300
+
301
+ function assertRole(role, reviewerId) {
302
+ if (HUMAN_ROLES.has(role) && MODEL_ID.test(reviewerId)) {
303
+ throw new Error(`Reviewer id "${reviewerId}" is a model. Use role model-assistant.`);
304
+ }
305
+ }
306
+
307
+ function formatMetric(metricValue) {
308
+ if (metricValue.agreement === null) return 'n/a';
309
+ return `${(metricValue.agreement * 100).toFixed(1)}% (${metricValue.hit}/${metricValue.left} vs ${metricValue.right})`;
310
+ }
311
+
312
+ function required(flags, name) {
313
+ if (!flags[name]) throw new Error(`Missing --${name}`);
314
+ return flags[name];
315
+ }
316
+
317
+ function readJson(file) {
318
+ return readBoundedJson(file);
319
+ }
320
+
321
+ function writeJson(file, value) {
322
+ writeFileSync(path.resolve(process.cwd(), file), `${JSON.stringify(value, null, 2)}\n`);
323
+ }