@knolo/cli 5.5.1 → 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/bin/kar-benchmark.mjs +323 -0
- package/bin/kar-graph.mjs +469 -0
- package/bin/kar-limits.mjs +36 -0
- package/bin/kar-produce.mjs +299 -0
- package/bin/kar.mjs +528 -0
- package/bin/knolo.mjs +23 -0
- package/package.json +4 -4
- package/templates/icp-knowledge-canister/canisters/knolo-core-rust/Cargo.toml +1 -1
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.lock +2 -2
- package/templates/icp-knowledge-canister/canisters/knolo-icp-canister/Cargo.toml +2 -2
|
@@ -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
|
+
}
|