@skill-harness/adapters 0.15.0 → 0.17.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.
@@ -0,0 +1,705 @@
1
+ import { readRetainedSession, readRetainedSessionSource } from './session-retention.js';
2
+ import { mkdirSync, readdirSync, lstatSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { buildHypothesis, buildAdoptionBinding, authorizeAdoption, authorizeRollback, buildRollbackRequest, classifyProductionObservation, } from '@skill-harness/core';
5
+ import { learningCopy, learningHash, learningJournal, registerLearningStore, verifyLearningStore } from './learning-journal.js';
6
+ import { readArchiveSource, readArchiveSourceReference, retainArchiveSource } from './evidence-archive.js';
7
+ import { createWorkCaseReviewer, createWorkSignalReviewer } from './work-case-review.js';
8
+ import { readLearningCase } from './learning-case.js';
9
+ import { readWorkCandidate } from './work-case-archive.js';
10
+ import { readWorkSignalCase } from './work-signal-cases.js';
11
+ import { openBlindIntervention, readBlindInterventionBinding } from './blind-intervention.js';
12
+ import { openTrustLifecycle, createTrustLifecycle, trustPolicyDigest } from './trust-lifecycle.js';
13
+ import { retainLearningLifecycle } from './learning-lifecycle.js';
14
+ const SHA = /^[a-f0-9]{64}$/;
15
+ const NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
16
+ function closed(value, keys) {
17
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join() !== [...keys].sort().join())
18
+ throw Error('closed learning workspace input required');
19
+ }
20
+ function text(value, max = 4000) {
21
+ if (typeof value !== 'string' || !value.trim() || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value))
22
+ throw Error('bounded learning text required');
23
+ }
24
+ function hash(value) { if (typeof value !== 'string' || !SHA.test(value))
25
+ throw Error('retained digest required'); }
26
+ function path(value) { if (!isAbsolute(value) || resolve(value) !== value)
27
+ throw Error('canonical absolute learning path required'); }
28
+ function named(value) { if (!NAME.test(value.name))
29
+ throw Error('learning name must be a short slug'); text(value.title, 512); }
30
+ function clock(now) { if (!Number.isSafeInteger(now) || now < 0)
31
+ throw Error('valid learning clock required'); }
32
+ const message = (error) => error instanceof Error ? error.message : 'learning evidence unavailable';
33
+ function source(root, id, parser) {
34
+ hash(id);
35
+ const r = readArchiveSource(root, id);
36
+ if (r.status !== 'available' || r.reference.retention !== 'exact' || parser && (r.reference.parser.id !== parser || r.reference.parser.version !== '1'))
37
+ throw Error('retained learning evidence missing, changed or unsupported');
38
+ return r;
39
+ }
40
+ function jsonSource(root, id, parser) {
41
+ return learningCopy(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(source(root, id, parser).bytes)));
42
+ }
43
+ function retain(root, parser, value) {
44
+ return retainArchiveSource(root, { sourceId: `${parser}-${learningHash(value).slice(0, 32)}`, parser: { id: parser, version: '1' }, retention: 'exact', bytes: Buffer.from(JSON.stringify(value)) }).manifestId;
45
+ }
46
+ /** Creates only explicitly selected private local directories; never follows a symlink. */
47
+ export function prepareLearningDirectory(directory) {
48
+ path(directory);
49
+ try {
50
+ const s = lstatSync(directory);
51
+ if (!s.isDirectory() || s.isSymbolicLink())
52
+ throw Error('learning directory substitution');
53
+ }
54
+ catch (e) {
55
+ if (e.code !== 'ENOENT')
56
+ throw e;
57
+ prepareLearningDirectory(dirname(directory));
58
+ mkdirSync(directory, { mode: 0o700 });
59
+ }
60
+ for (let p = directory;; p = dirname(p)) {
61
+ const s = lstatSync(p);
62
+ if (!s.isDirectory() || s.isSymbolicLink())
63
+ throw Error('learning directory ancestor substitution');
64
+ if (p === dirname(p))
65
+ break;
66
+ }
67
+ }
68
+ function validInput(input) {
69
+ closed(input, ['archiveRoot', 'scopeDigest', 'population', 'author']);
70
+ path(input.archiveRoot);
71
+ hash(input.scopeDigest);
72
+ text(input.population, 512);
73
+ text(input.author, 512);
74
+ if (/[\u0000-\u001f\u007f]/.test(input.population + input.author))
75
+ throw Error('single-line population and explicit author required');
76
+ }
77
+ const workspaceKey = (input) => learningHash({ scopeDigest: input.scopeDigest, population: input.population, author: input.author });
78
+ /** Composition over the existing archive and CAS journals. No worker callbacks, model calls,
79
+ * exposure, registry mutation, authentication or hostile-same-UID protection. */
80
+ export function createLearningWorkspace(directory, input) {
81
+ const safe = learningCopy(input);
82
+ validInput(safe);
83
+ path(directory);
84
+ prepareLearningDirectory(dirname(directory));
85
+ prepareLearningDirectory(safe.archiveRoot);
86
+ const initial = { type: 'learning-workspace-v1', input: safe };
87
+ let exists = true;
88
+ try {
89
+ lstatSync(directory);
90
+ }
91
+ catch (e) {
92
+ if (e.code !== 'ENOENT')
93
+ throw e;
94
+ exists = false;
95
+ }
96
+ if (exists) {
97
+ if (learningHash(learningJournal(directory).read()[0].value) !== learningHash(initial))
98
+ throw Error('learning workspace already bound');
99
+ return openLearningWorkspace(directory);
100
+ }
101
+ registerLearningStore(safe.archiveRoot, 'workspace', workspaceKey(safe), directory, learningHash(initial));
102
+ try {
103
+ learningJournal(directory, initial);
104
+ }
105
+ catch (e) {
106
+ if (e.code !== 'EEXIST')
107
+ throw e;
108
+ if (learningHash(learningJournal(directory).read()[0].value) !== learningHash(initial))
109
+ throw Error('learning workspace already bound');
110
+ }
111
+ return openLearningWorkspace(directory);
112
+ }
113
+ export function openLearningWorkspace(directory) {
114
+ path(directory);
115
+ const journal = learningJournal(directory), initial = journal.read()[0].value;
116
+ closed(initial, ['type', 'input']);
117
+ if (initial.type !== 'learning-workspace-v1')
118
+ throw Error('wrong learning workspace');
119
+ const input = initial.input;
120
+ validInput(input);
121
+ const history = () => {
122
+ verifyLearningStore(input.archiveRoot, 'workspace', workspaceKey(input), directory, learningHash(initial));
123
+ const rows = journal.read(), names = new Set(), decisions = new Map();
124
+ const known = new Set(['session-note', 'session-binding', 'case-binding', 'comparison-binding', 'comparison-context', 'hypothesis-binding', 'unflagged-binding', 'trust-binding', 'quality-pending', 'quality', 'decision', 'adoption', 'activation', 'rollback-request', 'rollback', 'outcome', 'lifecycle']);
125
+ for (const row of rows.slice(1)) {
126
+ const v = row.value;
127
+ if (!known.has(String(v.type)))
128
+ throw Error('unknown learning workspace event');
129
+ if (String(v.type).endsWith('-binding') && v.type !== 'trust-binding') {
130
+ const b = v.binding;
131
+ if (!b || !NAME.test(b.name) || names.has(`${v.type}:${b.name}`))
132
+ throw Error('duplicate or invalid learning binding');
133
+ names.add(`${v.type}:${b.name}`);
134
+ }
135
+ if (v.type === 'decision') {
136
+ const r = v.request;
137
+ if (!r || r.priorDecisionId !== (decisions.get(String(v.name)) ?? null))
138
+ throw Error('learning decision history fork');
139
+ decisions.set(String(v.name), row.id);
140
+ }
141
+ }
142
+ return rows;
143
+ };
144
+ const events = () => history().map(e => e.value);
145
+ const append = (value, prior = history().at(-1).id) => journal.append(prior, value);
146
+ const bindings = (type) => events().filter(e => e.type === type).map(e => e.binding);
147
+ const selected = (type, name) => {
148
+ const b = bindings(type).find(b => b.name === name);
149
+ if (!b)
150
+ throw Error(`unknown retained learning name: ${name}`);
151
+ return learningCopy(b);
152
+ };
153
+ const bound = (type, binding) => {
154
+ const before = history().at(-1).id;
155
+ const old = bindings(type).find(b => b.name === binding.name);
156
+ if (old) {
157
+ if (learningHash(old) !== learningHash(binding))
158
+ throw Error('learning name already bound; use a new name for changed evidence');
159
+ return;
160
+ }
161
+ if (bindings(type).length >= 128)
162
+ throw Error('learning binding limit reached');
163
+ append({ type, binding }, before);
164
+ };
165
+ const reviewer = (b) => b.version === 3 ? createWorkSignalReviewer(input.archiveRoot, b.batchId, input.author) : createWorkCaseReviewer(input.archiveRoot, b.batchId, input.author);
166
+ const validateCases = (b) => {
167
+ closed(b, ['name', 'title', 'version', 'batchId']);
168
+ named(b);
169
+ if (![2, 3].includes(b.version))
170
+ throw Error('unsupported case version');
171
+ hash(b.batchId);
172
+ const r = reviewer(b);
173
+ for (let offset = 0;; offset += 5) {
174
+ const p = r.list(offset);
175
+ for (const c of p.items)
176
+ if (c.candidate.detector.population !== input.population || c.candidate.target.snapshotDigest !== input.scopeDigest)
177
+ throw Error('case snapshot scope or population mismatch');
178
+ if (offset + 5 >= p.total)
179
+ break;
180
+ }
181
+ return r;
182
+ };
183
+ const caseEvidenceIds = (name, caseManifestId) => {
184
+ const binding = selected('case-binding', name), r = validateCases(binding);
185
+ for (let offset = 0;; offset += 5) {
186
+ const page = r.list(offset), item = page.items.find(c => c.caseManifestId === caseManifestId);
187
+ if (item) {
188
+ // Candidate.evidence contains byte/protocol digests, NOT archive manifest IDs.
189
+ const observationId = binding.version === 3 ? readWorkSignalCase(input.archiveRoot, caseManifestId).observationId : jsonSource(input.archiveRoot, binding.batchId, 'work-candidate-batch').observationId;
190
+ if (observationId === undefined)
191
+ return [caseManifestId]; // Explicit legacy nomination-only view.
192
+ hash(observationId);
193
+ const observed = readArchiveSourceReference(input.archiveRoot, observationId);
194
+ if (binding.version === 2 && observed.status === 'available' && !item.candidate.evidence.includes(observed.reference.sha256))
195
+ throw Error('observation link is outside case evidence');
196
+ return [caseManifestId, observationId];
197
+ }
198
+ if (offset + 5 >= page.total)
199
+ break;
200
+ }
201
+ throw Error('case outside named batch');
202
+ };
203
+ const hypothesis = (id) => {
204
+ const h = jsonSource(input.archiveRoot, id, 'factory-hypothesis');
205
+ if (learningHash(buildHypothesis(h.proposal)) !== learningHash(h) || h.proposal.population !== input.population)
206
+ throw Error('hypothesis changed or population mismatch');
207
+ return h;
208
+ };
209
+ const validateComparison = (b) => {
210
+ closed(b, ['name', 'title', 'scopeStatement', 'comparisonManifestId', 'caseReference', 'hypothesisManifestId', 'priorFeedbackManifestIds', 'adoptionBinding']);
211
+ named(b);
212
+ text(b.scopeStatement);
213
+ const metadata = readBlindInterventionBinding(input.archiveRoot, b.comparisonManifestId, input.author);
214
+ if (!Array.isArray(b.priorFeedbackManifestIds) || b.priorFeedbackManifestIds.length > 32 || new Set(b.priorFeedbackManifestIds).size !== b.priorFeedbackManifestIds.length)
215
+ throw Error('bounded prior feedback required');
216
+ b.priorFeedbackManifestIds.forEach(id => source(input.archiveRoot, id));
217
+ const c = b.caseReference ? readLearningCase(input.archiveRoot, b.caseReference) : null;
218
+ if (c && (!c.matched || c.candidate.detector.population !== input.population || c.candidate.target.snapshotDigest !== input.scopeDigest))
219
+ throw Error('case decision stale or snapshot scope/population mismatch');
220
+ const h = b.hypothesisManifestId ? hypothesis(b.hypothesisManifestId) : null;
221
+ if (h && c && !h.proposal.caseIds.includes(c.candidate.id))
222
+ throw Error('hypothesis case mismatch');
223
+ if (h && metadata.investigationDigest !== h.id)
224
+ throw Error('comparison belongs to a different retained hypothesis');
225
+ if (b.adoptionBinding) {
226
+ const { version, id, ...draft } = b.adoptionBinding;
227
+ if (version !== 'adoption-binding-v1' || buildAdoptionBinding(draft).id !== id || b.adoptionBinding.scopeDigest !== input.scopeDigest || b.adoptionBinding.experimentDigest !== metadata.experimentDigest || !h || !c || c.current?.disposition !== 'confirmed_defect' || b.adoptionBinding.hypothesisDigest !== h.id)
228
+ throw Error('adoption case/hypothesis/comparison/scope mismatch');
229
+ }
230
+ return openBlindIntervention(input.archiveRoot, b.comparisonManifestId, input.author);
231
+ };
232
+ const comparisonBinding = (name) => {
233
+ const b = selected('comparison-binding', name);
234
+ const context = events().filter(e => e.type === 'comparison-context' && e.name === name).at(-1);
235
+ return context ? { ...b, ...learningCopy(context.context) } : b;
236
+ };
237
+ const last = (type, name) => events().filter(e => e.type === type && e.name === name).at(-1);
238
+ const decision = (name) => {
239
+ const e = history().filter(e => e.value.type === 'decision' && e.value.name === name).at(-1);
240
+ return e ? { id: e.id, ...e.value.request } : null;
241
+ };
242
+ const adoption = (name) => {
243
+ const e = last('adoption', name);
244
+ if (!e)
245
+ throw Error('authorized adoption receipt missing');
246
+ if (learningHash(jsonSource(input.archiveRoot, String(e.manifestId), 'learning-adoption')) !== learningHash(e.receipt))
247
+ throw Error('retained adoption receipt changed or missing');
248
+ return learningCopy(e.receipt);
249
+ };
250
+ const quality = (name) => {
251
+ const b = comparisonBinding(name), blind = validateComparison(b), choice = blind.quality(), receipt = last('quality', name);
252
+ if (receipt) {
253
+ if (learningHash(receipt.choice) !== learningHash(choice))
254
+ throw Error('durable quality choice changed');
255
+ const { manifestId, ...body } = receipt;
256
+ if (learningHash(jsonSource(input.archiveRoot, String(manifestId), 'learning-quality')) !== learningHash(body))
257
+ throw Error('retained quality receipt changed or missing');
258
+ }
259
+ return { b, blind, choice, receipt };
260
+ };
261
+ const requireQuality = (name) => { const q = quality(name); if (!q.choice || !q.receipt)
262
+ throw Error('durable full-artifact-scope quality receipt required; excerpt feedback is not a choice'); return q; };
263
+ const validateTrustScope = (c) => {
264
+ if (c.archiveRoot !== input.archiveRoot || c.component.population !== input.population)
265
+ throw Error('trust archive/population mismatch');
266
+ const incidents = new Set(), unflagged = bindings('unflagged-binding');
267
+ for (const row of c.cohort) {
268
+ if (row.flagged) {
269
+ const p = source(input.archiveRoot, row.manifestId).reference.parser;
270
+ const candidate = p.id === 'work-capture' ? readWorkCandidate(input.archiveRoot, row.manifestId) : p.id === 'work-signal-case' ? readWorkSignalCase(input.archiveRoot, row.manifestId).candidate : null;
271
+ if (!candidate || candidate.target.snapshotDigest !== input.scopeDigest || candidate.detector.population !== input.population || c.component.kind === 'detector' && learningHash(candidate.detector) !== learningHash({ id: c.component.id, version: c.component.version, population: c.component.population }))
272
+ throw Error('trust cohort requires actual same-snapshot scoped cases');
273
+ const key = learningHash(candidate.target);
274
+ if (incidents.has(key))
275
+ throw Error('trust cohort repeats one work incident');
276
+ incidents.add(key);
277
+ }
278
+ else if (!unflagged.some(b => b.manifestId === row.manifestId && b.split === row.split))
279
+ throw Error('unflagged cohort source not explicitly bound to this workspace scope');
280
+ }
281
+ };
282
+ const currentTrust = () => {
283
+ const e = events().filter(e => e.type === 'trust-binding').at(-1);
284
+ if (!e)
285
+ return null;
286
+ const trust = openTrustLifecycle(String(e.directory)), c = trust.configuration();
287
+ validateTrustScope(c);
288
+ if (trustPolicyDigest(c) !== e.policyId)
289
+ throw Error('trust policy mismatch');
290
+ return trust;
291
+ };
292
+ const registryEvidence = (id, operation, name, permitted) => {
293
+ if (!permitted.includes(id))
294
+ throw Error('independent exact registry linkage authority required');
295
+ const r = jsonSource(input.archiveRoot, id, 'learning-registry-receipt');
296
+ closed(r, ['version', 'operation', 'requestId', 'adoptionId', 'scopeDigest', 'candidateDigest', 'revision', 'registryManifestId']);
297
+ const a = adoption(name);
298
+ if (r.version !== 'learning-registry-receipt-v1' || r.operation !== operation || r.adoptionId !== a.id || r.scopeDigest !== input.scopeDigest || !Number.isSafeInteger(r.revision) || r.revision < 1 || r.candidateDigest !== (operation === 'activate' ? a.candidateDigest : a.rollbackCandidateDigest))
299
+ throw Error('registry receipt outside adopted scope');
300
+ text(r.requestId, 128);
301
+ const view = jsonSource(input.archiveRoot, r.registryManifestId);
302
+ if (view.version !== 'factory-registry-view-v1' || view.scopeDigest !== r.scopeDigest || view.candidateDigest !== r.candidateDigest || view.revision !== r.revision)
303
+ throw Error('original registry observation mismatch');
304
+ if (operation === 'activate') {
305
+ const active = view.activation;
306
+ const change = view.lastChange;
307
+ if (view.application !== 'applied' || view.requestId !== r.requestId || change?.operation !== 'activate' || change.requestId !== r.requestId || change.adoptionId !== r.adoptionId || active?.requestId !== r.requestId || active.receipt?.id !== r.adoptionId || learningHash(active.receipt) !== learningHash(a))
308
+ throw Error('activation not applied by original registry for the exact request and adoption');
309
+ }
310
+ if (operation === 'rollback' && (view.application !== 'applied' || view.requestId !== r.requestId || r.requestId !== last('rollback-request', name)?.request?.id))
311
+ throw Error('rollback not applied by original registry');
312
+ return r;
313
+ };
314
+ history();
315
+ return {
316
+ configuration: () => learningCopy(input),
317
+ bindSession(raw) {
318
+ const b = learningCopy(raw);
319
+ closed(b, ['name', 'title', 'manifestId']);
320
+ named(b);
321
+ hash(b.manifestId);
322
+ readRetainedSession(input.archiveRoot, b.manifestId);
323
+ bound('session-binding', b);
324
+ return b;
325
+ },
326
+ noteSession(name, raw) {
327
+ const a = learningCopy(raw);
328
+ closed(a, ['kind', 'note', 'item', 'line']);
329
+ text(a.note);
330
+ if (!['finding', 'proposal', 'acceptance', 'context'].includes(a.kind) || !Number.isSafeInteger(a.item) || a.item < 1 || !Number.isSafeInteger(a.line) || a.line < 1)
331
+ throw Error('valid annotation and evidence reference required');
332
+ const b = selected('session-binding', name);
333
+ const bytes = readRetainedSessionSource(input.archiveRoot, b.manifestId, a.item - 1);
334
+ const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
335
+ const count = content.length === 0 ? 0 : content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
336
+ if (a.line > count)
337
+ throw Error('annotation evidence line outside retained source');
338
+ const annotation = { ...a, author: input.author };
339
+ append({ type: 'session-note', name, annotation });
340
+ return annotation;
341
+ },
342
+ sessions() {
343
+ return bindings('session-binding').map(b => {
344
+ try {
345
+ const view = readRetainedSession(input.archiveRoot, b.manifestId);
346
+ return { ...b, state: 'ready', sessionId: view.parent.sessionId, reason: 'retrospective evidence; assessment pending' };
347
+ }
348
+ catch (e) {
349
+ return { ...b, state: 'deferred', sessionId: null, reason: message(e) };
350
+ }
351
+ });
352
+ },
353
+ session(name) {
354
+ const b = selected('session-binding', name);
355
+ return { ...b, ...readRetainedSession(input.archiveRoot, b.manifestId),
356
+ annotations: events().filter(e => e.type === 'session-note' && e.name === name).map(e => e.annotation) };
357
+ },
358
+ dashboardBinding() {
359
+ const trust = currentTrust(), binding = events().filter(e => e.type === 'trust-binding').at(-1);
360
+ return { trustDirectory: trust && binding ? String(binding.directory) : null, trustPolicyId: trust ? trust.inspect(0).policyId : null };
361
+ },
362
+ preparedAdoption(name) { return last('adoption', name) ? adoption(name) : null; },
363
+ adoptionStatus(name) {
364
+ const b = comparisonBinding(name), a = last('adoption', name), activation = last('activation', name), rollback = last('rollback', name);
365
+ const receipt = a ? adoption(name) : null;
366
+ if (activation)
367
+ registryEvidence(String(activation.manifestId), 'activate', name, [String(activation.manifestId)]);
368
+ if (rollback)
369
+ registryEvidence(String(rollback.manifestId), 'rollback', name, [String(rollback.manifestId)]);
370
+ return { binding: b.adoptionBinding, receipt, activationManifestId: activation ? String(activation.manifestId) : null, rollbackManifestId: rollback ? String(rollback.manifestId) : null };
371
+ },
372
+ bindCases(raw) { const b = learningCopy(raw); validateCases(b); bound('case-binding', b); return b; },
373
+ bindComparison(raw) { const b = learningCopy(raw); validateComparison(b); bound('comparison-binding', b); return { name: b.name, comparisonManifestId: b.comparisonManifestId }; },
374
+ linkComparisonContext(name, context) {
375
+ const before = history().at(-1).id, b = comparisonBinding(name), safe = learningCopy(context);
376
+ closed(safe, ['caseReference', 'hypothesisManifestId', 'adoptionBinding']);
377
+ for (const key of ['caseReference', 'hypothesisManifestId', 'adoptionBinding'])
378
+ if (b[key] !== null && learningHash(b[key]) !== learningHash(safe[key]))
379
+ throw Error('comparison context cannot replace retained links');
380
+ validateComparison({ ...b, ...safe });
381
+ if (learningHash({ caseReference: b.caseReference, hypothesisManifestId: b.hypothesisManifestId, adoptionBinding: b.adoptionBinding }) !== learningHash(safe))
382
+ append({ type: 'comparison-context', name, context: safe }, before);
383
+ return { name, linked: true };
384
+ },
385
+ comparisonContext(name) {
386
+ const b = comparisonBinding(name);
387
+ validateComparison(b);
388
+ return { caseReference: b.caseReference, hypothesisManifestId: b.hypothesisManifestId, adoptionBinding: b.adoptionBinding };
389
+ },
390
+ bindTrust(trustDirectory) {
391
+ const before = history().at(-1).id;
392
+ path(trustDirectory);
393
+ const trust = openTrustLifecycle(trustDirectory), config = trust.configuration();
394
+ validateTrustScope(config);
395
+ const old = currentTrust();
396
+ if (old) {
397
+ if (old.inspect(0).policyId !== trust.inspect(0).policyId)
398
+ throw Error('trust is frozen for this workspace; no attention refill');
399
+ return;
400
+ }
401
+ append({ type: 'trust-binding', directory: trustDirectory, policyId: trust.inspect(0).policyId }, before);
402
+ },
403
+ configureTrust(raw, authorizedPolicyDigests) {
404
+ const config = learningCopy(raw);
405
+ validateTrustScope(config);
406
+ const existing = currentTrust();
407
+ if (existing && existing.inspect(0).policyId !== trustPolicyDigest(config))
408
+ throw Error('trust already frozen; no attention refill');
409
+ const trust = createTrustLifecycle(join(directory, 'trust'), config, authorizedPolicyDigests);
410
+ this.bindTrust(join(directory, 'trust'));
411
+ return trust.inspect(0);
412
+ },
413
+ trust() { const trust = currentTrust(); if (!trust)
414
+ throw Error('trust not configured; use learning trust setup'); return trust; },
415
+ addUnflagged(raw) {
416
+ const b = learningCopy(raw);
417
+ closed(b, ['name', 'title', 'manifestId', 'split']);
418
+ named(b);
419
+ source(input.archiveRoot, b.manifestId);
420
+ if (!['calibration', 'heldout', 'tuning'].includes(b.split))
421
+ throw Error('explicit unflagged split required');
422
+ if (currentTrust())
423
+ throw Error('cohort already frozen; cannot add samples after labels');
424
+ bound('unflagged-binding', b);
425
+ return b;
426
+ },
427
+ unflagged() { return bindings('unflagged-binding').map(b => { source(input.archiveRoot, b.manifestId); return { ...b, incidentId: learningHash({ scope: input.scopeDigest, evidence: source(input.archiveRoot, b.manifestId).reference.sha256 }) }; }); },
428
+ cases(name, offset = 0) { const b = selected('case-binding', name); return { ...validateCases(b).list(offset), batchId: b.batchId, version: b.version }; },
429
+ caseEvidence(name, caseManifestId) { return caseEvidenceIds(name, caseManifestId).map(manifestId => ({ manifestId, role: manifestId === caseManifestId ? 'nomination' : 'frozen-inputs', ...readArchiveSourceReference(input.archiveRoot, manifestId) })); },
430
+ readCaseEvidence(name, caseManifestId, manifestId) {
431
+ if (!caseEvidenceIds(name, caseManifestId).includes(manifestId))
432
+ throw Error('evidence outside selected case');
433
+ return readArchiveSource(input.archiveRoot, manifestId);
434
+ },
435
+ decideCase(name, request) { return validateCases(selected('case-binding', name)).decide(learningCopy(request)); },
436
+ hypotheses() {
437
+ const named = bindings('hypothesis-binding');
438
+ const origins = bindings('comparison-binding').map(b => comparisonBinding(b.name)).filter(b => b.hypothesisManifestId).map(b => ({ name: `origin:${b.name}`, manifestId: b.hypothesisManifestId }));
439
+ return [...named, ...origins.filter(b => !named.some(n => n.manifestId === b.manifestId))].map(b => ({ ...b, hypothesis: hypothesis(b.manifestId) }));
440
+ },
441
+ propose(name, proposal, hypothesisName = name) {
442
+ const h = buildHypothesis(proposal);
443
+ if (h.proposal.population !== input.population)
444
+ throw Error('hypothesis population mismatch');
445
+ const rows = this.cases(name);
446
+ const all = [...rows.items];
447
+ for (let offset = 5; offset < rows.total; offset += 5)
448
+ all.push(...this.cases(name, offset).items);
449
+ if (h.proposal.caseIds.some(id => !all.some(c => c.candidate.id === id && c.disposition === 'confirmed_defect')))
450
+ throw Error('current confirmed cases required for hypothesis');
451
+ if (!NAME.test(hypothesisName))
452
+ throw Error('hypothesis name must be a short slug');
453
+ const manifestId = retain(input.archiveRoot, 'factory-hypothesis', h);
454
+ bound('hypothesis-binding', { name: hypothesisName, manifestId });
455
+ return { hypothesis: h, manifestId };
456
+ },
457
+ comparison(name) {
458
+ const q = quality(name), cards = q.blind.view().cards.map((c, i) => ({ ...c, displayLabel: String.fromCharCode(65 + i) }));
459
+ const d = decision(name), a = this.adoptionStatus(name);
460
+ const outcomes = events().filter(e => e.type === 'outcome' && e.name === name).map(e => {
461
+ const { manifestId, ...body } = e;
462
+ if (learningHash(jsonSource(input.archiveRoot, String(manifestId), 'learning-outcome')) !== learningHash(body))
463
+ throw Error('retained outcome changed or missing');
464
+ const observation = e.observation;
465
+ observation.evidence.forEach(id => source(input.archiveRoot, id));
466
+ const classification = classifyProductionObservation(adoption(name), observation);
467
+ if (learningHash(classification) !== learningHash(e.classification))
468
+ throw Error('outcome classification changed');
469
+ return { manifestId: e.manifestId, classification };
470
+ });
471
+ return {
472
+ name, title: q.b.title, scopeStatement: q.b.scopeStatement, mode: 'deliberate-review',
473
+ cards, choice: q.choice, qualityScope: q.receipt ? 'full-artifacts' : q.choice ? 'legacy-unscoped' : null,
474
+ priorFeedback: q.b.priorFeedbackManifestIds.map(manifestId => ({ manifestId, scope: 'prior-feedback-only' })),
475
+ decision: d, activation: a.activationManifestId ? 'recorded' : 'not-activated', rollback: a.rollbackManifestId ? 'recorded' : 'not-recorded', outcomes,
476
+ adoptionReadiness: { state: a.rollbackManifestId ? 'rollback-recorded' : a.activationManifestId ? 'activation-recorded' : 'deferred', reason: a.rollbackManifestId ? 'original registry rollback recorded; historical activation retained' : a.activationManifestId ? 'original activation receipt linked; current registry state remains producer-owned' : !q.b.adoptionBinding ? 'scoped candidate and independent eligibility/authority not configured' : !q.receipt ? 'full-artifact quality choice missing' : d?.disposition !== 'adopt' ? 'explicit adoption intent missing' : !a.receipt ? 'independent adoption authority and current eligible facts required' : 'prepared only; original registry activation still required' },
477
+ laterOutcomeReadiness: outcomes.length ? 'observations-linked (not an improvement claim)' : 'no later outcome observed',
478
+ revealReady: !!q.choice && !!q.receipt,
479
+ adoptionConfigured: !!q.b.adoptionBinding,
480
+ limitations: ['Artifact content may disclose identity.', 'A quality choice is not acceptance, calibration or adoption.', ...(q.b.priorFeedbackManifestIds.length ? ['Previously exposed feedback is linked, not a fresh blind experiment.'] : [])],
481
+ };
482
+ },
483
+ artifact(name, label, digest) { return quality(name).blind.readArtifact(label, digest); },
484
+ choose(name, raw, rawReview) {
485
+ const before = history().at(-1).id, review = learningCopy(rawReview), q = quality(name), choice = q.blind.previewChoice(learningCopy(raw));
486
+ if (q.choice && learningHash(q.choice) !== learningHash(choice))
487
+ throw Error('blind quality choice locked');
488
+ closed(review, ['scope', 'note', 'reviewedArtifactDigests']);
489
+ text(review.note);
490
+ const required = [...new Set(q.blind.view().cards.flatMap(c => c.artifactDigests))].sort();
491
+ if (review.scope !== 'full-artifacts' || !required.length && choice.kind !== 'insufficient' || !Array.isArray(review.reviewedArtifactDigests) || new Set(review.reviewedArtifactDigests).size !== review.reviewedArtifactDigests.length || review.reviewedArtifactDigests.some(h => !required.includes(h)) || choice.kind !== 'insufficient' && learningHash([...review.reviewedArtifactDigests].sort()) !== learningHash(required))
492
+ throw Error('explicit complete artifact review required; excerpts do not qualify');
493
+ review.reviewedArtifactDigests.sort();
494
+ if (q.receipt) {
495
+ if (learningHash(q.receipt.choice) !== learningHash(choice) || learningHash(q.receipt.review) !== learningHash(review))
496
+ throw Error('quality receipt locked');
497
+ return learningCopy(q.receipt);
498
+ }
499
+ const pending = last('quality-pending', name), request = { choice, review };
500
+ if (pending && learningHash(pending.request) !== learningHash(request))
501
+ throw Error('quality write pending; repeat the exact choice to finish');
502
+ const claimed = pending ? before : append({ type: 'quality-pending', name, request }, before).id;
503
+ const actual = q.blind.choose(choice);
504
+ const receipt = { type: 'quality', name, choice: actual, review, author: input.author, comparisonManifestId: q.b.comparisonManifestId };
505
+ const manifestId = retain(input.archiveRoot, 'learning-quality', receipt);
506
+ append({ ...receipt, manifestId }, claimed);
507
+ return { ...receipt, manifestId };
508
+ },
509
+ reveal(name) { return requireQuality(name).blind.reveal(); },
510
+ decisionStatus(name) { const b = comparisonBinding(name); return { name, title: b.title, scopeStatement: b.scopeStatement, current: decision(name) }; },
511
+ decide(name, raw) {
512
+ const before = history().at(-1).id, request = learningCopy(raw);
513
+ closed(request, ['disposition', 'note', 'priorDecisionId']);
514
+ text(request.note);
515
+ if (!['adopt', 'reject', 'defer'].includes(request.disposition))
516
+ throw Error('choose adopt, reject or defer');
517
+ if (request.priorDecisionId !== null)
518
+ hash(request.priorDecisionId);
519
+ comparisonBinding(name);
520
+ const old = decision(name);
521
+ if (old && learningHash({ disposition: old.disposition, note: old.note, priorDecisionId: old.priorDecisionId }) === learningHash(request))
522
+ return old;
523
+ if ((old?.id ?? null) !== request.priorDecisionId)
524
+ throw Error('stale learning decision');
525
+ if (last('adoption', name))
526
+ throw Error('adoption already authorized; use explicit rollback rather than replacing its decision');
527
+ if (request.disposition === 'adopt') {
528
+ const q = requireQuality(name);
529
+ if (!['one', 'tie'].includes(q.choice.kind))
530
+ throw Error('positive quality choice required for adoption intent');
531
+ }
532
+ const e = append({ type: 'decision', name, request, author: input.author }, before);
533
+ return { id: e.id, ...request };
534
+ },
535
+ prepareAdoption(name, authority, facts, now) {
536
+ const before = history().at(-1).id, q = requireQuality(name), b = q.b.adoptionBinding;
537
+ if (!b || decision(name)?.disposition !== 'adopt')
538
+ throw Error('scoped binding and explicit adopt decision required');
539
+ const selected = q.blind.reveal().arms.filter(a => q.choice.labels.includes(a.label));
540
+ if (!selected.some(a => a.configuration.configuration === b.candidateDigest))
541
+ throw Error('adoption candidate is not a quality-selected configuration');
542
+ const receipt = authorizeAdoption(b, authority, facts, now), old = last('adoption', name);
543
+ if (old) {
544
+ if (learningHash(old.receipt) !== learningHash(receipt))
545
+ throw Error('adoption receipt locked');
546
+ return receipt;
547
+ }
548
+ append({ type: 'adoption', name, receipt, manifestId: retain(input.archiveRoot, 'learning-adoption', receipt) }, before);
549
+ return receipt;
550
+ },
551
+ linkActivation(name, manifestId, authorizedManifestIds) {
552
+ const before = history().at(-1).id, receipt = registryEvidence(manifestId, 'activate', name, authorizedManifestIds), old = last('activation', name);
553
+ if (old) {
554
+ if (old.manifestId !== manifestId)
555
+ throw Error('activation link locked');
556
+ return receipt;
557
+ }
558
+ append({ type: 'activation', name, manifestId }, before);
559
+ return receipt;
560
+ },
561
+ previewRollback(name, reason, evidence, expiresAt) {
562
+ if (!this.adoptionStatus(name).activationManifestId)
563
+ throw Error('observed activation required before rollback');
564
+ evidence.forEach(id => source(input.archiveRoot, id));
565
+ return buildRollbackRequest(adoption(name), reason, evidence, expiresAt);
566
+ },
567
+ prepareRollback(name, request, authority, now, current) {
568
+ const before = history().at(-1).id;
569
+ if (!this.adoptionStatus(name).activationManifestId)
570
+ throw Error('observed activation required before rollback');
571
+ request.evidence.forEach(id => source(input.archiveRoot, id));
572
+ if (buildRollbackRequest(adoption(name), request.reason, [...request.evidence], request.expiresAt).id !== request.id)
573
+ throw Error('rollback request changed');
574
+ const result = authorizeRollback(adoption(name), request, authority, now, current), old = last('rollback-request', name);
575
+ if (old) {
576
+ if (learningHash(old.request) !== learningHash(request))
577
+ throw Error('rollback request locked');
578
+ return result;
579
+ }
580
+ append({ type: 'rollback-request', name, request: learningCopy(request) }, before);
581
+ return result;
582
+ },
583
+ linkRollback(name, manifestId, authorizedManifestIds) {
584
+ const before = history().at(-1).id, receipt = registryEvidence(manifestId, 'rollback', name, authorizedManifestIds), old = last('rollback', name);
585
+ const activation = jsonSource(input.archiveRoot, String(last('activation', name)?.manifestId), 'learning-registry-receipt');
586
+ if (receipt.revision <= activation.revision)
587
+ throw Error('rollback registry revision must follow activation');
588
+ if (old) {
589
+ if (old.manifestId !== manifestId)
590
+ throw Error('rollback link locked');
591
+ return receipt;
592
+ }
593
+ append({ type: 'rollback', name, manifestId }, before);
594
+ return receipt;
595
+ },
596
+ previewObservation(name, raw) {
597
+ if (!this.adoptionStatus(name).activationManifestId)
598
+ throw Error('later outcome needs observed activation, not just prepared adoption');
599
+ const observation = learningCopy(raw);
600
+ closed(observation, ['id', 'adoptionId', 'candidateDigest', 'scopeDigest', 'originalRequirementDigest', 'currentRequirementDigest', 'outcome', 'acceptanceDigest', 'acceptedArtifactDigest', 'observedArtifactDigest', 'evidence']);
601
+ observation.evidence.forEach(id => source(input.archiveRoot, id));
602
+ const classification = classifyProductionObservation(adoption(name), observation);
603
+ return { observation, classification, digest: learningHash({ name, observation }) };
604
+ },
605
+ observe(name, raw, authorizedDigests, referenceAuthor) {
606
+ const before = history().at(-1).id, preview = this.previewObservation(name, raw);
607
+ if (!authorizedDigests.includes(preview.digest))
608
+ throw Error('independent exact observation authority required');
609
+ const old = events().find(e => e.type === 'outcome' && e.name === name && e.observation.id === raw.id);
610
+ if (old) {
611
+ if (learningHash(old.observation) !== learningHash(preview.observation))
612
+ throw Error('outcome identity conflict');
613
+ return learningCopy(old);
614
+ }
615
+ if (referenceAuthor !== undefined)
616
+ text(referenceAuthor, 512);
617
+ const value = { type: 'outcome', name, ...preview, ...(referenceAuthor === undefined ? {} : { referenceAuthor }) };
618
+ const manifestId = retain(input.archiveRoot, 'learning-outcome', value);
619
+ append({ ...value, manifestId }, before);
620
+ return { ...value, manifestId };
621
+ },
622
+ lifecycle(name) {
623
+ const before = history().at(-1).id, q = quality(name), b = q.b;
624
+ this.comparison(name); // Revalidate every linked retained receipt before advancing navigation.
625
+ if (!b.caseReference || !b.hypothesisManifestId)
626
+ return { state: 'incomplete-links', manifestId: null };
627
+ const activation = last('activation', name), rollback = last('rollback', name), prior = last('lifecycle', name);
628
+ const links = { caseManifestId: b.caseReference.manifestId, hypothesisManifestId: b.hypothesisManifestId, comparisonManifestId: b.comparisonManifestId,
629
+ choiceManifestId: q.receipt ? String(q.receipt.manifestId) : null, adoptionManifestId: activation ? String(activation.manifestId) : null, rollbackManifestId: rollback ? String(rollback.manifestId) : null,
630
+ outcomeManifestIds: events().filter(e => e.type === 'outcome' && e.name === name).map(e => String(e.manifestId)) };
631
+ if (prior && learningHash(prior.links) === learningHash(links))
632
+ return { state: 'linked', manifestId: String(prior.manifestId) };
633
+ const manifestId = retainLearningLifecycle(input.archiveRoot, { ...links, predecessorManifestId: prior ? String(prior.manifestId) : null });
634
+ append({ type: 'lifecycle', name, links, manifestId }, before);
635
+ return { state: 'linked', manifestId };
636
+ },
637
+ inspect(now) {
638
+ clock(now);
639
+ const cases = bindings('case-binding').map(b => { try {
640
+ const p = validateCases(b).list();
641
+ return { name: b.name, title: b.title, state: 'ready', total: p.total, reason: 'deliberate review; nomination is not confirmation' };
642
+ }
643
+ catch (e) {
644
+ return { name: b.name, title: b.title, state: 'deferred', total: null, reason: message(e) };
645
+ } });
646
+ const comparisons = bindings('comparison-binding').map(b => { try {
647
+ const v = this.comparison(b.name);
648
+ return { state: 'ready', reason: v.revealReady ? 'quality recorded; adoption remains separate' : 'quality choice pending; identity and cost hidden', ...v };
649
+ }
650
+ catch (e) {
651
+ return { name: b.name, title: b.title, state: 'deferred', reason: message(e), decision: decision(b.name) };
652
+ } });
653
+ let trust;
654
+ try {
655
+ const t = currentTrust();
656
+ trust = t ? { state: 'configured', ...t.inspect(now), automaticExposure: t.previewExposure(now) } : { state: 'deferred', reason: 'trust not configured', automaticExposure: { mode: 'silent', reason: 'policy-unavailable' } };
657
+ }
658
+ catch (e) {
659
+ trust = { state: 'deferred', reason: message(e), automaticExposure: { mode: 'silent', reason: 'invalid-trust-evidence' } };
660
+ }
661
+ return { version: 'learning-workspace-view-v1', configuration: learningCopy(input), mode: 'deliberate-review', sessions: this.sessions(), cases, comparisons, trust, automaticExposureReserved: false, delegationStarted: false, acceptance: 'not-assessed' };
662
+ },
663
+ };
664
+ }
665
+ export function catalogLearningArchive(root, author) {
666
+ path(root);
667
+ text(author, 512);
668
+ const dir = join(root, 'manifests');
669
+ for (let p = dir;; p = dirname(p)) {
670
+ const s = lstatSync(p);
671
+ if (!s.isDirectory() || s.isSymbolicLink())
672
+ throw Error('archive manifest directory substituted');
673
+ if (p === dirname(p))
674
+ break;
675
+ }
676
+ const files = readdirSync(dir);
677
+ if (files.length > 8192)
678
+ throw Error('archive catalog bound exceeded');
679
+ return files.filter(id => SHA.test(id)).sort().flatMap(manifestId => {
680
+ const r = readArchiveSourceReference(root, manifestId);
681
+ if (r.status !== 'available')
682
+ return [];
683
+ const p = r.reference.parser;
684
+ if (p.id === 'work-candidate-batch' && p.version === '1' || p.id === 'work-signal-batch' && p.version === '1') {
685
+ try {
686
+ const page = (p.id === 'work-signal-batch' ? createWorkSignalReviewer(root, manifestId, author) : createWorkCaseReviewer(root, manifestId, author)).list();
687
+ return [{ kind: 'cases', manifestId, version: p.id === 'work-signal-batch' ? 3 : 2, description: `${page.total} nomination(s)${page.items[0] ? ` — ${page.items[0].candidate.target.obligationId}: ${page.items[0].candidate.reason}` : ''}`, ...(page.items[0] ? { scopeDigest: page.items[0].candidate.target.snapshotDigest, population: page.items[0].candidate.detector.population } : {}) }];
688
+ }
689
+ catch {
690
+ return [];
691
+ }
692
+ }
693
+ if (p.id === 'blind-intervention' && p.version === '1') {
694
+ try {
695
+ readBlindInterventionBinding(root, manifestId, author);
696
+ return [{ kind: 'comparison', manifestId, version: 1, description: 'Retained comparison (identity hidden)' }];
697
+ }
698
+ catch {
699
+ return [];
700
+ }
701
+ }
702
+ return [];
703
+ });
704
+ }
705
+ //# sourceMappingURL=learning-workspace.js.map