@skill-harness/cli 0.16.0 → 0.18.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 +0 -13
- package/assets/report.template.html +1 -43
- package/dist/cli.d.ts +0 -8
- package/dist/cli.js +16 -237
- package/dist/serve.js +2 -82
- package/package.json +4 -6
- package/dist/archive-learning.d.ts +0 -3
- package/dist/archive-learning.js +0 -153
- package/dist/archive.d.ts +0 -3
- package/dist/archive.js +0 -52
- package/dist/compare.d.ts +0 -26
- package/dist/compare.js +0 -232
- package/dist/learning.d.ts +0 -26
- package/dist/learning.js +0 -656
- package/dist/qualification.d.ts +0 -7
- package/dist/qualification.js +0 -210
- package/docs/PRODUCT-GUIDE.md +0 -133
- package/docs/STATUS.md +0 -45
package/dist/learning.js
DELETED
|
@@ -1,656 +0,0 @@
|
|
|
1
|
-
import { resolve, join, dirname } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { createInterface } from 'node:readline/promises';
|
|
5
|
-
import { createLearningWorkspace, openLearningWorkspace, catalogLearningArchive, learningHash, learningFile, retainArchiveSource, previewLearningTrust, configureLearningTrust, previewLearningCaseLabel, trustOutcomeDigest, } from '@skill-harness/adapters';
|
|
6
|
-
export const LEARNING_HELP = `Learning — retained evidence, no model calls
|
|
7
|
-
learning open guided review (interactive terminal)
|
|
8
|
-
learning init --archive DIR --author NAME [--scope-item N] --confirm
|
|
9
|
-
[--scope TEXT --population TEXT only for a comparison-only archive without retained cases]
|
|
10
|
-
learning status [--json] readiness; never automatic exposure
|
|
11
|
-
learning import list retained batches/comparisons
|
|
12
|
-
learning import --item N --name NAME --title TEXT --scope-note TEXT --confirm
|
|
13
|
-
learning cases NAME [--offset N]
|
|
14
|
-
learning evidence NAME --item N [--evidence N] inspect only the selected case's retained sources
|
|
15
|
-
learning case NAME --item N --disposition confirmed_defect|expected_behavior|exemplar|uncertain|skip --note TEXT --confirm
|
|
16
|
-
learning comparisons
|
|
17
|
-
learning review NAME guided complete-output review in a terminal
|
|
18
|
-
learning artifact NAME --variant A [--artifact N]
|
|
19
|
-
learning choose NAME --kind one|tie|none|insufficient [--variants A,B] --full-review --note TEXT --confirm
|
|
20
|
-
learning reveal NAME requires durable full-scope quality receipt
|
|
21
|
-
learning decide NAME --disposition adopt|reject|defer --note TEXT --confirm
|
|
22
|
-
learning hypothesis NAME --name SLUG --intervention TEXT --prediction TEXT --disproof TEXT --downside TEXT --rollback TEXT --alternative TEXT --confirm
|
|
23
|
-
learning link NAME --cases BATCH --item N --hypothesis SLUG --confirm
|
|
24
|
-
learning trust sample FILE --name SLUG --title TEXT [--split calibration|heldout|tuning] --confirm
|
|
25
|
-
learning trust setup --cases NAME [--detector ID] [--split calibration|heldout|tuning] [--max-unflagged N]
|
|
26
|
-
[--attention N --minimum-resolved N --minimum-lower-bound T --expires DAYS] --confirm
|
|
27
|
-
learning trust status
|
|
28
|
-
learning trust label --cases NAME --item N --reference-author NAME --confirm
|
|
29
|
-
learning trust unflagged --item N --miss yes|no --evidence FILE --reference-author NAME --note TEXT --confirm
|
|
30
|
-
learning adoption NAME scope/decision/readiness; activation is producer-owned
|
|
31
|
-
learning outcome NAME inspect observed outcomes
|
|
32
|
-
learning outcome NAME --result success|confirmed-defect|unknown --artifact FILE --original-requirement FILE --current-requirement FILE --evidence FILE --reference-author NAME --note TEXT [--accepted-artifact FILE --acceptance-evidence FILE] --confirm
|
|
33
|
-
learning guide | current installed portable guide / requirement register
|
|
34
|
-
All commands accept --state DIR (default .skill-harness/learning), --json for structured output.
|
|
35
|
-
--confirm authorizes only the displayed local write, never models, acceptance or registry activation.
|
|
36
|
-
No raw manifest authoring. Import selects actual retained inputs. Exit zero is command completion, not quality.
|
|
37
|
-
Registry activation/rollback use /grants learning in interactive Pi, with independent authority.`;
|
|
38
|
-
/** Escape terminal control/format characters in untrusted retained display text. Exact archive
|
|
39
|
-
* bytes never change; output honestly identifies this as a display representation. */
|
|
40
|
-
export function learningDisplay(text) {
|
|
41
|
-
return text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
42
|
-
}
|
|
43
|
-
function parse(argv) {
|
|
44
|
-
const args = [], flags = {};
|
|
45
|
-
for (let i = 0; i < argv.length; i++) {
|
|
46
|
-
const token = argv[i];
|
|
47
|
-
if (!token.startsWith('--')) {
|
|
48
|
-
args.push(token);
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const eq = token.indexOf('='), key = token.slice(2, eq < 0 ? undefined : eq);
|
|
52
|
-
if (!key || Object.hasOwn(flags, key))
|
|
53
|
-
throw Error('duplicate or empty learning option');
|
|
54
|
-
if (eq >= 0)
|
|
55
|
-
flags[key] = token.slice(eq + 1);
|
|
56
|
-
else if (['confirm', 'json', 'full-review', 'help'].includes(key))
|
|
57
|
-
flags[key] = true;
|
|
58
|
-
else {
|
|
59
|
-
if (!argv[i + 1] || argv[i + 1].startsWith('--'))
|
|
60
|
-
throw Error(`--${key} needs a value`);
|
|
61
|
-
flags[key] = argv[++i];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { args, flags };
|
|
65
|
-
}
|
|
66
|
-
const word = (v, name) => { if (typeof v !== 'string' || !v.trim())
|
|
67
|
-
throw Error(`${name} is required`); return v; };
|
|
68
|
-
const integer = (v, fallback) => { const n = v === undefined ? fallback : Number(v); if (!Number.isSafeInteger(n) || n < 0)
|
|
69
|
-
throw Error('nonnegative integer required'); return n; };
|
|
70
|
-
function artifactText(bytes) {
|
|
71
|
-
try {
|
|
72
|
-
return learningDisplay(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
throw Error('Retained source is not UTF-8 text; no complete text view is claimed');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function allCases(workspace, name) {
|
|
79
|
-
const page = workspace.cases(name), items = [...page.items];
|
|
80
|
-
for (let offset = 5; offset < page.total; offset += 5)
|
|
81
|
-
items.push(...workspace.cases(name, offset).items);
|
|
82
|
-
return { ...page, items };
|
|
83
|
-
}
|
|
84
|
-
function caseAt(workspace, name, index) {
|
|
85
|
-
const page = allCases(workspace, name), item = page.items[index - 1];
|
|
86
|
-
if (!item)
|
|
87
|
-
throw Error('select a listed case number');
|
|
88
|
-
return { page, item };
|
|
89
|
-
}
|
|
90
|
-
function trustSetup(workspace, flags) {
|
|
91
|
-
const caseName = word(flags.cases, '--cases'), cases = allCases(workspace, caseName).items;
|
|
92
|
-
const detectorId = typeof flags.detector === 'string' ? flags.detector : cases[0]?.candidate.detector.id;
|
|
93
|
-
if (!detectorId)
|
|
94
|
-
throw Error('no retained detector cases available');
|
|
95
|
-
const attention = integer(flags.attention, 0), days = integer(flags.expires, 7), split = String(flags.split ?? 'calibration');
|
|
96
|
-
if (attention && (flags['minimum-resolved'] === undefined || flags['minimum-lower-bound'] === undefined))
|
|
97
|
-
throw Error('automatic questions need explicit --minimum-resolved and --minimum-lower-bound');
|
|
98
|
-
const minimumLowerBound = Number(flags['minimum-lower-bound']);
|
|
99
|
-
if (attention && (!Number.isFinite(minimumLowerBound) || minimumLowerBound < 0 || minimumLowerBound > 1 || days < 1 || days > 365))
|
|
100
|
-
throw Error('invalid explicit exposure policy');
|
|
101
|
-
return { caseName, detectorId, split, maxUnflagged: integer(flags['max-unflagged'], 0), unflagged: workspace.unflagged().map(({ incidentId, manifestId, split }) => ({ incidentId, manifestId, split })),
|
|
102
|
-
exposure: attention ? { id: `operator-${workspace.configuration().scopeDigest.slice(0, 20)}`, attentionRemaining: attention, minimumResolved: integer(flags['minimum-resolved'], 10), minimumLowerBound, expiresAt: Date.now() + days * 86400000 } : null };
|
|
103
|
-
}
|
|
104
|
-
export function formatLearningStatus(view) {
|
|
105
|
-
const lines = [`Learning — ${view.configuration.population}`, 'Deliberate review; no model calls, automatic questions, or activation from reading.'];
|
|
106
|
-
for (const c of view.cases)
|
|
107
|
-
lines.push(`Cases: ${c.title} (${c.name}) — ${c.state === 'ready' ? `${c.total} retained nomination(s)` : c.reason}`);
|
|
108
|
-
for (const c of view.comparisons)
|
|
109
|
-
lines.push(`Comparison: ${c.title} (${c.name}) — ${c.reason}${'decision' in c && c.decision ? `; decision: ${c.decision.disposition}` : ''}`);
|
|
110
|
-
if (!view.cases.length && !view.comparisons.length)
|
|
111
|
-
lines.push('No inputs connected. Choose Connect retained input; missing evidence is not a failure or acceptance.');
|
|
112
|
-
lines.push(formatLearningTrust(view.trust));
|
|
113
|
-
return learningDisplay(lines.join('\n'));
|
|
114
|
-
}
|
|
115
|
-
export function formatLearningTrust(trust) {
|
|
116
|
-
const advice = trust.automaticExposure;
|
|
117
|
-
const calibration = trust.calibration;
|
|
118
|
-
const sample = trust.unflagged;
|
|
119
|
-
const lines = [`Automatic questions: ${advice?.mode ?? 'silent'} — ${advice?.reason ?? trust.reason ?? 'not configured'} (reading reserves no attention).`];
|
|
120
|
-
for (const r of calibration?.reports ?? [])
|
|
121
|
-
lines.push(`Independent labels: ${r.resolved} resolved, ${r.unresolved} unresolved, ${r.conflicted} conflicted; precision ${r.precision === null ? 'unknown' : `${Math.round(r.precision * 100)}%`}${r.interval ? ` (95% interval ${Math.round(r.interval.lower * 100)}–${Math.round(r.interval.upper * 100)}%)` : ''}.`);
|
|
122
|
-
if (sample)
|
|
123
|
-
lines.push(`Unflagged sample: ${sample.sampled} frozen, ${sample.resolved} independently labelled, ${sample.misses} observed miss(es).${sample.sampled ? '' : ' No unflagged coverage established.'}`);
|
|
124
|
-
if (typeof trust.attentionUsed === 'number')
|
|
125
|
-
lines.push(`Attention already used: ${trust.attentionUsed}. Reopening does not refill it.`);
|
|
126
|
-
return learningDisplay(lines.join('\n'));
|
|
127
|
-
}
|
|
128
|
-
export async function runLearningCommand(argv, options = {}) {
|
|
129
|
-
const { args, flags } = parse(argv), cwd = options.cwd ?? process.cwd(), directory = resolve(cwd, typeof flags.state === 'string' ? flags.state : '.skill-harness/learning');
|
|
130
|
-
const out = options.write ?? console.log;
|
|
131
|
-
const emit = (value) => { out(flags.json ? JSON.stringify(value, null, 2) : typeof value === 'string' ? value : learningDisplay(JSON.stringify(value, null, 2))); return value; };
|
|
132
|
-
const confirm = (preview, write) => {
|
|
133
|
-
if (flags.confirm !== true)
|
|
134
|
-
return emit({ state: 'confirmation-required', preview, next: 'Repeat with --confirm to authorize this exact local action.' });
|
|
135
|
-
return emit(write());
|
|
136
|
-
};
|
|
137
|
-
const global = ['state', 'json', 'confirm', 'help'];
|
|
138
|
-
const allowed = {
|
|
139
|
-
init: ['archive', 'scope', 'population', 'author', 'scope-item'], status: [], import: ['item', 'name', 'title', 'scope-note'], cases: ['offset'], evidence: ['item', 'evidence'], case: ['item', 'disposition', 'note'], comparisons: [], review: [], artifact: ['variant', 'artifact'], choose: ['kind', 'variants', 'full-review', 'note'], reveal: [], decide: ['disposition', 'note'], hypothesis: ['name', 'intervention', 'prediction', 'disproof', 'downside', 'rollback', 'alternative'], link: ['cases', 'item', 'hypothesis'], trust: ['cases', 'item', 'detector', 'split', 'max-unflagged', 'attention', 'minimum-resolved', 'minimum-lower-bound', 'expires', 'name', 'title', 'reference-author', 'miss', 'evidence', 'note'], adoption: [], outcome: ['result', 'artifact', 'original-requirement', 'current-requirement', 'evidence', 'reference-author', 'note', 'accepted-artifact', 'acceptance-evidence'], guide: [], help: []
|
|
140
|
-
};
|
|
141
|
-
const command = args[0];
|
|
142
|
-
if (command === 'guide' || command === 'current') {
|
|
143
|
-
const file = command === 'guide' ? 'PRODUCT-GUIDE.md' : 'STATUS.md', base = dirname(fileURLToPath(import.meta.url));
|
|
144
|
-
const selected = [resolve(base, '../docs', file), resolve(base, '../../../docs/factory', file)].find(p => existsSync(p));
|
|
145
|
-
if (!selected)
|
|
146
|
-
throw Error('packaged learning guide missing; reinstall a complete compatible package');
|
|
147
|
-
return emit(learningFile(selected, 65536).toString('utf8'));
|
|
148
|
-
}
|
|
149
|
-
if (flags.help || command === 'help')
|
|
150
|
-
return emit(LEARNING_HELP);
|
|
151
|
-
if (!command) {
|
|
152
|
-
const terminal = options.ui ? null : terminalLearningUI();
|
|
153
|
-
try {
|
|
154
|
-
return await runLearningWizard({ cwd, directory, ui: options.ui ?? terminal });
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
terminal?.close();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!Object.hasOwn(allowed, command))
|
|
161
|
-
throw Error('unknown learning command; use learning help');
|
|
162
|
-
const unexpected = Object.keys(flags).filter(k => !global.includes(k) && !allowed[command].includes(k));
|
|
163
|
-
if (unexpected.length)
|
|
164
|
-
throw Error(`unsupported learning option: --${unexpected[0]}`);
|
|
165
|
-
if (command === 'init') {
|
|
166
|
-
const archiveRoot = resolve(cwd, word(flags.archive, '--archive')), author = word(flags.author, '--author'), scopes = learningArchiveScopes(archiveRoot, author);
|
|
167
|
-
if (scopes.length > 1 && flags['scope-item'] === undefined)
|
|
168
|
-
return emit({ state: 'scope-selection-required', scopes: scopes.map((s, i) => ({ item: i + 1, description: s.description, population: s.population })), next: 'Repeat init with --scope-item N; scope identity is derived from retained facts, not typed hashes.' });
|
|
169
|
-
const selected = scopes[integer(flags['scope-item'], 1) - 1];
|
|
170
|
-
if (scopes.length && !selected)
|
|
171
|
-
throw Error('select a listed retained scope number');
|
|
172
|
-
if (selected && flags.population !== undefined && flags.population !== selected.population)
|
|
173
|
-
throw Error('population differs from retained case scope');
|
|
174
|
-
const input = { archiveRoot, scopeDigest: selected?.scopeDigest ?? learningHash({ declaredScope: word(flags.scope, '--scope for a comparison-only archive') }), population: selected?.population ?? word(flags.population, '--population for a comparison-only archive'), author };
|
|
175
|
-
return confirm({ directory, ...input, scopeMeaning: selected ? 'exact retained case snapshot' : 'operator-declared comparison-only scope; cannot bind mismatching case snapshots' }, () => createLearningWorkspace(directory, input).inspect(Date.now()));
|
|
176
|
-
}
|
|
177
|
-
if (!existsSync(directory))
|
|
178
|
-
throw Error('learning workspace not connected; use learning init or /grants learning with the current host');
|
|
179
|
-
const workspace = openLearningWorkspace(directory), config = workspace.configuration(), name = args[1];
|
|
180
|
-
if (command === 'status') {
|
|
181
|
-
const view = workspace.inspect(Date.now());
|
|
182
|
-
return emit(flags.json ? view : formatLearningStatus(view));
|
|
183
|
-
}
|
|
184
|
-
if (command === 'import') {
|
|
185
|
-
const catalog = catalogLearningArchive(config.archiveRoot, config.author);
|
|
186
|
-
if (flags.item === undefined)
|
|
187
|
-
return emit(flags.json ? catalog.map((c, i) => ({ item: i + 1, ...c })) : catalog.length ? catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`).join('\n') : 'No compatible retained inputs. Ordinary work must retain actual evidence first.');
|
|
188
|
-
const item = catalog[integer(flags.item, 0) - 1];
|
|
189
|
-
if (!item)
|
|
190
|
-
throw Error('select a listed retained input number');
|
|
191
|
-
const binding = { name: word(flags.name, '--name'), title: word(flags.title, '--title') };
|
|
192
|
-
return confirm({ selected: item, ...binding }, () => item.kind === 'cases' ? workspace.bindCases({ ...binding, version: item.version, batchId: item.manifestId }) : workspace.bindComparison({ ...binding, scopeStatement: word(flags['scope-note'], '--scope-note'), comparisonManifestId: item.manifestId, caseReference: null, hypothesisManifestId: null, adoptionBinding: null, priorFeedbackManifestIds: [] }));
|
|
193
|
-
}
|
|
194
|
-
if (command === 'cases') {
|
|
195
|
-
const page = workspace.cases(word(name, 'case batch name'), integer(flags.offset, 0));
|
|
196
|
-
return emit(flags.json ? page : page.items.map((c, i) => `${page.offset + i + 1}. ${c.candidate.target.obligationId}: ${c.candidate.reason} — ${c.disposition}`).join('\n') + `\n${page.total} retained nomination(s). Use learning evidence for retained bytes; learning case records an independent disposition.`);
|
|
197
|
-
}
|
|
198
|
-
if (command === 'evidence') {
|
|
199
|
-
const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 1)), refs = workspace.caseEvidence(batch, item.caseManifestId);
|
|
200
|
-
if (flags.evidence === undefined)
|
|
201
|
-
return emit(flags.json ? refs : (refs.length === 1 ? 'Only nomination retained; no original observation link. Not independent proof.\n' : '') + refs.map((r, i) => `${i + 1}. ${r.role}: ${r.status === 'available' ? `${r.reference.parser.id} (${r.reference.retention})` : r.status}`).join('\n'));
|
|
202
|
-
const ref = refs[integer(flags.evidence, 1) - 1];
|
|
203
|
-
if (!ref)
|
|
204
|
-
throw Error('select a listed evidence number');
|
|
205
|
-
const result = workspace.readCaseEvidence(batch, item.caseManifestId, ref.manifestId);
|
|
206
|
-
if (result.status !== 'available')
|
|
207
|
-
return emit({ state: 'deferred', reason: result.status });
|
|
208
|
-
return emit(`${result.reference.retention === 'exact' ? 'Exact retained source' : 'Redacted retained source — not full evidence'}\n${artifactText(result.bytes)}`);
|
|
209
|
-
}
|
|
210
|
-
if (command === 'case') {
|
|
211
|
-
const batch = word(name, 'case batch name'), { item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
212
|
-
const request = { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note') };
|
|
213
|
-
return confirm({ candidate: item.candidate, request }, () => workspace.decideCase(batch, request));
|
|
214
|
-
}
|
|
215
|
-
if (command === 'comparisons') {
|
|
216
|
-
const view = workspace.inspect(Date.now());
|
|
217
|
-
return emit(flags.json ? view.comparisons : view.comparisons.map(c => `${c.name}: ${c.title} — ${c.reason}`).join('\n') || 'No retained comparisons connected.');
|
|
218
|
-
}
|
|
219
|
-
if (command === 'review') {
|
|
220
|
-
const terminal = options.ui ? null : terminalLearningUI();
|
|
221
|
-
try {
|
|
222
|
-
return await reviewLearningComparison(directory, word(name, 'comparison name'), options.ui ?? terminal);
|
|
223
|
-
}
|
|
224
|
-
finally {
|
|
225
|
-
terminal?.close();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (command === 'artifact') {
|
|
229
|
-
const v = workspace.comparison(word(name, 'comparison name')), card = v.cards.find(c => c.displayLabel === flags.variant);
|
|
230
|
-
if (!card)
|
|
231
|
-
throw Error('select a listed variant letter');
|
|
232
|
-
const digest = card.artifactDigests[integer(flags.artifact, 1) - 1];
|
|
233
|
-
if (!digest)
|
|
234
|
-
throw Error('select a listed artifact number');
|
|
235
|
-
const bytes = workspace.artifact(name, card.label, digest);
|
|
236
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
237
|
-
return emit(flags.json ? { variant: card.displayLabel, digest, bytes: bytes.length, scope: 'complete-artifact-display', controlCharacters: 'escaped (retained bytes unchanged)', text: learningDisplay(text) } : `Variant ${card.displayLabel} — complete artifact (${bytes.length} bytes; controls escaped, retained bytes unchanged)\n\n${learningDisplay(text)}`);
|
|
238
|
-
}
|
|
239
|
-
if (command === 'choose') {
|
|
240
|
-
const v = workspace.comparison(word(name, 'comparison name')), kind = word(flags.kind, '--kind');
|
|
241
|
-
const letters = typeof flags.variants === 'string' ? flags.variants.split(',') : [];
|
|
242
|
-
const labels = letters.map(letter => { const c = v.cards.find(c => c.displayLabel === letter); if (!c)
|
|
243
|
-
throw Error('select listed variant letters'); return c.label; });
|
|
244
|
-
if (kind !== 'insufficient' && flags['full-review'] !== true)
|
|
245
|
-
throw Error('--full-review explicitly acknowledges all complete artifacts, not excerpts');
|
|
246
|
-
const choice = { kind, labels }, review = { scope: 'full-artifacts', note: word(flags.note, '--note'), reviewedArtifactDigests: flags['full-review'] === true ? [...new Set(v.cards.flatMap(c => c.artifactDigests))] : [] };
|
|
247
|
-
return confirm({ comparison: name, choice, review }, () => workspace.choose(name, choice, review));
|
|
248
|
-
}
|
|
249
|
-
if (command === 'reveal')
|
|
250
|
-
return emit(workspace.reveal(word(name, 'comparison name')));
|
|
251
|
-
if (command === 'decide') {
|
|
252
|
-
const v = workspace.decisionStatus(word(name, 'comparison name')), request = { disposition: word(flags.disposition, '--disposition'), note: word(flags.note, '--note'), priorDecisionId: v.current?.id ?? null };
|
|
253
|
-
return confirm({ comparison: name, scope: v.scopeStatement, request, activation: 'not-performed' }, () => workspace.decide(name, request));
|
|
254
|
-
}
|
|
255
|
-
if (command === 'hypothesis') {
|
|
256
|
-
const batch = word(name, 'case batch name'), cases = allCases(workspace, batch).items.filter(c => c.disposition === 'confirmed_defect');
|
|
257
|
-
if (!cases.length)
|
|
258
|
-
throw Error('confirm a retained defect before proposing a repair');
|
|
259
|
-
const proposal = { archiveSnapshot: learningHash(cases.map(c => ({ case: c.caseManifestId, decision: c.priorDecisionId }))), caseIds: cases.map(c => c.candidate.id), population: config.population,
|
|
260
|
-
intervention: word(flags.intervention, '--intervention'), prediction: word(flags.prediction, '--prediction'), disproof: word(flags.disproof, '--disproof'), downside: word(flags.downside, '--downside'), rollback: word(flags.rollback, '--rollback'), alternatives: [word(flags.alternative, '--alternative')], limits: { subjectCalls: 0, judgeCalls: 0, wallMs: 0 }, effectProfile: null };
|
|
261
|
-
return confirm({ proposal, scope: 'proposal-only; zero calls, no authorization' }, () => workspace.propose(batch, proposal, word(flags.name, '--name')));
|
|
262
|
-
}
|
|
263
|
-
if (command === 'link') {
|
|
264
|
-
const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
265
|
-
if (!item.priorDecisionId)
|
|
266
|
-
throw Error('case decision required');
|
|
267
|
-
const h = workspace.hypotheses().find(h => h.name === flags.hypothesis);
|
|
268
|
-
if (!h)
|
|
269
|
-
throw Error('select a hypothesis created with learning hypothesis');
|
|
270
|
-
const context = { ...workspace.comparisonContext(word(name, 'comparison name')), caseReference: { version: page.version, batchId: page.batchId, manifestId: item.caseManifestId, decisionId: item.priorDecisionId }, hypothesisManifestId: h.manifestId };
|
|
271
|
-
return confirm({ comparison: name, context }, () => workspace.linkComparisonContext(name, context));
|
|
272
|
-
}
|
|
273
|
-
if (command === 'trust') {
|
|
274
|
-
if (name === 'sample') {
|
|
275
|
-
const bytes = learningFile(resolve(cwd, word(args[2], 'sample evidence file')), 65536), sampleName = word(flags.name, '--name');
|
|
276
|
-
return confirm({ source: args[2], bytes: bytes.length, title: flags.title, meaning: 'operator-selected unflagged incident; NOT a correctness label' }, () => { const manifestId = retainArchiveSource(config.archiveRoot, { sourceId: `unflagged-${learningHash(bytes.toString('base64')).slice(0, 24)}`, parser: { id: 'operator-unflagged-evidence', version: '1' }, retention: 'exact', bytes }).manifestId; return workspace.addUnflagged({ name: sampleName, title: word(flags.title, '--title'), manifestId, split: String(flags.split ?? 'calibration') }); });
|
|
277
|
-
}
|
|
278
|
-
if (name === 'setup') {
|
|
279
|
-
const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
|
|
280
|
-
return confirm({ ...preview, independentLabels: 'none created', automaticQuestions: setup.exposure ? 'only if independently calibrated policy is met' : 'disabled' }, () => configureLearningTrust(directory, setup, [preview.policyDigest]));
|
|
281
|
-
}
|
|
282
|
-
const trust = workspace.trust();
|
|
283
|
-
if (name === 'status' || !name) {
|
|
284
|
-
const view = { ...trust.inspect(Date.now()), automaticExposure: trust.previewExposure(Date.now()), cohort: trust.configuration().cohort };
|
|
285
|
-
return emit(flags.json ? view : formatLearningTrust(view));
|
|
286
|
-
}
|
|
287
|
-
if (name === 'label') {
|
|
288
|
-
const batch = word(flags.cases, '--cases'), { page, item } = caseAt(workspace, batch, integer(flags.item, 0));
|
|
289
|
-
if (!item.priorDecisionId)
|
|
290
|
-
throw Error('record an independent case decision first');
|
|
291
|
-
const reference = { version: page.version, batchId: page.batchId, manifestId: item.caseManifestId, decisionId: item.priorDecisionId }, preview = previewLearningCaseLabel(directory, reference), author = word(flags['reference-author'], '--reference-author');
|
|
292
|
-
return confirm({ author, reference, preview, meaning: 'independent correctness label, NOT writing preference or panel agreement' }, () => { trust.linkCaseOutcome(preview.predictionId, reference, [preview.digest], author); return { recorded: true, author, ...trust.inspect(Date.now()) }; });
|
|
293
|
-
}
|
|
294
|
-
if (name === 'unflagged') {
|
|
295
|
-
const id = trust.inspect(Date.now()).sampledIncidentIds[integer(flags.item, 0) - 1];
|
|
296
|
-
if (!id)
|
|
297
|
-
throw Error('select a frozen sampled unflagged incident number');
|
|
298
|
-
const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note');
|
|
299
|
-
if (!['yes', 'no'].includes(String(flags.miss)))
|
|
300
|
-
throw Error('--miss yes|no required');
|
|
301
|
-
const bytes = learningFile(resolve(cwd, word(flags.evidence, '--evidence')), 65536);
|
|
302
|
-
return confirm({ author, note, incident: id, miss: flags.miss, evidenceBytes: bytes.length }, () => {
|
|
303
|
-
const evidence = retainArchiveSource(config.archiveRoot, { sourceId: `independent-label-${learningHash({ author, note, bytes: bytes.toString('base64') }).slice(0, 24)}`, parser: { id: 'operator-reference-evidence', version: '1' }, retention: 'exact', bytes }).manifestId;
|
|
304
|
-
const outcome = { kind: 'unflagged', targetId: id, value: flags.miss === 'yes', evidenceManifestId: evidence, referenceId: learningHash({ author, note, id, evidence }) };
|
|
305
|
-
trust.outcome(outcome, [trustOutcomeDigest(outcome)], author);
|
|
306
|
-
return trust.inspect(Date.now());
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
throw Error('unknown learning trust operation');
|
|
310
|
-
}
|
|
311
|
-
if (command === 'adoption') {
|
|
312
|
-
const v = workspace.comparison(word(name, 'comparison name'));
|
|
313
|
-
return emit({ scope: v.scopeStatement, quality: v.qualityScope, decision: v.decision, configured: v.adoptionConfigured, activation: v.activation, rollback: v.rollback,
|
|
314
|
-
reason: !v.adoptionConfigured ? 'independent scoped candidate/eligibility/authority not configured' : v.activation === 'recorded' ? 'activation receipt linked; existing sessions unchanged' : 'prepared/intent is not activation; use original producer registry controls', next: 'Use /grants learning in interactive Pi for authorized next-order activation/rollback.' });
|
|
315
|
-
}
|
|
316
|
-
if (command === 'outcome') {
|
|
317
|
-
word(name, 'comparison name');
|
|
318
|
-
if (flags.result === undefined)
|
|
319
|
-
return emit({ outcomes: workspace.comparison(name).outcomes, meaning: 'observed linkage only; no later outcome is inferred from exit zero', next: 'Record with learning outcome --result … and explicit independently reviewed evidence, or connected producer controls.' });
|
|
320
|
-
const status = workspace.adoptionStatus(name);
|
|
321
|
-
if (!status.receipt || !status.activationManifestId)
|
|
322
|
-
throw Error('an actual linked registry activation is required before later outcomes');
|
|
323
|
-
const author = word(flags['reference-author'], '--reference-author'), note = word(flags.note, '--note'), result = word(flags.result, '--result');
|
|
324
|
-
if (!['success', 'confirmed-defect', 'unknown'].includes(result))
|
|
325
|
-
throw Error('explicit success, confirmed-defect or unknown result required');
|
|
326
|
-
if ((flags['accepted-artifact'] === undefined) !== (flags['acceptance-evidence'] === undefined))
|
|
327
|
-
throw Error('accepted artifact and independent acceptance evidence must be supplied together; excerpts never imply acceptance');
|
|
328
|
-
const keys = ['artifact', 'original-requirement', 'current-requirement', 'evidence', ...(flags['accepted-artifact'] === undefined ? [] : ['accepted-artifact', 'acceptance-evidence'])];
|
|
329
|
-
const files = keys.map(key => ({ key, bytes: learningFile(resolve(cwd, word(flags[key], `--${key}`)), 1024 * 1024) }));
|
|
330
|
-
return confirm({ name, result, author, note, files: files.map(f => ({ kind: f.key, bytes: f.bytes.length })), meaning: 'independently supplied observation, not detector calibration or model exit status' }, () => {
|
|
331
|
-
const retained = new Map(files.map(f => [f.key, retainArchiveSource(config.archiveRoot, { sourceId: `outcome-${f.key}-${learningHash(f.bytes.toString('base64')).slice(0, 24)}`, parser: { id: 'operator-outcome-evidence', version: '1' }, retention: 'exact', bytes: f.bytes })]));
|
|
332
|
-
const audit = retainArchiveSource(config.archiveRoot, { sourceId: `outcome-reference-${learningHash({ author, note, result }).slice(0, 24)}`, parser: { id: 'operator-outcome-reference', version: '1' }, retention: 'exact', bytes: Buffer.from(JSON.stringify({ author, note, result, scopeDigest: config.scopeDigest, source: 'explicit-operator-review-not-exit-status' })) });
|
|
333
|
-
const observation = { id: learningHash({ author, note, result, files: [...retained.values()].map(r => r.manifestId) }), adoptionId: status.receipt.id, candidateDigest: status.receipt.candidateDigest, scopeDigest: config.scopeDigest,
|
|
334
|
-
originalRequirementDigest: retained.get('original-requirement').reference.sha256, currentRequirementDigest: retained.get('current-requirement').reference.sha256, outcome: result,
|
|
335
|
-
observedArtifactDigest: retained.get('artifact').reference.sha256, acceptedArtifactDigest: retained.get('accepted-artifact')?.reference.sha256 ?? null, acceptanceDigest: retained.get('acceptance-evidence')?.reference.sha256 ?? null, evidence: [...retained.values()].map(r => r.manifestId).concat(audit.manifestId) };
|
|
336
|
-
const preview = workspace.previewObservation(name, observation);
|
|
337
|
-
return workspace.observe(name, observation, [preview.digest], author);
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
function learningArchiveScopes(root, author) {
|
|
342
|
-
if (!existsSync(join(root, 'manifests')))
|
|
343
|
-
return [];
|
|
344
|
-
const selected = catalogLearningArchive(root, author).filter((c) => !!c.scopeDigest && !!c.population);
|
|
345
|
-
return selected.filter((c, i) => selected.findIndex(s => s.scopeDigest === c.scopeDigest && s.population === c.population) === i);
|
|
346
|
-
}
|
|
347
|
-
function terminalLearningUI() {
|
|
348
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
349
|
-
throw Error('guided learning needs an interactive terminal; use explicit learning subcommands');
|
|
350
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
351
|
-
return { close: () => rl.close(), notify: text => console.log(learningDisplay(text)),
|
|
352
|
-
input: async (title, initial) => { const answer = await rl.question(`${title}${initial ? ` [${initial}]` : ''}: `); return answer || initial; },
|
|
353
|
-
select: async (title, choices) => { console.log(learningDisplay(title)); choices.forEach((c, i) => console.log(`${i + 1}. ${learningDisplay(c)}`)); const answer = await rl.question('Number (empty cancels): '); return choices[Number(answer) - 1]; },
|
|
354
|
-
confirm: async (title, detail) => (console.log(learningDisplay(`${title}\n${detail}`)), (await rl.question('Confirm [y/N]: ')).toLowerCase() === 'y'),
|
|
355
|
-
editor: async (title, text) => { console.log(learningDisplay(`${title}\n${text}`)); await rl.question('Enter to return (this is the complete display): '); return text; } };
|
|
356
|
-
}
|
|
357
|
-
export async function reviewLearningComparison(directory, name, ui) {
|
|
358
|
-
const workspace = openLearningWorkspace(directory);
|
|
359
|
-
let view = workspace.comparison(name);
|
|
360
|
-
ui.notify(`${view.title}\n${view.scopeStatement}\nDeliberate review, not an earned automatic question.\n${view.limitations.join('\n')}`);
|
|
361
|
-
const reviewed = new Set();
|
|
362
|
-
for (;;) {
|
|
363
|
-
view = workspace.comparison(name);
|
|
364
|
-
const choices = [...view.cards.map(c => `Read variant ${c.displayLabel} (${c.artifactDigests.length} artifact(s))`),
|
|
365
|
-
...(view.revealReady ? ['Reveal model and cost'] : ['Record quality choice']), 'Adopt / reject / defer', 'Done'];
|
|
366
|
-
const selected = await ui.select('Comparison — quality is separate from adoption', choices);
|
|
367
|
-
if (!selected || selected === 'Done')
|
|
368
|
-
return;
|
|
369
|
-
const index = choices.indexOf(selected);
|
|
370
|
-
if (index < view.cards.length) {
|
|
371
|
-
const card = view.cards[index];
|
|
372
|
-
for (const digest of card.artifactDigests) {
|
|
373
|
-
const bytes = workspace.artifact(name, card.label, digest);
|
|
374
|
-
let text;
|
|
375
|
-
try {
|
|
376
|
-
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
ui.notify('This artifact is not UTF-8 text; use a suitable full-artifact viewer. No review acknowledgement recorded.', 'warning');
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
if (bytes.length > 1024 * 1024) {
|
|
383
|
-
ui.notify('Artifact exceeds interactive viewer bound. Use learning artifact in a terminal; no partial view counts as full review.', 'warning');
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
const display = learningDisplay(text), returned = await ui.editor(`Variant ${card.displayLabel} — complete artifact (${bytes.length} bytes; controls escaped)`, display);
|
|
387
|
-
if (returned === display && await ui.confirm('Acknowledge complete artifact review?', 'Only confirm if you reviewed the complete artifact, not an excerpt. This does not accept or adopt it.'))
|
|
388
|
-
reviewed.add(digest);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
else if (selected === 'Record quality choice') {
|
|
392
|
-
const all = [...new Set(view.cards.flatMap(c => c.artifactDigests))], complete = all.length > 0 && all.every(id => reviewed.has(id));
|
|
393
|
-
const answers = [...(complete ? [...view.cards.map(c => `Prefer ${c.displayLabel}`), ...(view.cards.length > 1 ? ['Tie (all shown variants)'] : []), 'None acceptable'] : []), 'Insufficient evidence', 'Cancel'];
|
|
394
|
-
const answer = await ui.select(complete ? 'Quality — full artifacts' : 'Full artifacts not yet acknowledged; only insufficient evidence is available', answers);
|
|
395
|
-
if (!answer || answer === 'Cancel')
|
|
396
|
-
continue;
|
|
397
|
-
const choice = answer === 'Insufficient evidence' ? { kind: 'insufficient', labels: [] } : answer === 'None acceptable' ? { kind: 'none', labels: [] } : answer.startsWith('Tie') ? { kind: 'tie', labels: view.cards.map(c => c.label) } : { kind: 'one', labels: [view.cards.find(c => `Prefer ${c.displayLabel}` === answer).label] };
|
|
398
|
-
const note = await ui.input('Reason / limitations (required)');
|
|
399
|
-
if (!note)
|
|
400
|
-
continue;
|
|
401
|
-
if (await ui.confirm('Record immutable quality choice?', 'This locks the quality choice before reveal; it is not acceptance, calibration, or adoption.')) {
|
|
402
|
-
workspace.choose(name, choice, { scope: 'full-artifacts', note, reviewedArtifactDigests: [...reviewed] });
|
|
403
|
-
ui.notify('Quality choice durably recorded. Identity remains hidden until you choose Reveal.');
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
else if (selected === 'Reveal model and cost')
|
|
407
|
-
ui.notify(JSON.stringify(workspace.reveal(name), null, 2));
|
|
408
|
-
else if (selected === 'Adopt / reject / defer') {
|
|
409
|
-
const disposition = await ui.select('Adoption decision (not activation)', [...(view.revealReady && ['one', 'tie'].includes(view.choice.kind) ? ['adopt'] : []), 'reject', 'defer']);
|
|
410
|
-
if (!disposition)
|
|
411
|
-
continue;
|
|
412
|
-
const note = await ui.input('Decision reason (required)');
|
|
413
|
-
if (!note)
|
|
414
|
-
continue;
|
|
415
|
-
if (await ui.confirm(`Record ${disposition}?`, `${view.scopeStatement}\nNo active session or next order changes here.`)) {
|
|
416
|
-
workspace.decide(name, { disposition: disposition, note, priorDecisionId: view.decision?.id ?? null });
|
|
417
|
-
ui.notify('Decision recorded. Adoption still needs independent eligibility, authority and original registry activation.');
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
export async function runLearningWizard(options) {
|
|
423
|
-
const { directory, ui, cwd } = options;
|
|
424
|
-
if (!existsSync(directory)) {
|
|
425
|
-
ui.notify('Connect an explicitly selected retained archive. This reads no live/private Pi session and calls no model.');
|
|
426
|
-
const archive = await ui.input('Retained archive directory'), author = await ui.input('Review author');
|
|
427
|
-
if (!archive || !author)
|
|
428
|
-
return;
|
|
429
|
-
const archiveRoot = resolve(cwd, archive), scopes = learningArchiveScopes(archiveRoot, author), labels = scopes.map((s, i) => `${i + 1}. ${s.description} (${s.population})`);
|
|
430
|
-
const choice = scopes.length ? await ui.select('Exact retained work scope', labels) : undefined;
|
|
431
|
-
if (scopes.length && !choice)
|
|
432
|
-
return;
|
|
433
|
-
const selected = choice ? scopes[labels.indexOf(choice)] : null;
|
|
434
|
-
const population = selected?.population ?? await ui.input('Comparison-only population (no case scope available)'), scope = selected ? 'Retained case snapshot' : await ui.input('Comparison-only scope description');
|
|
435
|
-
if (!population || !scope)
|
|
436
|
-
return;
|
|
437
|
-
if (!await ui.confirm('Create learning workspace?', `${archiveRoot}\nPopulation: ${population}\nScope: ${scope}\nAuthor: ${author}\nFor an existing producer host use /grants learning so the exact snapshot is bound.`))
|
|
438
|
-
return;
|
|
439
|
-
createLearningWorkspace(directory, { archiveRoot, population, scopeDigest: selected?.scopeDigest ?? learningHash({ declaredScope: scope }), author });
|
|
440
|
-
}
|
|
441
|
-
const workspace = openLearningWorkspace(directory);
|
|
442
|
-
for (;;) {
|
|
443
|
-
const status = workspace.inspect(Date.now());
|
|
444
|
-
const selected = await ui.select('Learning — retained evidence, no model calls', ['Readiness', 'Connect retained input', 'Review cases', 'Review comparisons', 'Record adopt / reject / defer', 'Propose hypothesis', 'Link original hypothesis', 'Trust / independent labels', 'Adoption / outcomes', 'Done']);
|
|
445
|
-
if (!selected || selected === 'Done')
|
|
446
|
-
return;
|
|
447
|
-
try {
|
|
448
|
-
if (selected === 'Readiness')
|
|
449
|
-
ui.notify(formatLearningStatus(status));
|
|
450
|
-
if (selected === 'Connect retained input') {
|
|
451
|
-
const catalog = catalogLearningArchive(status.configuration.archiveRoot, status.configuration.author), labels = catalog.map((c, i) => `${i + 1}. ${c.kind}: ${c.description}`);
|
|
452
|
-
if (!labels.length) {
|
|
453
|
-
ui.notify('No compatible retained case batch or qualified comparison for this author. Run ordinary work first; no evidence will be fabricated.', 'warning');
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
const item = await ui.select('Retained inputs in selected archive', labels);
|
|
457
|
-
if (!item)
|
|
458
|
-
continue;
|
|
459
|
-
const c = catalog[labels.indexOf(item)], name = await ui.input('Short name (letters, digits, - or _)'), title = await ui.input('Display title');
|
|
460
|
-
if (!name || !title)
|
|
461
|
-
continue;
|
|
462
|
-
const scope = c.kind === 'comparison' ? await ui.input('What complete artifacts will this quality review assess?') : undefined;
|
|
463
|
-
if (c.kind === 'comparison' && !scope)
|
|
464
|
-
continue;
|
|
465
|
-
if (!await ui.confirm('Connect retained input?', `${title}\n${scope ?? 'Case nominations remain unconfirmed.'}`))
|
|
466
|
-
continue;
|
|
467
|
-
if (c.kind === 'cases')
|
|
468
|
-
workspace.bindCases({ name, title, version: c.version, batchId: c.manifestId });
|
|
469
|
-
else
|
|
470
|
-
workspace.bindComparison({ name, title, scopeStatement: scope, comparisonManifestId: c.manifestId, caseReference: null, hypothesisManifestId: null, priorFeedbackManifestIds: [], adoptionBinding: null });
|
|
471
|
-
ui.notify('Retained input connected. Reading never activates or reserves attention.');
|
|
472
|
-
}
|
|
473
|
-
if (selected === 'Review comparisons') {
|
|
474
|
-
const names = status.comparisons.map(c => c.name), name = await ui.select('Choose retained comparison', names);
|
|
475
|
-
if (name)
|
|
476
|
-
await reviewLearningComparison(directory, name, ui);
|
|
477
|
-
}
|
|
478
|
-
if (selected === 'Review cases') {
|
|
479
|
-
const name = await ui.select('Choose case batch', status.cases.map(c => c.name));
|
|
480
|
-
if (!name)
|
|
481
|
-
continue;
|
|
482
|
-
const p = allCases(workspace, name), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.reason} — ${c.candidate.target.obligationId} (${c.disposition})`);
|
|
483
|
-
const chosen = await ui.select('Retained nominations (not confirmed defects)', labels);
|
|
484
|
-
if (!chosen)
|
|
485
|
-
continue;
|
|
486
|
-
const item = p.items[labels.indexOf(chosen)];
|
|
487
|
-
await ui.editor('Case nomination — not an independent label', learningDisplay(JSON.stringify(item.candidate, null, 2)));
|
|
488
|
-
const refs = workspace.caseEvidence(name, item.caseManifestId), choices = refs.map((r, i) => `${i + 1}. ${r.role}: ${r.status === 'available' ? `${r.reference.parser.id} (${r.reference.retention})` : r.status}`);
|
|
489
|
-
let cancelled = false;
|
|
490
|
-
if (refs.length === 1)
|
|
491
|
-
ui.notify('Only nomination retained; no original observation link. This alone is not independent proof.', 'warning');
|
|
492
|
-
for (;;) {
|
|
493
|
-
const selected = await ui.select('Inspect retained case evidence (no bytes are edited)', [...choices, 'Choose disposition', 'Back']);
|
|
494
|
-
if (!selected || selected === 'Back') {
|
|
495
|
-
cancelled = true;
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
if (selected === 'Choose disposition')
|
|
499
|
-
break;
|
|
500
|
-
const ref = refs[choices.indexOf(selected)], result = workspace.readCaseEvidence(name, item.caseManifestId, ref.manifestId);
|
|
501
|
-
if (result.status !== 'available') {
|
|
502
|
-
ui.notify(`Evidence unavailable: ${result.status}`, 'warning');
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
await ui.editor(`${result.reference.retention === 'exact' ? 'Exact' : 'Redacted — not full'} retained source (display only)`, artifactText(result.bytes));
|
|
506
|
-
}
|
|
507
|
-
if (cancelled)
|
|
508
|
-
continue;
|
|
509
|
-
const disposition = await ui.select('Your independent case disposition', ['confirmed_defect', 'expected_behavior', 'exemplar', 'uncertain', 'skip']);
|
|
510
|
-
if (!disposition)
|
|
511
|
-
continue;
|
|
512
|
-
const note = await ui.input('Evidence/reason (required)');
|
|
513
|
-
if (note && await ui.confirm('Record case disposition?', `${disposition}\n${note}\nThis is not a writing preference, adoption or test promotion.`)) {
|
|
514
|
-
workspace.decideCase(name, { caseManifestId: item.caseManifestId, priorDecisionId: item.priorDecisionId, disposition: disposition, note });
|
|
515
|
-
ui.notify('Case disposition durably recorded. Trust linkage is a separate action.');
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
if (selected === 'Record adopt / reject / defer') {
|
|
519
|
-
const name = await ui.select('Retained comparison (defer/reject also works when evidence is unavailable)', status.comparisons.map(c => c.name));
|
|
520
|
-
if (!name)
|
|
521
|
-
continue;
|
|
522
|
-
const current = workspace.decisionStatus(name);
|
|
523
|
-
let adopt = false;
|
|
524
|
-
try {
|
|
525
|
-
const c = workspace.comparison(name);
|
|
526
|
-
adopt = c.revealReady && !!c.choice && ['one', 'tie'].includes(c.choice.kind);
|
|
527
|
-
}
|
|
528
|
-
catch { /* Missing evidence cannot prevent recording a defer. */ }
|
|
529
|
-
const disposition = await ui.select(current.scopeStatement, [...(adopt ? ['adopt'] : []), 'reject', 'defer']), note = await ui.input('Reason (required)');
|
|
530
|
-
if (!disposition || !note)
|
|
531
|
-
continue;
|
|
532
|
-
if (await ui.confirm(`Record ${disposition}?`, 'Intent only. No acceptance, activation, model calls or automatic exposure.')) {
|
|
533
|
-
workspace.decide(name, { disposition: disposition, note, priorDecisionId: current.current?.id ?? null });
|
|
534
|
-
ui.notify('Decision durably recorded.');
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (selected === 'Propose hypothesis') {
|
|
538
|
-
const name = await ui.select('Confirmed case batch', status.cases.map(c => c.name));
|
|
539
|
-
if (!name)
|
|
540
|
-
continue;
|
|
541
|
-
const flags = [];
|
|
542
|
-
for (const [key, title] of [['name', 'Hypothesis short name'], ['intervention', 'Proposed change'], ['prediction', 'Falsifiable expected result'], ['disproof', 'What would disprove it?'], ['downside', 'Downside/risk'], ['rollback', 'Rollback plan'], ['alternative', 'Alternative explanation']]) {
|
|
543
|
-
const value = await ui.input(title);
|
|
544
|
-
if (!value)
|
|
545
|
-
break;
|
|
546
|
-
flags.push(`--${key}`, value);
|
|
547
|
-
}
|
|
548
|
-
if (flags.length !== 14)
|
|
549
|
-
continue;
|
|
550
|
-
if (await ui.confirm('Retain hypothesis?', 'Proposal only: no scenario edit, model call, adoption or causal claim.'))
|
|
551
|
-
await runLearningCommand(['hypothesis', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
552
|
-
}
|
|
553
|
-
if (selected === 'Link original hypothesis') {
|
|
554
|
-
const comparison = await ui.select('Comparison', status.comparisons.map(c => c.name)), batch = await ui.select('Confirmed case batch', status.cases.map(c => c.name));
|
|
555
|
-
if (!comparison || !batch)
|
|
556
|
-
continue;
|
|
557
|
-
const p = allCases(workspace, batch), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.target.obligationId} — ${c.disposition}`), item = await ui.select('Current independent case decision', labels), hypothesis = await ui.select('Original retained hypothesis', workspace.hypotheses().map(h => h.name));
|
|
558
|
-
if (!item || !hypothesis)
|
|
559
|
-
continue;
|
|
560
|
-
if (await ui.confirm('Link original evidence?', 'Only the comparison’s actual originating hypothesis is accepted. A new hypothesis cannot be retroactively presented as the experiment origin.'))
|
|
561
|
-
await runLearningCommand(['link', comparison, '--cases', batch, '--item', String(labels.indexOf(item) + 1), '--hypothesis', hypothesis, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
562
|
-
}
|
|
563
|
-
if (selected === 'Trust / independent labels')
|
|
564
|
-
await trustLearningWizard(directory, cwd, ui);
|
|
565
|
-
if (selected === 'Adoption / outcomes') {
|
|
566
|
-
const name = await ui.select('Comparison', status.comparisons.map(c => c.name));
|
|
567
|
-
if (!name)
|
|
568
|
-
continue;
|
|
569
|
-
await runLearningCommand(['adoption', name, '--state', directory], { cwd, write: t => ui.notify(t) });
|
|
570
|
-
await runLearningCommand(['outcome', name, '--state', directory], { cwd, write: t => ui.notify(t) });
|
|
571
|
-
if (await ui.confirm('Record a later independent observation?', 'Requires an actual linked activation. Unknown is valid; exit zero is not quality. No adoption or rollback is performed.')) {
|
|
572
|
-
const result = await ui.select('Observed result', ['success', 'confirmed-defect', 'unknown']);
|
|
573
|
-
if (!result)
|
|
574
|
-
continue;
|
|
575
|
-
const flags = ['--result', result];
|
|
576
|
-
for (const [key, title] of [['artifact', 'Complete later artifact file'], ['original-requirement', 'Original requirement file'], ['current-requirement', 'Current requirement file'], ['evidence', 'Independent outcome evidence file'], ['reference-author', 'Independent reference author'], ['note', 'Evidence/reason']]) {
|
|
577
|
-
const value = await ui.input(title);
|
|
578
|
-
if (!value)
|
|
579
|
-
break;
|
|
580
|
-
flags.push(`--${key}`, value);
|
|
581
|
-
}
|
|
582
|
-
if (flags.length !== 14)
|
|
583
|
-
continue;
|
|
584
|
-
if (await ui.confirm('Was this exact artifact independently accepted?', 'No is normal. Never substitute an excerpt preference for full artifact acceptance.')) {
|
|
585
|
-
const artifact = await ui.input('Exact independently accepted artifact file'), evidence = await ui.input('Independent acceptance evidence file');
|
|
586
|
-
if (!artifact || !evidence)
|
|
587
|
-
continue;
|
|
588
|
-
flags.push('--accepted-artifact', artifact, '--acceptance-evidence', evidence);
|
|
589
|
-
}
|
|
590
|
-
if (await ui.confirm('Retain this independent outcome?', 'Links only to the scoped adoption. It does not become calibration, improvement proof or automatic rollback.'))
|
|
591
|
-
await runLearningCommand(['outcome', name, ...flags, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
catch (e) {
|
|
596
|
-
ui.notify(e instanceof Error ? e.message : 'Learning action failed; no success claimed.', 'error');
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
async function trustLearningWizard(directory, cwd, ui) {
|
|
601
|
-
const workspace = openLearningWorkspace(directory), action = await ui.select('Trust — independent evidence, never preference', ['Readiness', 'Add unflagged evidence before freeze', 'Configure frozen policy', 'Link current case label', 'Label frozen unflagged sample', 'Back']);
|
|
602
|
-
if (!action || action === 'Back')
|
|
603
|
-
return;
|
|
604
|
-
if (action === 'Readiness') {
|
|
605
|
-
ui.notify(formatLearningTrust(workspace.inspect(Date.now()).trust));
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (action === 'Add unflagged evidence before freeze') {
|
|
609
|
-
const file = await ui.input('Explicit retained/source evidence file (no private sessions)'), name = await ui.input('Incident short name'), title = await ui.input('What unflagged incident does this evidence document?'), split = await ui.select('Predeclare split', ['calibration', 'heldout', 'tuning']);
|
|
610
|
-
if (file && name && title && split && await ui.confirm('Retain unflagged incident?', 'This is not a correctness label. Do not include secrets or private sessions.'))
|
|
611
|
-
await runLearningCommand(['trust', 'sample', file, '--name', name, '--title', title, '--split', split, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
|
-
if (action === 'Configure frozen policy') {
|
|
615
|
-
const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
|
|
616
|
-
if (!name)
|
|
617
|
-
return;
|
|
618
|
-
const detectors = [...new Set(allCases(workspace, name).items.map(c => c.candidate.detector.id))], detector = await ui.select('Detector (version and population remain exact)', detectors), split = await ui.select('Predeclare evaluation split', ['calibration', 'heldout', 'tuning']);
|
|
619
|
-
if (!detector || !split)
|
|
620
|
-
return;
|
|
621
|
-
const max = await ui.input('Maximum unflagged sample (0–32; missing samples stay missing)', '0');
|
|
622
|
-
if (max === undefined)
|
|
623
|
-
return;
|
|
624
|
-
const flags = { cases: name, detector, split, 'max-unflagged': max };
|
|
625
|
-
if (await ui.confirm('Configure automatic questions?', 'Default is silent. Explicit policy plus sufficient independent labels is required. This does not show a question or refill attention.')) {
|
|
626
|
-
const count = await ui.input('Minimum resolved independent incidents', '10'), lower = await ui.input('Minimum 95% interval lower bound', '0.8'), quota = await ui.input('Total question attention budget', '5'), days = await ui.input('Policy validity in days', '7');
|
|
627
|
-
if (!count || !lower || !quota || !days)
|
|
628
|
-
return;
|
|
629
|
-
Object.assign(flags, { 'minimum-resolved': count, 'minimum-lower-bound': lower, attention: quota, expires: days });
|
|
630
|
-
}
|
|
631
|
-
const setup = trustSetup(workspace, flags), preview = previewLearningTrust(directory, setup);
|
|
632
|
-
if (await ui.confirm('Freeze this exact scoped policy?', JSON.stringify({ ...preview, notice: 'No independent labels are created. Missing evidence keeps automatic questions silent.' }, null, 2))) {
|
|
633
|
-
configureLearningTrust(directory, setup, [preview.policyDigest]);
|
|
634
|
-
ui.notify('Policy and predictions retained; independent labels still pending.');
|
|
635
|
-
}
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
if (action === 'Link current case label') {
|
|
639
|
-
const name = await ui.select('Case batch', workspace.inspect(0).cases.map(c => c.name));
|
|
640
|
-
if (!name)
|
|
641
|
-
return;
|
|
642
|
-
const p = allCases(workspace, name), labels = p.items.map((c, i) => `${i + 1}. ${c.candidate.reason}: ${c.disposition}`), chosen = await ui.select('Current independently reviewed case', labels), author = await ui.input('Independent reference author');
|
|
643
|
-
if (!chosen || !author)
|
|
644
|
-
return;
|
|
645
|
-
if (await ui.confirm('Link independent correctness label?', 'Use the actual case disposition; never treat writing preference, panel agreement or model self-report as detector accuracy.'))
|
|
646
|
-
await runLearningCommand(['trust', 'label', '--cases', name, '--item', String(labels.indexOf(chosen) + 1), '--reference-author', author, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const trust = workspace.trust(), view = trust.inspect(Date.now()), labels = view.sampledIncidentIds.map((id, i) => `${i + 1}. ${id.slice(0, 12)} (frozen unflagged incident)`), chosen = await ui.select('Frozen sample', labels);
|
|
650
|
-
if (!chosen)
|
|
651
|
-
return;
|
|
652
|
-
const miss = await ui.select('Independent review: was a defect missed?', ['yes', 'no']), file = await ui.input('Independent evidence file'), author = await ui.input('Independent reference author'), note = await ui.input('Evidence/reason');
|
|
653
|
-
if (miss && file && author && note && await ui.confirm('Record independent sample outcome?', 'This label is scope-bound and separate from positive precision.'))
|
|
654
|
-
await runLearningCommand(['trust', 'unflagged', '--item', String(labels.indexOf(chosen) + 1), '--miss', miss, '--evidence', file, '--reference-author', author, '--note', note, '--state', directory, '--confirm'], { cwd, write: t => ui.notify(t) });
|
|
655
|
-
}
|
|
656
|
-
//# sourceMappingURL=learning.js.map
|