fraim 2.0.346 → 2.0.348
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/dist/registry/scripts/learning/completion-state.js +80 -4
- package/dist/registry/scripts/learning/corpus-audit.js +7 -7
- package/dist/registry/scripts/learning/entry-authoring.js +181 -0
- package/dist/registry/scripts/learning/entry-format.js +10 -1
- package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
- package/dist/registry/scripts/learning/outcome-types.js +92 -0
- package/dist/registry/scripts/learning/owner-identity.js +11 -0
- package/dist/registry/scripts/learning/proposal-queue.js +81 -0
- package/dist/registry/scripts/learning/run-application.js +1003 -0
- package/dist/registry/scripts/learning/run-publication.js +364 -0
- package/dist/registry/scripts/learning/run-record.js +895 -0
- package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
- package/dist/src/cli/commands/learning.js +64 -0
- package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/learning/inventory.js +285 -0
- package/dist/src/cli/learning/queue-operations.js +199 -0
- package/dist/src/cli/learning/runtime.js +82 -0
- package/dist/src/cli/learning/usage.js +277 -0
- package/dist/src/core/fraim-config-schema.generated.js +0 -4
- package/dist/src/local-mcp-server/learning-context-builder.js +91 -377
- package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
- package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
- package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
- package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
- package/dist/src/local-mcp-server/stdio-server.js +0 -48
- package/package.json +9 -1
- package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
- package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
- package/dist/src/local-mcp-server/learning-usage-command.js +0 -518
|
@@ -4,10 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.COMPLETION_END = exports.COMPLETION_START = exports.UnsupportedRunHistoryError = exports.sourcePathKey = void 0;
|
|
7
|
+
exports.validateDeferredAttempt = validateDeferredAttempt;
|
|
8
|
+
exports.validateDeferredWork = validateDeferredWork;
|
|
7
9
|
exports.cycleRetrospectivePath = cycleRetrospectivePath;
|
|
8
10
|
exports.validateProcessedSources = validateProcessedSources;
|
|
9
11
|
exports.canonicalJournal = canonicalJournal;
|
|
10
12
|
exports.readRunJournal = readRunJournal;
|
|
13
|
+
exports.readRunReservations = readRunReservations;
|
|
11
14
|
exports.readSourceReservations = readSourceReservations;
|
|
12
15
|
exports.normalizeSourceParticipants = normalizeSourceParticipants;
|
|
13
16
|
exports.sourceAuthorAllowed = sourceAuthorAllowed;
|
|
@@ -17,6 +20,33 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
17
20
|
const node_crypto_1 = require("node:crypto");
|
|
18
21
|
const artifact_paths_1 = require("../artifact-paths");
|
|
19
22
|
const corpus_audit_1 = require("./corpus-audit");
|
|
23
|
+
const outcome_types_1 = require("./outcome-types");
|
|
24
|
+
function validateDeferredAttempt(value) {
|
|
25
|
+
if (!keys(value, ['at', 'evidenceHash', 'deferred', 'reason']) || !text(value.at) || !Number.isFinite(Date.parse(value.at)) || !hash(value.evidenceHash))
|
|
26
|
+
throw new Error('invalid-deferred-attempt');
|
|
27
|
+
// Older journals embedded the checkpoint snapshot; keep accepting those exact records.
|
|
28
|
+
if (value.deferred !== undefined && value.reason === undefined)
|
|
29
|
+
validateDeferredWork(value.deferred);
|
|
30
|
+
else if (value.deferred !== undefined || !text(value.reason))
|
|
31
|
+
throw new Error('invalid-deferred-attempt');
|
|
32
|
+
}
|
|
33
|
+
function validateDeferredWork(value) {
|
|
34
|
+
if (!keys(value, ['actions', 'sources', 'corpusComplete', 'corpusErrors']) || value.corpusComplete !== false || !Array.isArray(value.actions) || !Array.isArray(value.sources))
|
|
35
|
+
throw new Error('invalid-deferred-work');
|
|
36
|
+
if (value.corpusErrors !== undefined && (!Array.isArray(value.corpusErrors) || value.corpusErrors.some(item => !keys(item, ['path', 'reason']) || !text(item.path) || !node_path_1.default.isAbsolute(item.path) || !text(item.reason))))
|
|
37
|
+
throw new Error('invalid-deferred-work');
|
|
38
|
+
const actions = new Set(), sources = new Set();
|
|
39
|
+
for (const item of value.actions) {
|
|
40
|
+
if (!keys(item, ['actionId', 'revision', 'digest', 'reason']) || !text(item.actionId) || !Number.isInteger(item.revision) || item.revision < 1 || !hash(item.digest) || !text(item.reason) || actions.has(item.actionId))
|
|
41
|
+
throw new Error('invalid-deferred-work');
|
|
42
|
+
actions.add(item.actionId);
|
|
43
|
+
}
|
|
44
|
+
for (const item of value.sources) {
|
|
45
|
+
if (!keys(item, ['path', 'owner', 'reason', 'sha256']) || !text(item.path) || !node_path_1.default.isAbsolute(item.path) || /[\x00-\x1f]/.test(item.path) || !ownerValid(item.owner) || !text(item.reason) || (item.sha256 !== undefined && !hash(item.sha256)) || sources.has((0, exports.sourcePathKey)(item.path)))
|
|
46
|
+
throw new Error('invalid-deferred-work');
|
|
47
|
+
sources.add((0, exports.sourcePathKey)(item.path));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
20
50
|
const sourcePathKey = (file) => {
|
|
21
51
|
const value = node_path_1.default.resolve(file).replace(/\\/g, '/');
|
|
22
52
|
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
@@ -145,9 +175,10 @@ function readRunJournal(file) {
|
|
|
145
175
|
}
|
|
146
176
|
return { events, lastHash, format: 'current' };
|
|
147
177
|
}
|
|
148
|
-
/**
|
|
149
|
-
function
|
|
178
|
+
/** Current archive plans and prepared assessments share one verified reservation inventory. */
|
|
179
|
+
function readRunReservations(runDirectories, excludingRunFile) {
|
|
150
180
|
const reserved = new Set();
|
|
181
|
+
const assessmentKeys = new Set();
|
|
151
182
|
for (const directory of new Set(runDirectories.map(dir => node_path_1.default.resolve(dir)))) {
|
|
152
183
|
let names;
|
|
153
184
|
try {
|
|
@@ -168,6 +199,23 @@ function readSourceReservations(runDirectories, excludingRunFile) {
|
|
|
168
199
|
const journal = readRunJournal(file);
|
|
169
200
|
if (journal.format === 'historical')
|
|
170
201
|
continue;
|
|
202
|
+
const assessmentSources = new Map();
|
|
203
|
+
for (const event of journal.events) {
|
|
204
|
+
if (event.payload.type === 'assessment-prepared') {
|
|
205
|
+
const { record, sourcePaths } = event.payload.data;
|
|
206
|
+
(0, outcome_types_1.validateOutcomeAssessmentRecord)(record);
|
|
207
|
+
if (!Array.isArray(sourcePaths) || sourcePaths.length !== record.sourceRefs.length || sourcePaths.some(file => typeof file !== 'string' || !node_path_1.default.isAbsolute(file)))
|
|
208
|
+
throw new Error('invalid-assessment-sources');
|
|
209
|
+
assessmentSources.set(record.digest, { paths: sourcePaths, key: (0, outcome_types_1.assessmentKey)(record) });
|
|
210
|
+
}
|
|
211
|
+
else if (event.payload.type === 'assessment-committed')
|
|
212
|
+
assessmentSources.delete(event.payload.data.digest);
|
|
213
|
+
}
|
|
214
|
+
for (const reservation of assessmentSources.values()) {
|
|
215
|
+
assessmentKeys.add(reservation.key);
|
|
216
|
+
for (const source of reservation.paths)
|
|
217
|
+
reserved.add((0, exports.sourcePathKey)(source));
|
|
218
|
+
}
|
|
171
219
|
for (const event of journal.events)
|
|
172
220
|
if (event.payload.type === 'source-completion') {
|
|
173
221
|
for (const source of event.payload.data) {
|
|
@@ -183,7 +231,10 @@ function readSourceReservations(runDirectories, excludingRunFile) {
|
|
|
183
231
|
}
|
|
184
232
|
}
|
|
185
233
|
}
|
|
186
|
-
return [...reserved].sort();
|
|
234
|
+
return { sources: [...reserved].sort(), assessmentKeys: [...assessmentKeys].sort() };
|
|
235
|
+
}
|
|
236
|
+
function readSourceReservations(runDirectories, excludingRunFile) {
|
|
237
|
+
return readRunReservations(runDirectories, excludingRunFile).sources;
|
|
187
238
|
}
|
|
188
239
|
exports.COMPLETION_START = '<!-- fraim-sleep-decisions:v1 -->';
|
|
189
240
|
exports.COMPLETION_END = '<!-- /fraim-sleep-decisions -->';
|
|
@@ -234,7 +285,7 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
234
285
|
catch {
|
|
235
286
|
fail();
|
|
236
287
|
}
|
|
237
|
-
if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources', 'sourceParticipants', 'corpusAudit', 'reconciliationHistory']) || state.schemaVersion !== 1 || !text(state.runId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(state.runId) || !ownerValid(state.owner)
|
|
288
|
+
if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources', 'sourceParticipants', 'corpusAudit', 'reconciliationHistory', 'deferred', 'deferredHistory', 'deferredAttempts', 'outcomeAssessments']) || state.schemaVersion !== 1 || !text(state.runId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(state.runId) || !ownerValid(state.owner)
|
|
238
289
|
|| !['pending', 'complete'].includes(state.finalizationState) || !Array.isArray(state.decisions) || !Array.isArray(state.sourceDispositions) || !Array.isArray(state.cleanup))
|
|
239
290
|
fail();
|
|
240
291
|
if ((expected.runId && expected.runId !== state.runId) || (expected.owner && !sameOwner(expected.owner, state.owner)))
|
|
@@ -249,8 +300,31 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
249
300
|
fail();
|
|
250
301
|
if (state.processedSources !== undefined)
|
|
251
302
|
validateProcessedSources(state.processedSources);
|
|
303
|
+
if (state.outcomeAssessments !== undefined) {
|
|
304
|
+
if (!Array.isArray(state.outcomeAssessments))
|
|
305
|
+
fail();
|
|
306
|
+
const digests = new Set();
|
|
307
|
+
for (const assessment of state.outcomeAssessments) {
|
|
308
|
+
(0, outcome_types_1.validateOutcomeAssessmentRecord)(assessment);
|
|
309
|
+
if (assessment.runId !== state.runId || digests.has(assessment.digest))
|
|
310
|
+
fail();
|
|
311
|
+
digests.add(assessment.digest);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
252
314
|
if (state.sourceParticipants !== undefined)
|
|
253
315
|
normalizeSourceParticipants(state.sourceParticipants, state.owner);
|
|
316
|
+
if (state.deferred !== undefined)
|
|
317
|
+
validateDeferredWork(state.deferred);
|
|
318
|
+
if (state.deferredHistory !== undefined) {
|
|
319
|
+
if (!Array.isArray(state.deferredHistory))
|
|
320
|
+
fail();
|
|
321
|
+
state.deferredHistory.forEach(validateDeferredWork);
|
|
322
|
+
}
|
|
323
|
+
if (state.deferredAttempts !== undefined) {
|
|
324
|
+
if (!Array.isArray(state.deferredAttempts))
|
|
325
|
+
fail();
|
|
326
|
+
state.deferredAttempts.forEach(validateDeferredAttempt);
|
|
327
|
+
}
|
|
254
328
|
const ids = new Set();
|
|
255
329
|
for (const item of state.decisions) {
|
|
256
330
|
if (!keys(item, ['actionId', 'revision', 'digest', 'owner', 'claimKey', 'evidenceFingerprint', 'sourceRefs', 'decision', 'publicationRef', 'target', 'dependencies']) || !text(item.actionId) || ids.has(item.actionId) || !Number.isInteger(item.revision) || item.revision < 1 || !hash(item.digest) || !ownerValid(item.owner)
|
|
@@ -282,6 +356,8 @@ function decodeCompletion(markdown, expected = {}) {
|
|
|
282
356
|
fail();
|
|
283
357
|
}
|
|
284
358
|
const decisions = new Map(state.decisions.map((item) => [item.actionId, item]));
|
|
359
|
+
if (state.deferred?.actions.some((item) => decisions.has(item.actionId)))
|
|
360
|
+
fail();
|
|
285
361
|
if (state.corpusAudit !== undefined) {
|
|
286
362
|
try {
|
|
287
363
|
(0, corpus_audit_1.validateCorpusAudit)(state.corpusAudit);
|
|
@@ -10,7 +10,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const node_crypto_1 = require("node:crypto");
|
|
12
12
|
const owner_identity_1 = require("./owner-identity");
|
|
13
|
-
const { learningEntryBlocks } = require('./entry-format');
|
|
13
|
+
const { learningEntryBlocks, learningEntryIdentity, isLearningEntryId } = require('./entry-format');
|
|
14
14
|
const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
15
15
|
? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
|
|
16
16
|
const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
|
|
@@ -63,9 +63,8 @@ function collectLearningCorpus(homes, subjects, readerRoots = []) {
|
|
|
63
63
|
for (const block of blocks) {
|
|
64
64
|
// Match the runtime reader's handwritten identity contract.
|
|
65
65
|
// Approved mutations still use the author's narrower generated-ID schema.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if ((declarations.length && (!id || declarations.length !== 1)) || (id && ids.has(id))
|
|
66
|
+
const { id, ambiguous } = learningEntryIdentity(block.text);
|
|
67
|
+
if (ambiguous || (id && ids.has(id))
|
|
69
68
|
|| !/^#{2,6}\s+\[P-(CRITICAL|HIGH|MED|LOW)\]\s+\S/.test(block.text)) {
|
|
70
69
|
errors.push({ path: file.replace(/\\/g, '/'), reason: 'ambiguous-corpus-entry' });
|
|
71
70
|
continue;
|
|
@@ -157,8 +156,9 @@ function collectLearningCorpus(homes, subjects, readerRoots = []) {
|
|
|
157
156
|
/** Shared by journal replay and the strict compact receipt decoder. No filesystem access. */
|
|
158
157
|
function validateCorpusAudit(audit, reviewer) {
|
|
159
158
|
const fail = () => { throw new Error('invalid-corpus-audit'); };
|
|
160
|
-
if (!keys(audit, ['snapshot', 'reviewer', 'dispositions']) || typeof audit.reviewer !== 'string' || !audit.reviewer.trim() || (reviewer && audit.reviewer !== reviewer)
|
|
161
|
-
|| !keys(audit.snapshot, ['schemaVersion', 'homes', 'entries', 'duplicates', 'fingerprint', 'complete', 'errors']) || audit.snapshot.schemaVersion !== 1 || audit.snapshot.complete !==
|
|
159
|
+
if (!keys(audit, ['snapshot', 'reviewer', 'dispositions', 'partial']) || typeof audit.reviewer !== 'string' || !audit.reviewer.trim() || (reviewer && audit.reviewer !== reviewer) || (audit.partial !== undefined && audit.partial !== true)
|
|
160
|
+
|| !keys(audit.snapshot, ['schemaVersion', 'homes', 'entries', 'duplicates', 'fingerprint', 'complete', 'errors']) || audit.snapshot.schemaVersion !== 1 || typeof audit.snapshot.complete !== 'boolean' || (!audit.partial && !audit.snapshot.complete) || !Array.isArray(audit.snapshot.errors) || (!audit.partial && audit.snapshot.errors.length)
|
|
161
|
+
|| audit.snapshot.errors.some(item => !keys(item, ['path', 'reason']) || typeof item.path !== 'string' || !node_path_1.default.isAbsolute(item.path) || typeof item.reason !== 'string' || !item.reason.trim())
|
|
162
162
|
|| !Array.isArray(audit.snapshot.homes) || !Array.isArray(audit.snapshot.entries) || !Array.isArray(audit.dispositions))
|
|
163
163
|
fail();
|
|
164
164
|
const snapshot = audit.snapshot;
|
|
@@ -172,7 +172,7 @@ function validateCorpusAudit(audit, reviewer) {
|
|
|
172
172
|
fail();
|
|
173
173
|
(0, owner_identity_1.validateOwner)(entry.ref?.owner);
|
|
174
174
|
if (!snapshot.homes.some(owner => (0, owner_identity_1.ownerKey)(owner) === (0, owner_identity_1.ownerKey)(entry.ref.owner))
|
|
175
|
-
|| (entry.ref.entryId === undefined ? !Number.isSafeInteger(entry.occurrence) || entry.occurrence < 0 :
|
|
175
|
+
|| (entry.ref.entryId === undefined ? !Number.isSafeInteger(entry.occurrence) || entry.occurrence < 0 : !isLearningEntryId(entry.ref.entryId) || entry.occurrence !== undefined)
|
|
176
176
|
|| !/^learnings\//.test(entry.ref.relativePath) || /[\\:]/.test(entry.ref.relativePath) || entry.ref.relativePath.split('/').some(part => !part || part === '.' || part === '..')
|
|
177
177
|
|| !/^[a-f0-9]{64}$/.test(entry.sha256) || entries.has(corpusEntryKey(entry)))
|
|
178
178
|
fail();
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.deriveDomain = deriveDomain;
|
|
7
|
+
exports.readerWordCount = readerWordCount;
|
|
8
|
+
exports.validateEntry = validateEntry;
|
|
9
|
+
exports.renderEntry = renderEntry;
|
|
10
|
+
exports.isLearningScaffolding = isLearningScaffolding;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
// Relative to the shipped helper, never to the job's working directory.
|
|
14
|
+
const map = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(__dirname, 'learning-domains.json'), 'utf8'));
|
|
15
|
+
function deriveDomain(origins) {
|
|
16
|
+
const unknown = (reason) => ({ status: 'needs-domain', value: null, reason, mapVersion: map.mapVersion });
|
|
17
|
+
if (!origins.length)
|
|
18
|
+
return unknown('No originating job recorded');
|
|
19
|
+
const decisions = [];
|
|
20
|
+
for (const origin of origins) {
|
|
21
|
+
let declaration;
|
|
22
|
+
const frontmatter = origin.content?.match(/^\uFEFF?---\r?\n([\s\S]*?)\r?\n---/);
|
|
23
|
+
if (frontmatter) {
|
|
24
|
+
try {
|
|
25
|
+
declaration = JSON.parse(frontmatter[1]).domain;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
declaration = frontmatter[1].match(/^domain:\s*["']?([\w-]+)/m)?.[1];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (declaration !== undefined) {
|
|
32
|
+
if (declaration !== 'global' && !map.domains.includes(String(declaration)))
|
|
33
|
+
return unknown(`Invalid domain declaration for ${origin.name}`);
|
|
34
|
+
decisions.push({ value: String(declaration), basis: 'job-declaration' });
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const clean = origin.registryPath?.replace(/\\/g, '/').replace(/^\/?registry\//, '');
|
|
38
|
+
const category = clean?.match(/^jobs\/(?:ai-employee\/|ai-manager\/|personalized-employee\/)?([^/]+)\/[^/]+\.md$/)?.[1];
|
|
39
|
+
if (!category)
|
|
40
|
+
return unknown(`No mapped job path for ${origin.name}`);
|
|
41
|
+
const value = map.jobDomainMap[category] || (map.globalJobCategories.includes(category) ? 'global' : null);
|
|
42
|
+
if (!value)
|
|
43
|
+
return unknown(`Unmapped category for ${origin.name}`);
|
|
44
|
+
decisions.push({ value, basis: 'category' });
|
|
45
|
+
}
|
|
46
|
+
if (new Set(decisions.map(d => d.value)).size !== 1)
|
|
47
|
+
return unknown('Originating jobs span multiple domains');
|
|
48
|
+
return { status: 'resolved', ...decisions[0], mapVersion: map.mapVersion };
|
|
49
|
+
}
|
|
50
|
+
function readerText(entry) {
|
|
51
|
+
return [entry.title, entry.takeaway, entry.why, entry.example, entry.next].join(' ');
|
|
52
|
+
}
|
|
53
|
+
function readerWordCount(entry) {
|
|
54
|
+
const plain = readerText(entry).replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/[*_`~#]/g, '').trim();
|
|
55
|
+
return plain ? plain.split(/\s+/u).length : 0;
|
|
56
|
+
}
|
|
57
|
+
function validateEntry(entry) {
|
|
58
|
+
if (!entry || !['P-CRITICAL', 'P-HIGH', 'P-MED', 'P-LOW'].includes(entry.severity))
|
|
59
|
+
throw new Error('invalid-entry: severity');
|
|
60
|
+
for (const field of ['title', 'takeaway', 'why', 'example', 'next']) {
|
|
61
|
+
if (typeof entry[field] !== 'string' || !entry[field].trim() || /[\r\n\0]/.test(entry[field]))
|
|
62
|
+
throw new Error(`invalid-entry: ${field}`);
|
|
63
|
+
}
|
|
64
|
+
if (readerWordCount(entry) > 80)
|
|
65
|
+
throw new Error('invalid-entry: 80-word budget exceeded');
|
|
66
|
+
// Domain wording and whether an identifier is essential require semantic
|
|
67
|
+
// authoring review/evals; a vocabulary regex rejects legitimate lessons.
|
|
68
|
+
if (!entry.domain || !Number.isInteger(entry.domain.mapVersion))
|
|
69
|
+
throw new Error('invalid-entry: domain');
|
|
70
|
+
if (entry.domain.status === 'resolved') {
|
|
71
|
+
if (!['global', ...map.domains].includes(entry.domain.value) || !['category', 'job-declaration', 'manager'].includes(entry.domain.basis))
|
|
72
|
+
throw new Error('invalid-entry: domain');
|
|
73
|
+
}
|
|
74
|
+
else if (entry.domain.status !== 'needs-domain' || entry.domain.value !== null || !entry.domain.reason)
|
|
75
|
+
throw new Error('invalid-entry: domain');
|
|
76
|
+
const trace = entry.trace;
|
|
77
|
+
if (!trace || !Number.isFinite(trace.score) || !Number.isInteger(trace.recurrences) || trace.recurrences < 1 || !Array.isArray(trace.originatingJobs) || !Array.isArray(trace.sources))
|
|
78
|
+
throw new Error('invalid-entry: trace');
|
|
79
|
+
for (const date of [trace.lastSeen, trace.firstRecorded, ...(trace.firstSynthesized ? [trace.firstSynthesized] : [])]) {
|
|
80
|
+
if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) || new Date(date).toISOString().slice(0, 10) !== date)
|
|
81
|
+
throw new Error('invalid-entry: date');
|
|
82
|
+
}
|
|
83
|
+
if (trace.firstRecorded > trace.lastSeen)
|
|
84
|
+
throw new Error('invalid-entry: source dates');
|
|
85
|
+
if (!trace.originatingJobs.every(job => typeof job.name === 'string' && job.name.length > 0 && (job.registryPath === null || typeof job.registryPath === 'string')))
|
|
86
|
+
throw new Error('invalid-entry: origins');
|
|
87
|
+
if (!trace.sources.every(source => typeof source === 'string'))
|
|
88
|
+
throw new Error('invalid-entry: sources');
|
|
89
|
+
}
|
|
90
|
+
function renderEntry(entryId, entry) {
|
|
91
|
+
validateEntry(entry);
|
|
92
|
+
if (!/^L-[a-f0-9]{10}$/.test(entryId))
|
|
93
|
+
throw new Error('invalid-entry: Id');
|
|
94
|
+
if (entry.domain.status !== 'resolved')
|
|
95
|
+
throw new Error('needs-domain');
|
|
96
|
+
const trace = entry.trace;
|
|
97
|
+
const line = (value) => value.replace(/[\r\n\0]+/g, ' ');
|
|
98
|
+
// Metadata stays on standalone lines for existing learning readers.
|
|
99
|
+
return [
|
|
100
|
+
`### [${entry.severity}] ${entry.title}`, '',
|
|
101
|
+
`**Takeaway**: ${entry.takeaway}`, `**Why this matters**: ${entry.why}`,
|
|
102
|
+
`**Example**: ${entry.example}`, `**What to do next**: ${entry.next}`, '**Technical trace**:',
|
|
103
|
+
`**Id**: ${entryId}`, `**Domain**: ${entry.domain.value}`,
|
|
104
|
+
`**Originating job**: ${line(trace.originatingJobs.map(job => job.registryPath || job.name).join('; '))}`,
|
|
105
|
+
`**Domain basis**: ${entry.domain.basis}; map ${entry.domain.mapVersion}`,
|
|
106
|
+
`**Score**: ${trace.score}`, `**Last seen**: ${trace.lastSeen}`,
|
|
107
|
+
`**Recurrences**: ${trace.recurrences}`, `**First recorded**: ${trace.firstRecorded}`,
|
|
108
|
+
...(trace.firstSynthesized ? [`**First synthesized**: ${trace.firstSynthesized}`] : []),
|
|
109
|
+
`**Sources**: ${line(trace.sources.join('; '))}`,
|
|
110
|
+
// Investigation detail stays in evidence; active guidance retains only provenance metadata.
|
|
111
|
+
'',
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
/** Recognize only known generated family preambles; unknown prose remains user content. */
|
|
115
|
+
function isLearningScaffolding(body, familyPath) {
|
|
116
|
+
const name = node_path_1.default.basename(familyPath);
|
|
117
|
+
const match = name.match(/-(preferences|mistake-patterns|validated-patterns|manager-coaching)(\.cold)?\.md$/);
|
|
118
|
+
if (!match)
|
|
119
|
+
return false;
|
|
120
|
+
const family = match[1];
|
|
121
|
+
const title = family.replace(/-/g, ' ');
|
|
122
|
+
const cold = !!match[2];
|
|
123
|
+
const descriptions = {
|
|
124
|
+
preferences: ['This file captures durable communication and interaction preferences observed across sessions.', 'This file captures working preferences common across multiple users. Run `organizational-learning-synthesis` to populate it.'],
|
|
125
|
+
'mistake-patterns': ['This file is an execution playbook for future agents. Keep it focused on durable behavior and decision quality.', 'This file captures recurring failure modes observed across multiple users in the org. Run `organizational-learning-synthesis` to populate it.'],
|
|
126
|
+
'validated-patterns': ['This file captures validated decisions, near-misses that were caught, and moments where past learnings successfully fired. These are behaviors to reinforce.'],
|
|
127
|
+
'manager-coaching': ['This file captures coaching and reflections about your management of AI agents - patterns you have developed (or been coached on) that make you a more effective practitioner. The audience is **you**, not the AI agent.', 'This file captures coaching patterns shared across managers in the org - guidance that managers have received or developed about how to work effectively with AI agents. The audience is the human manager, not the AI agent. Run `organizational-learning-synthesis` to populate it.'],
|
|
128
|
+
};
|
|
129
|
+
const heading = (value) => {
|
|
130
|
+
if (['# Learnings', '# Synthesized Learnings'].includes(value) || (family === 'mistake-patterns' && value === '# Prioritized Learnings'))
|
|
131
|
+
return true;
|
|
132
|
+
if (!value.startsWith('# '))
|
|
133
|
+
return false;
|
|
134
|
+
let text = value.slice(2);
|
|
135
|
+
const author = text.match(/ [\-\u2014] ([a-zA-Z0-9@._+-]+)$/);
|
|
136
|
+
if (author) {
|
|
137
|
+
if (!name.startsWith(author[1] + '-'))
|
|
138
|
+
return false;
|
|
139
|
+
text = text.slice(0, -author[0].length);
|
|
140
|
+
}
|
|
141
|
+
if (/^Cold-Tier /i.test(text)) {
|
|
142
|
+
if (!cold)
|
|
143
|
+
return false;
|
|
144
|
+
text = text.slice(10);
|
|
145
|
+
}
|
|
146
|
+
if (/^Org /i.test(text)) {
|
|
147
|
+
if (!name.startsWith('org-'))
|
|
148
|
+
return false;
|
|
149
|
+
text = text.slice(4);
|
|
150
|
+
}
|
|
151
|
+
const domain = text.match(/ \(([a-zA-Z][a-zA-Z -]*)\)$/);
|
|
152
|
+
if (domain) {
|
|
153
|
+
if (!name.toLowerCase().endsWith('-' + domain[1].toLowerCase().replace(/ /g, '-') + '-' + family + (cold ? '.cold' : '') + '.md'))
|
|
154
|
+
return false;
|
|
155
|
+
text = text.slice(0, -domain[0].length);
|
|
156
|
+
}
|
|
157
|
+
text = text.toLowerCase().replace(/-/g, ' ');
|
|
158
|
+
if (text === title)
|
|
159
|
+
return true;
|
|
160
|
+
const prefix = text.endsWith(' ' + title) ? text.slice(0, -title.length - 1).replace(/ /g, '-') : '';
|
|
161
|
+
return !!prefix && name.toLowerCase().endsWith('-' + prefix + '-' + family + (cold ? '.cold' : '') + '.md');
|
|
162
|
+
};
|
|
163
|
+
return body.split(/\r?\n/).every(line => {
|
|
164
|
+
const value = line.trim();
|
|
165
|
+
if (!value || value === '---' || heading(value) || descriptions[family].includes(value) || /^Last (?:synthesized|updated): \d{4}-\d{2}-\d{2}$/.test(value))
|
|
166
|
+
return true;
|
|
167
|
+
if (cold && (value === 'Entries whose effective score has fallen below the injection threshold. They stay readable and return to the active file the first time their problem recurs.'
|
|
168
|
+
|| /^Entries below the active-tier score threshold \(\d+(?:\.\d+)?\)\. Preserved for re-activation if recurrence rises\.$/.test(value)))
|
|
169
|
+
return true;
|
|
170
|
+
const sections = {
|
|
171
|
+
'mistake-patterns': ['## Severity Definitions', '## Critical Priorities', '- Critical: recurring across 3+ users, causes customer impact or significant rework.', '- Important: recurring across 2+ users, degrades quality or velocity.', '- Monitor: emerging pattern, not yet widespread but worth tracking.', '- Critical: recurring failure mode that causes rework, trust loss, or architectural mistakes.', '- Important: meaningful quality risk that frequently degrades output.', '- Monitor: lower-frequency risk to track and prevent from growing.'],
|
|
172
|
+
'validated-patterns': ['## Severity Definitions', '## Patterns to Reinforce', '- HIGH: validated decision that prevented major rework or unblocked a high-stakes path (reusable across many future contexts).', '- MED: validated context-specific judgment call (reusable when same context recurs).', '- LOW: validated micro-correctness (useful but narrow).'],
|
|
173
|
+
};
|
|
174
|
+
if (sections[family]?.includes(value))
|
|
175
|
+
return true;
|
|
176
|
+
const domain = value.match(/^([A-Za-z][A-Za-z-]*)-domain (manager coaching|validated patterns)\. (.+)$/);
|
|
177
|
+
const detail = family === 'manager-coaching' ? 'Feedback for how the manager should continue or improve managing AI on ' + domain?.[1].toLowerCase() + ' work.' : 'Durable judgment calls and successful unusual-but-correct decisions worth reproducing.';
|
|
178
|
+
return !!domain && domain[2] === title && name.toLowerCase().endsWith('-' + domain[1].toLowerCase() + '-' + family + '.md')
|
|
179
|
+
&& domain[3] === detail + ' These load only when an ' + domain[1].toLowerCase() + '-domain job runs.';
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const LEARNING_ENTRY_ID_LINE = /^\*\*Id\*\*:\s*(L-[0-9A-Za-z]{6,24})\s*$/;
|
|
4
|
+
function isLearningEntryId(value) { return typeof value === 'string' && /^L-[0-9A-Za-z]{6,24}$/.test(value); }
|
|
5
|
+
/** Reader/corpus identity accepts stable handwritten IDs; approved mutation IDs remain narrower below. */
|
|
6
|
+
function learningEntryIdentity(content) {
|
|
7
|
+
const declarations = content.split(/\r?\n/).map(line => line.trim()).filter(line => /^\*\*Id\*\*:/i.test(line));
|
|
8
|
+
const id = declarations[0]?.match(LEARNING_ENTRY_ID_LINE)?.[1] ?? null;
|
|
9
|
+
return { id: declarations.length === 1 ? id : null, ambiguous: declarations.length > 0 && (!id || declarations.length !== 1) };
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
// Read the shared author's current entry format for identity and update guards.
|
|
4
13
|
function learningEntryBlocks(body) {
|
|
5
14
|
const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
|
|
@@ -24,4 +33,4 @@ function learningEntryMetadata(content) {
|
|
|
24
33
|
return metadata;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
module.exports = { learningEntryMetadata, learningEntryBlocks };
|
|
36
|
+
module.exports = { learningEntryMetadata, learningEntryBlocks, learningEntryIdentity, isLearningEntryId, LEARNING_ENTRY_ID_LINE };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.summarizeLearningOutcomes = void 0;
|
|
7
|
+
exports.recordOutcomeAssessment = recordOutcomeAssessment;
|
|
8
|
+
exports.readOutcomeAssessments = readOutcomeAssessments;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const learning_usage_store_1 = require("../../../src/local-mcp-server/learning-usage-store");
|
|
12
|
+
const owner_identity_1 = require("./owner-identity");
|
|
13
|
+
const proposal_queue_1 = require("./proposal-queue");
|
|
14
|
+
const completion_state_1 = require("./completion-state");
|
|
15
|
+
const artifact_paths_1 = require("../artifact-paths");
|
|
16
|
+
const learning_source_record_1 = require("../learning-usage/learning-source-record");
|
|
17
|
+
const corpus_audit_1 = require("./corpus-audit");
|
|
18
|
+
const outcome_types_1 = require("./outcome-types");
|
|
19
|
+
var outcome_types_2 = require("./outcome-types");
|
|
20
|
+
Object.defineProperty(exports, "summarizeLearningOutcomes", { enumerable: true, get: function () { return outcome_types_2.summarizeLearningOutcomes; } });
|
|
21
|
+
const { learningEntryBlocks, learningEntryMetadata, learningEntryIdentity } = require('./entry-format');
|
|
22
|
+
function resolveAssessmentHomes(run, input, options) {
|
|
23
|
+
const homes = new Map();
|
|
24
|
+
for (const owner of [run.owner, input.learning.owner, ...input.sourceRefs.map(source => source.owner)]) {
|
|
25
|
+
const home = options.resolveHome(owner);
|
|
26
|
+
(0, owner_identity_1.assertSameOwner)(owner, home.owner);
|
|
27
|
+
homes.set((0, owner_identity_1.ownerKey)(owner), home);
|
|
28
|
+
}
|
|
29
|
+
return homes;
|
|
30
|
+
}
|
|
31
|
+
function resolveAssessmentSources(input, homes) {
|
|
32
|
+
return input.sourceRefs.map(ref => {
|
|
33
|
+
const home = homes.get((0, owner_identity_1.ownerKey)(ref.owner));
|
|
34
|
+
const originalFile = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join((0, owner_identity_1.ownerArtifactRoot)(home), ref.relativePath));
|
|
35
|
+
let file = originalFile;
|
|
36
|
+
if (!node_fs_1.default.existsSync(file)) {
|
|
37
|
+
const paths = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
|
|
38
|
+
const archive = (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.raw) ? paths.archiveRaw
|
|
39
|
+
: (0, completion_state_1.sourcePathKey)(node_path_1.default.dirname(file)) === (0, completion_state_1.sourcePathKey)(paths.retrospectives) ? paths.archiveRetrospectives : undefined;
|
|
40
|
+
if (archive)
|
|
41
|
+
file = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join(archive, node_path_1.default.basename(file)));
|
|
42
|
+
}
|
|
43
|
+
return { ref, home, file, archived: file !== originalFile };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function verifyAssessmentLearning(input, home, target) {
|
|
47
|
+
if (!node_fs_1.default.existsSync(target) || node_fs_1.default.lstatSync(target).isSymbolicLink())
|
|
48
|
+
throw new Error('assessment-learning-missing');
|
|
49
|
+
const entries = learningEntryBlocks(node_fs_1.default.readFileSync(target, 'utf8')).map(block => ({ ...block, ...learningEntryIdentity(block.text) }));
|
|
50
|
+
if (entries.some(block => block.ambiguous))
|
|
51
|
+
throw new Error('assessment-learning-missing-or-ambiguous');
|
|
52
|
+
const blocks = entries.filter(block => block.id === input.learning.entryId);
|
|
53
|
+
if (blocks.length !== 1)
|
|
54
|
+
throw new Error('assessment-learning-missing-or-ambiguous');
|
|
55
|
+
const domain = blocks[0].text.match(/^\*\*Domain\*\*:\s*([^\r\n]+)$/m)?.[1].trim();
|
|
56
|
+
(0, owner_identity_1.assertLearningTargetPath)(input.learning, domain);
|
|
57
|
+
const occurrences = (0, corpus_audit_1.collectLearningCorpus)([home], [input.learning.learningSubject]).entries.filter(entry => {
|
|
58
|
+
if (entry.ref.entryId !== input.learning.entryId)
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
(0, owner_identity_1.assertLearningTargetPath)({ ...entry.ref, learningSubject: input.learning.learningSubject });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
} // Shared org guidance is a distinct subject in a Project home.
|
|
67
|
+
});
|
|
68
|
+
if (occurrences.length !== 1)
|
|
69
|
+
throw new Error('assessment-learning-identity-ambiguous');
|
|
70
|
+
const metadata = learningEntryMetadata(blocks[0].text);
|
|
71
|
+
const introduced = metadata['First synthesized'] || metadata['First recorded'];
|
|
72
|
+
if (!introduced || !Number.isFinite(Date.parse(introduced)))
|
|
73
|
+
throw new Error('guidance-date-unavailable');
|
|
74
|
+
if (Date.parse(introduced) > Date.parse(input.observedAt) || Date.parse(introduced) > Date.parse(input.guidanceExistedAt))
|
|
75
|
+
throw new Error('guidance-after-incident');
|
|
76
|
+
}
|
|
77
|
+
function verifyAssessmentSources(sources, state) {
|
|
78
|
+
for (const source of sources) {
|
|
79
|
+
if (!node_fs_1.default.existsSync(source.file) || node_fs_1.default.lstatSync(source.file).isSymbolicLink())
|
|
80
|
+
throw new Error('assessment-source-unavailable');
|
|
81
|
+
const body = node_fs_1.default.readFileSync(source.file, 'utf8');
|
|
82
|
+
// Normal archival only fills this existing frontmatter field; source identity and all evidence bytes remain bound.
|
|
83
|
+
const originalBody = source.archived ? body.replace(/^(synthesized:)[^\r\n]*/m, '$1') : body;
|
|
84
|
+
if ((0, proposal_queue_1.hashContent)(body) !== source.ref.sha256 && (0, proposal_queue_1.hashContent)(originalBody) !== source.ref.sha256)
|
|
85
|
+
throw new Error('assessment-source-hash-conflict');
|
|
86
|
+
const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(body, source.file, (0, owner_identity_1.ownerArtifactRoot)(source.home), source.home.contentRoot);
|
|
87
|
+
if (parsed.status !== 'valid')
|
|
88
|
+
throw new Error(`invalid-assessment-source: ${parsed.reason}`);
|
|
89
|
+
if (parsed.record.sourceId !== source.ref.sourceId
|
|
90
|
+
|| !(0, completion_state_1.sourceAuthorAllowed)(state, parsed.record.author, source.home.owner))
|
|
91
|
+
throw new Error('invalid-assessment-source');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function replayAssessment(run, state, pending, prior) {
|
|
95
|
+
if (pending?.digest === prior.digest)
|
|
96
|
+
run.commitOutcomeAssessment(prior.digest, state.sequence);
|
|
97
|
+
return prior;
|
|
98
|
+
}
|
|
99
|
+
function commitAssessment(transaction) {
|
|
100
|
+
const { run, input, state, pending, options, home, target, sources } = transaction;
|
|
101
|
+
// File locks also coordinate callers with different Manager coordination paths.
|
|
102
|
+
// Refresh after acquiring them, before recording any recoverable intent.
|
|
103
|
+
const prior = (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { key: (0, outcome_types_1.assessmentKey)(input) })[0];
|
|
104
|
+
if (prior && (0, outcome_types_1.assessmentContent)(prior) === (0, outcome_types_1.assessmentContent)(input))
|
|
105
|
+
return replayAssessment(run, state, pending, prior);
|
|
106
|
+
const reservations = (0, completion_state_1.readRunReservations)([run.directory, ...options.runDirectories], run.file);
|
|
107
|
+
if (reservations.assessmentKeys.includes((0, outcome_types_1.assessmentKey)(input)))
|
|
108
|
+
throw new Error('assessment-reserved-by-pending-run');
|
|
109
|
+
if ((prior?.digest ?? null) !== input.expectedPriorDigest)
|
|
110
|
+
throw new Error('stale-assessment-conflict');
|
|
111
|
+
verifyAssessmentLearning(input, home, target);
|
|
112
|
+
verifyAssessmentSources(sources, state);
|
|
113
|
+
const record = pending ?? { ...structuredClone(input), revision: (prior?.revision ?? 0) + 1,
|
|
114
|
+
assessedAt: new Date().toISOString(), reviewer: state.reviewer, runId: state.runId, digest: '' };
|
|
115
|
+
record.digest = (0, outcome_types_1.outcomeAssessmentDigest)(record);
|
|
116
|
+
if (!pending)
|
|
117
|
+
run.prepareOutcomeAssessment(record, state.sequence, sources.map(source => source.file));
|
|
118
|
+
options.afterStage?.('assessment-prepared');
|
|
119
|
+
const persisted = (0, learning_usage_store_1.recordAssessedOutcome)(record);
|
|
120
|
+
options.afterStage?.('assessment-stored');
|
|
121
|
+
run.commitOutcomeAssessment(record.digest, run.read().sequence);
|
|
122
|
+
options.afterStage?.('assessment-committed');
|
|
123
|
+
return persisted;
|
|
124
|
+
}
|
|
125
|
+
function withAssessmentFileLocks(transaction) {
|
|
126
|
+
const files = [...new Set([transaction.target, ...transaction.sources.map(source => source.file)])].sort();
|
|
127
|
+
const locked = (index) => index < files.length
|
|
128
|
+
? (0, proposal_queue_1.withFileLock)(files[index], () => locked(index + 1)) : commitAssessment(transaction);
|
|
129
|
+
return locked(0);
|
|
130
|
+
}
|
|
131
|
+
/** Commit semantic judgments supplied by Sleep; no prose, title or retrospective-ID matching. */
|
|
132
|
+
function recordOutcomeAssessment(run, input, expectedSequence, options) {
|
|
133
|
+
(0, outcome_types_1.validateOutcomeAssessmentInput)(input);
|
|
134
|
+
(0, owner_identity_1.assertLearningTargetPath)(input.learning);
|
|
135
|
+
return (0, proposal_queue_1.withFileLock)(options.coordinationFile || node_path_1.default.join(run.directory, 'owner-application'), () => {
|
|
136
|
+
const state = run.read();
|
|
137
|
+
if (!Number.isInteger(expectedSequence) || state.sequence !== expectedSequence)
|
|
138
|
+
throw new Error('stale-sequence');
|
|
139
|
+
if (state.finalization || state.checkpoint?.receipt.finalizationState === 'pending')
|
|
140
|
+
throw new Error('finalization-started');
|
|
141
|
+
const homes = resolveAssessmentHomes(run, input, options);
|
|
142
|
+
if (!(0, completion_state_1.sourceAuthorAllowed)(state, input.learning.learningSubject, input.learning.owner))
|
|
143
|
+
throw new Error('assessment-subject-conflict');
|
|
144
|
+
const prior = (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { key: (0, outcome_types_1.assessmentKey)(input) })[0];
|
|
145
|
+
const pending = state.pendingOutcomeAssessments?.find(record => (0, outcome_types_1.assessmentKey)(record) === (0, outcome_types_1.assessmentKey)(input));
|
|
146
|
+
if (pending && (0, outcome_types_1.assessmentContent)(pending) !== (0, outcome_types_1.assessmentContent)(input))
|
|
147
|
+
throw new Error('pending-assessment-conflict');
|
|
148
|
+
// Exact replay remains valid after sources have archived and detailed evidence expires.
|
|
149
|
+
if (prior && (0, outcome_types_1.assessmentContent)(prior) === (0, outcome_types_1.assessmentContent)(input))
|
|
150
|
+
return replayAssessment(run, state, pending, prior);
|
|
151
|
+
if ((prior?.digest ?? null) !== input.expectedPriorDigest)
|
|
152
|
+
throw new Error('stale-assessment-conflict');
|
|
153
|
+
const home = homes.get((0, owner_identity_1.ownerKey)(input.learning.owner));
|
|
154
|
+
const target = (0, owner_identity_1.ownerArtifactPath)(home, input.learning.relativePath);
|
|
155
|
+
const sources = resolveAssessmentSources(input, homes);
|
|
156
|
+
return withAssessmentFileLocks({ run, input, state, pending, options, home, target, sources });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Durable usage history outlives transient run journals, receipts and original source retention. */
|
|
160
|
+
function readOutcomeAssessments(homes, principal) {
|
|
161
|
+
return (0, learning_usage_store_1.readAssessedOutcomes)(undefined, { owners: homes.map(home => home.owner), principal });
|
|
162
|
+
}
|