@lumoai/cli 1.46.0 → 1.48.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/assets/skill/SKILL.md +35 -5
- package/assets/skill/references/criteria.md +9 -9
- package/assets/skill/references/doc-editing.md +10 -10
- package/assets/skill/references/docs.md +22 -22
- package/assets/skill/references/memory.md +9 -10
- package/assets/skill/references/outcome.md +65 -0
- package/assets/skill/references/sessions.md +10 -10
- package/assets/skill/references/task-context.md +7 -7
- package/assets/skill/references/tasks.md +8 -8
- package/assets/skill/references/verify.md +43 -33
- package/assets/skill/references/worktree.md +1 -1
- package/dist/cli/src/commands/criteria-audit.js +52 -0
- package/dist/cli/src/commands/outcome.js +221 -0
- package/dist/cli/src/commands/task-criteria-list.js +8 -1
- package/dist/cli/src/commands/task-criteria-set.js +35 -1
- package/dist/cli/src/commands/task-status.js +68 -4
- package/dist/cli/src/commands/verify.js +14 -1
- package/dist/cli/src/index.js +30 -0
- package/dist/shared/src/referent-kind.js +168 -0
- package/package.json +1 -1
|
@@ -229,19 +229,35 @@ function pushClaimVsVerification(lines, data) {
|
|
|
229
229
|
lines.push(' ▸ Claim — what the agent says it did');
|
|
230
230
|
lines.push(' (agent self-report · estimated, not verification):');
|
|
231
231
|
const claim = data.claim;
|
|
232
|
-
if (claim && claim.text) {
|
|
232
|
+
if (claim && claim.source === 'AGENT' && claim.text) {
|
|
233
|
+
// LUM-597: the agent's OWN self-report (attached to verify) — the
|
|
234
|
+
// authoritative claim faithfulness judges, not a summarizer paraphrase.
|
|
233
235
|
for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
|
|
234
236
|
lines.push(` ${cl}`);
|
|
235
237
|
}
|
|
236
|
-
lines.push(
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
lines.push(' ↳ source: agent self-report (verify --note)');
|
|
239
|
+
}
|
|
240
|
+
else if (claim && claim.source === 'RUN_SUMMARY' && claim.text) {
|
|
241
|
+
for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
|
|
242
|
+
lines.push(` ${cl}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push(' ↳ source: synthesized run summary (no self-report)');
|
|
245
|
+
}
|
|
246
|
+
else if (claim && claim.source === 'DIGEST') {
|
|
247
|
+
// LUM-583 ③ / LUM-574: a raw turn digest is not a claim — withhold it and
|
|
248
|
+
// say the formal summary is still generating, same-source with the web card.
|
|
249
|
+
lines.push(' generating — the formal run summary is still being synthesized');
|
|
239
250
|
}
|
|
240
251
|
else {
|
|
241
252
|
// Fail-closed: no run summary → say so, never invent a claim. Run summaries
|
|
242
253
|
// are synthesized when the bound task reaches DONE (LUM-481).
|
|
243
254
|
lines.push(' not generated yet — the agent run summary is synthesized when the task reaches DONE');
|
|
244
255
|
}
|
|
256
|
+
// ── Faithfulness (LUM-583): the third state — is the claim itself true? Read
|
|
257
|
+
// from the persisted LUM-582 verdict; PENDING (not yet judged) is distinct
|
|
258
|
+
// from UNJUDGEABLE (a real verdict), STALE keeps the verdict but flags a
|
|
259
|
+
// pending re-check. Skipped only when the server didn't emit it (older server).
|
|
260
|
+
pushFaithfulness(lines, data.faithfulness);
|
|
245
261
|
// ── Verification (核验): the measured verdict — machine-verified vs override.
|
|
246
262
|
lines.push(' ▸ Verification — what was actually confirmed (measured):');
|
|
247
263
|
const mv = data.machineVerification;
|
|
@@ -257,6 +273,54 @@ function pushClaimVsVerification(lines, data) {
|
|
|
257
273
|
lines.push(` ${met} of ${data.criteria.length} criteria met by their latest verdict`);
|
|
258
274
|
}
|
|
259
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Append the "Faithfulness" conclusion (LUM-583) — the third state over the
|
|
278
|
+
* 声称/核验 columns: whether the agent's CLAIM is itself TRUE, judged
|
|
279
|
+
* independently against the delivery (LUM-582). Same read model the web honest
|
|
280
|
+
* report renders, so the two surfaces cannot drift. Fail-closed: PENDING (not
|
|
281
|
+
* yet judged) reads distinctly from UNJUDGEABLE (a real verdict), and STALE
|
|
282
|
+
* keeps the prior verdict while flagging a pending re-check. Skipped only when
|
|
283
|
+
* the server didn't emit the field (older server) — never fabricated.
|
|
284
|
+
*/
|
|
285
|
+
function pushFaithfulness(lines, f) {
|
|
286
|
+
if (!f)
|
|
287
|
+
return; // older server: field absent → don't fabricate a conclusion.
|
|
288
|
+
const verdictPhrase = (v) => {
|
|
289
|
+
switch (v) {
|
|
290
|
+
case 'FAITHFUL':
|
|
291
|
+
return 'faithful — the claim matches the delivery';
|
|
292
|
+
case 'OVERSTATED':
|
|
293
|
+
return 'overstated — the claim says more than the diff/PR shows';
|
|
294
|
+
case 'UNDERREPORT':
|
|
295
|
+
return 'under-reported — the claim says less than the diff/PR shows';
|
|
296
|
+
default:
|
|
297
|
+
return "unjudgeable — couldn't decide from the diff/PR";
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
let body;
|
|
301
|
+
if (f.state === 'PENDING') {
|
|
302
|
+
body =
|
|
303
|
+
'not yet judged — the faithfulness check runs in a batch after delivery';
|
|
304
|
+
}
|
|
305
|
+
else if (f.state === 'STALE') {
|
|
306
|
+
body = `${f.verdict ? verdictPhrase(f.verdict) : 'judged'} (stale — re-checked on the next batch)`;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
body = verdictPhrase(f.state);
|
|
310
|
+
}
|
|
311
|
+
const ev = [];
|
|
312
|
+
if (f.prNumbers.length > 0) {
|
|
313
|
+
ev.push(`PR ${f.prNumbers.map(n => `#${n}`).join(', ')}`);
|
|
314
|
+
}
|
|
315
|
+
if (f.diffShas.length > 0) {
|
|
316
|
+
ev.push(f.diffShas
|
|
317
|
+
.slice(0, 3)
|
|
318
|
+
.map(s => (0, sanitize_1.sanitizeField)(s).slice(0, 7))
|
|
319
|
+
.join(' '));
|
|
320
|
+
}
|
|
321
|
+
lines.push(' ▸ Faithfulness — does the claim match the delivery (LUM-583):');
|
|
322
|
+
lines.push(` ${body}${ev.length > 0 ? ` · evidence: ${ev.join(' · ')}` : ''}`);
|
|
323
|
+
}
|
|
260
324
|
/**
|
|
261
325
|
* Append the honest "Cost" section (LUM-560) — 规律 1: surface the costs a human
|
|
262
326
|
* should weigh (token spend, active time, machine rework) on the same report as
|
|
@@ -144,6 +144,19 @@ async function verify(identifier, options = {}) {
|
|
|
144
144
|
`The contract is HUMAN-only; finish your work and hand off for human review (lumo task update ${taskId} --status in_review).\n`);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
+
// ── Require the self-report (LUM-597) ────────────────────────────────────
|
|
148
|
+
// Requesting acceptance is a structured action; `--note` is the agent's
|
|
149
|
+
// mandatory self-report. It is captured deterministically here (the agent
|
|
150
|
+
// can't be made to phrase it well, but it can be made to provide one); its
|
|
151
|
+
// truthfulness is the faithfulness audit's job downstream. No note → no round
|
|
152
|
+
// is posted (no round burned), so re-running with one is free.
|
|
153
|
+
const note = options.note?.trim();
|
|
154
|
+
if (!note) {
|
|
155
|
+
console.error('Error: --note is required — state what you did and why it is ready, e.g.\n' +
|
|
156
|
+
` lumo verify ${taskId} --note "implemented X in foo.ts because Y; tests + tsc pass"\n` +
|
|
157
|
+
'This self-report is recorded as your claim and checked against the diff for faithfulness.');
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
147
160
|
// ── Execute every checkpointer locally ───────────────────────────────────
|
|
148
161
|
process.stdout.write(`Verifying ${taskId} — ${machine.length} MACHINE criteria\n`);
|
|
149
162
|
const results = [];
|
|
@@ -165,7 +178,7 @@ async function verify(identifier, options = {}) {
|
|
|
165
178
|
res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/verify`, {
|
|
166
179
|
method: 'POST',
|
|
167
180
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
168
|
-
body: JSON.stringify({ results }),
|
|
181
|
+
body: JSON.stringify({ results, note }),
|
|
169
182
|
});
|
|
170
183
|
}
|
|
171
184
|
catch (err) {
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,9 +47,11 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const next_1 = require("./commands/next");
|
|
49
49
|
const cost_1 = require("./commands/cost");
|
|
50
|
+
const criteria_audit_1 = require("./commands/criteria-audit");
|
|
50
51
|
const verify_1 = require("./commands/verify");
|
|
51
52
|
const verdict_1 = require("./commands/verdict");
|
|
52
53
|
const crossing_explain_1 = require("./commands/crossing-explain");
|
|
54
|
+
const outcome_1 = require("./commands/outcome");
|
|
53
55
|
const task_context_1 = require("./commands/task-context");
|
|
54
56
|
const task_create_1 = require("./commands/task-create");
|
|
55
57
|
const task_update_1 = require("./commands/task-update");
|
|
@@ -223,6 +225,7 @@ program
|
|
|
223
225
|
.command('verify [task]')
|
|
224
226
|
.description('Machine verification loop (LUM-343): run every MACHINE criterion checkpointer locally, report structured verdicts to the server (round cap 3), and print next actions. All-pass moves the task to IN_REVIEW. Defaults to the session-bound task.')
|
|
225
227
|
.option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
|
|
228
|
+
.option('--note <text>', 'Required self-report — what you did and why it is ready (LUM-597). Recorded as your claim (source AGENT) when the round passes into IN_REVIEW, and checked against the diff for faithfulness.')
|
|
226
229
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
227
230
|
program
|
|
228
231
|
.command('verdict [task]')
|
|
@@ -241,6 +244,26 @@ crossing
|
|
|
241
244
|
.description('Append an agent self-explanation ("申辩") to a boundary crossing (LUM-542). Append-only and for the human reviewer — it never clears the crossing or unblocks Done (a human dispositions that). Targets a crossing on the session-bound task.')
|
|
242
245
|
.requiredOption('--note <text>', 'The explanation to record (the rationale for the action / why it may be a false positive)')
|
|
243
246
|
.action(wrap((id, options) => (0, crossing_explain_1.crossingExplain)(id, options)));
|
|
247
|
+
const outcome = program
|
|
248
|
+
.command('outcome')
|
|
249
|
+
.description('Read / record the post-hoc outcome well (LUM-598)');
|
|
250
|
+
outcome
|
|
251
|
+
.command('show <task>')
|
|
252
|
+
.description('Read the post-hoc outcome well for a task: the falsifier verdict (REJECTED | INCONCLUSIVE — never a "pass") plus its backing rejection signals. INCONCLUSIVE means no rejection on record, NOT satisfied.')
|
|
253
|
+
.action(wrap((task) => (0, outcome_1.outcomeShow)(task)));
|
|
254
|
+
outcome
|
|
255
|
+
.command('record <task>')
|
|
256
|
+
.description('Record a human-observed post-hoc REJECTION of a delivery (revert / rollback / CI regression / downstream redirect / bypass). Append-only; there is no "mark satisfied" counterpart — the well only asserts rejection.')
|
|
257
|
+
.requiredOption('--note <text>', 'What reality did (the observed referent), e.g. "reverted in #812 after a prod incident"')
|
|
258
|
+
.option('--kind <kind>', 'reverted | rolled_back | ci_regression | downstream_redirect | bypassed | manual (case-insensitive; default: manual)')
|
|
259
|
+
.option('--occurred-at <iso>', 'When the event happened (ISO 8601; defaults to now)')
|
|
260
|
+
.action(wrap((task, options) => (0, outcome_1.outcomeRecord)(task, options)));
|
|
261
|
+
outcome
|
|
262
|
+
.command('rate')
|
|
263
|
+
.description('Trust × post-hoc fate: per delivery-time forecast-confidence bracket, the post-hoc REJECTED rate from the outcome well. Honest by construction — thin brackets read "insufficient" and the high-vs-low comparison stays "inconclusive" until the well has enough signal. The rate is a LOWER BOUND (no signal = INCONCLUSIVE, never satisfied).')
|
|
264
|
+
.option('--min <N>', 'Per-bracket sample floor below which a rate is withheld (default 10)')
|
|
265
|
+
.option('--json', 'Emit the report as JSON')
|
|
266
|
+
.action(wrap(options => (0, outcome_1.outcomeRate)(options)));
|
|
244
267
|
program
|
|
245
268
|
.command('next')
|
|
246
269
|
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|
|
@@ -255,6 +278,13 @@ program
|
|
|
255
278
|
.option('--by <dim>', 'Headline grouping: tool | model | member | session (case-insensitive; default tool)')
|
|
256
279
|
.option('--json', 'Emit the versioned payload as JSON')
|
|
257
280
|
.action(wrap(options => (0, cost_1.cost)(options)));
|
|
281
|
+
const criteria = program
|
|
282
|
+
.command('criteria')
|
|
283
|
+
.description('Workspace-level acceptance-criteria analytics');
|
|
284
|
+
criteria
|
|
285
|
+
.command('audit')
|
|
286
|
+
.description('Show the workspace referent-kind distribution + self-confirming-green ratio (GET /api/criteria/audit)')
|
|
287
|
+
.action(wrap(() => (0, criteria_audit_1.criteriaAudit)()));
|
|
258
288
|
const session = program
|
|
259
289
|
.command('session')
|
|
260
290
|
.description('Manage per-terminal coding-session context');
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REFERENT_KIND_DECLARABLE = void 0;
|
|
4
|
+
exports.classifyCheckpointerGrounding = classifyCheckpointerGrounding;
|
|
5
|
+
exports.effectiveReferentKind = effectiveReferentKind;
|
|
6
|
+
/**
|
|
7
|
+
* referentKind (LUM-602): what a criterion anchors — the only dimension that
|
|
8
|
+
* determines independence. Dependency-light (no @prisma/client) so both the
|
|
9
|
+
* Next.js server and the CLI can import it; the Prisma `ReferentKind` enum
|
|
10
|
+
* mirrors REFERENT_KIND_DECLARABLE exactly.
|
|
11
|
+
*/
|
|
12
|
+
exports.REFERENT_KIND_DECLARABLE = [
|
|
13
|
+
'EXTERNAL_FACT',
|
|
14
|
+
'AGENT_CONSTRUCTED_STATE',
|
|
15
|
+
'PENDING_OUTCOME',
|
|
16
|
+
];
|
|
17
|
+
/** Tools whose output the agent cannot author — external facts. */
|
|
18
|
+
const EXTERNAL_TOOLS = [
|
|
19
|
+
'git',
|
|
20
|
+
'gh',
|
|
21
|
+
'curl',
|
|
22
|
+
'wget',
|
|
23
|
+
'http',
|
|
24
|
+
'https',
|
|
25
|
+
'psql',
|
|
26
|
+
'dig',
|
|
27
|
+
'nc',
|
|
28
|
+
];
|
|
29
|
+
/** Agent's own test/type/build harness — green here is self-confirming. */
|
|
30
|
+
const HARNESS_TOOLS = [
|
|
31
|
+
'jest',
|
|
32
|
+
'vitest',
|
|
33
|
+
'mocha',
|
|
34
|
+
'pytest',
|
|
35
|
+
'tsc',
|
|
36
|
+
'node',
|
|
37
|
+
'ts-node',
|
|
38
|
+
'tsx',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* `prisma migrate status` is EXTERNAL (queries DB/migration history); a bare
|
|
42
|
+
* `prisma generate` is harness. Matched as a two-word phrase before the
|
|
43
|
+
* single-word scan.
|
|
44
|
+
*/
|
|
45
|
+
const EXTERNAL_PHRASES = ['prisma migrate status'];
|
|
46
|
+
function wordMatches(word, tools) {
|
|
47
|
+
// exact tool, or a tool-prefixed filename like jest-t.ts / vitest.config
|
|
48
|
+
return tools.some(t => word === t || word.startsWith(`${t}-`) || word.startsWith(`${t}.`));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Given a raw command word (possibly `npx jest`, `npm run build`, etc.),
|
|
52
|
+
* normalize to the effective tool name. Returns null for pure wrapper tokens
|
|
53
|
+
* (npx, sudo, command) that should cause us to advance to the next word.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeCommandWord(word) {
|
|
56
|
+
// strip a leading path: scripts/jest-t.ts → jest-t.ts (keep basename)
|
|
57
|
+
const slash = word.lastIndexOf('/');
|
|
58
|
+
return slash >= 0 ? word.slice(slash + 1) : word;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract command-position words from a shell script string.
|
|
62
|
+
*
|
|
63
|
+
* Algorithm:
|
|
64
|
+
* 1. Strip `#` comments (everything from # to end of line).
|
|
65
|
+
* 2. Extract `$(...)` and backtick substitutions — they are command
|
|
66
|
+
* lists; recurse into them and add their command-position words.
|
|
67
|
+
* 3. For `bash -c '...'` / `sh -c '...'` tokens, extract the quoted
|
|
68
|
+
* payload and recurse into it as a command list.
|
|
69
|
+
* 4. Split the remaining text on command separators (|, &&, ||, ;, \n)
|
|
70
|
+
* to get individual command segments.
|
|
71
|
+
* 5. From each segment, take the first meaningful word as the command.
|
|
72
|
+
* Skip wrapper words (npx, sudo, command) and "npm run" pairs.
|
|
73
|
+
*/
|
|
74
|
+
function collectCommandWords(script) {
|
|
75
|
+
const result = [];
|
|
76
|
+
// Step 1: strip # comments (not inside quotes — simple heuristic: strip
|
|
77
|
+
// from # that is preceded by whitespace or start-of-line to end of line)
|
|
78
|
+
const noComments = script.replace(/(^|\s)#[^\n]*/gm, ' ');
|
|
79
|
+
// Step 2: recurse into $(...) substitutions
|
|
80
|
+
// We use a simple balanced-paren extractor.
|
|
81
|
+
let remaining = noComments;
|
|
82
|
+
remaining = remaining.replace(/\$\(([^)]*)\)/g, (_match, inner) => {
|
|
83
|
+
result.push(...collectCommandWords(inner));
|
|
84
|
+
return ' ';
|
|
85
|
+
});
|
|
86
|
+
// Recurse into backtick substitutions
|
|
87
|
+
remaining = remaining.replace(/`([^`]*)`/g, (_match, inner) => {
|
|
88
|
+
result.push(...collectCommandWords(inner));
|
|
89
|
+
return ' ';
|
|
90
|
+
});
|
|
91
|
+
// Step 3: handle bash -c / sh -c with a single-quoted payload.
|
|
92
|
+
// Pattern: (bash|sh) ... -c '...' or (bash|sh) ... -c "..."
|
|
93
|
+
// We do this BEFORE splitting on separators so the payload content
|
|
94
|
+
// doesn't pollute the segment split.
|
|
95
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+'([^']*)'/g, (_match, payload) => {
|
|
96
|
+
result.push(...collectCommandWords(payload));
|
|
97
|
+
return ' ';
|
|
98
|
+
});
|
|
99
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+"([^"]*)"/g, (_match, payload) => {
|
|
100
|
+
result.push(...collectCommandWords(payload));
|
|
101
|
+
return ' ';
|
|
102
|
+
});
|
|
103
|
+
// Step 4: split on command separators to get segments.
|
|
104
|
+
// Split on |, &&, ||, ;, newlines. Note: || must be checked before |.
|
|
105
|
+
const segments = remaining.split(/\|\||&&|[|;\n]/);
|
|
106
|
+
// Step 5: from each segment extract the command word.
|
|
107
|
+
for (const seg of segments) {
|
|
108
|
+
// Strip leading/trailing whitespace and any residual shell chars
|
|
109
|
+
// (quotes, $, brackets not already consumed).
|
|
110
|
+
const cleaned = seg
|
|
111
|
+
.replace(/['"]/g, ' ') // remove remaining quotes (string args)
|
|
112
|
+
.replace(/\[|\]/g, ' ') // remove [ ] test brackets
|
|
113
|
+
.replace(/\$\{?[^}]*}?/g, ' ') // remove remaining $VAR references
|
|
114
|
+
.trim();
|
|
115
|
+
if (!cleaned)
|
|
116
|
+
continue;
|
|
117
|
+
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
|
118
|
+
// Walk tokens to find the effective command, skipping wrappers.
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < tokens.length) {
|
|
121
|
+
const tok = tokens[i];
|
|
122
|
+
const lower = tok.toLowerCase();
|
|
123
|
+
if (lower === 'npx' || lower === 'sudo' || lower === 'command') {
|
|
124
|
+
i++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (lower === 'npm' && tokens[i + 1]?.toLowerCase() === 'run') {
|
|
128
|
+
// "npm run <script>" — script name is the command
|
|
129
|
+
i += 2;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// This is the effective command
|
|
133
|
+
const word = normalizeCommandWord(lower);
|
|
134
|
+
if (word)
|
|
135
|
+
result.push(word);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function classifyCheckpointerGrounding(checkpointer) {
|
|
142
|
+
if (!checkpointer || checkpointer.trim() === '')
|
|
143
|
+
return 'NONE';
|
|
144
|
+
const lower = checkpointer.toLowerCase();
|
|
145
|
+
// Fast path for known two-word external phrases (e.g. prisma migrate status)
|
|
146
|
+
if (EXTERNAL_PHRASES.some(p => lower.includes(p)))
|
|
147
|
+
return 'EXTERNAL';
|
|
148
|
+
const words = collectCommandWords(checkpointer.toLowerCase());
|
|
149
|
+
if (words.length === 0)
|
|
150
|
+
return 'NONE';
|
|
151
|
+
// Strongest signal wins: EXTERNAL > AGENT_HARNESS > ASSERTION_ONLY
|
|
152
|
+
if (words.some(w => wordMatches(w, EXTERNAL_TOOLS)))
|
|
153
|
+
return 'EXTERNAL';
|
|
154
|
+
if (words.some(w => wordMatches(w, HARNESS_TOOLS)))
|
|
155
|
+
return 'AGENT_HARNESS';
|
|
156
|
+
return 'ASSERTION_ONLY';
|
|
157
|
+
}
|
|
158
|
+
function effectiveReferentKind(args) {
|
|
159
|
+
const { declared, verifierType, checkpointer } = args;
|
|
160
|
+
if (declared === null)
|
|
161
|
+
return 'UNCLASSIFIED';
|
|
162
|
+
if (declared === 'EXTERNAL_FACT' && verifierType === 'MACHINE') {
|
|
163
|
+
if (classifyCheckpointerGrounding(checkpointer) !== 'EXTERNAL') {
|
|
164
|
+
return 'UNVERIFIED_ASSERTION';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return declared;
|
|
168
|
+
}
|