fraim 2.0.345 → 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.
Files changed (32) hide show
  1. package/dist/registry/scripts/learning/completion-state.js +123 -4
  2. package/dist/registry/scripts/learning/corpus-audit.js +205 -0
  3. package/dist/registry/scripts/learning/entry-authoring.js +181 -0
  4. package/dist/registry/scripts/learning/entry-format.js +10 -1
  5. package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
  6. package/dist/registry/scripts/learning/outcome-types.js +92 -0
  7. package/dist/registry/scripts/learning/owner-identity.js +148 -0
  8. package/dist/registry/scripts/learning/proposal-queue.js +81 -0
  9. package/dist/registry/scripts/learning/run-application.js +1003 -0
  10. package/dist/registry/scripts/learning/run-publication.js +364 -0
  11. package/dist/registry/scripts/learning/run-record.js +895 -0
  12. package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
  13. package/dist/src/cli/commands/learning.js +64 -0
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
  15. package/dist/src/cli/fraim.js +2 -0
  16. package/dist/src/cli/learning/inventory.js +285 -0
  17. package/dist/src/cli/learning/queue-operations.js +199 -0
  18. package/dist/src/cli/learning/runtime.js +82 -0
  19. package/dist/src/cli/learning/usage.js +277 -0
  20. package/dist/src/core/fraim-config-schema.generated.js +0 -4
  21. package/dist/src/local-mcp-server/learning-context-builder.js +141 -477
  22. package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
  23. package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
  24. package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
  25. package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
  26. package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
  27. package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
  28. package/dist/src/local-mcp-server/stdio-server.js +0 -48
  29. package/package.json +11 -1
  30. package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
  31. package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
  32. 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;
@@ -16,6 +19,34 @@ const node_path_1 = __importDefault(require("node:path"));
16
19
  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");
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
+ }
19
50
  const sourcePathKey = (file) => {
20
51
  const value = node_path_1.default.resolve(file).replace(/\\/g, '/');
21
52
  return process.platform === 'win32' ? value.toLowerCase() : value;
@@ -144,9 +175,10 @@ function readRunJournal(file) {
144
175
  }
145
176
  return { events, lastHash, format: 'current' };
146
177
  }
147
- /** Only exact, hash-chain-verified current archive plans reserve files. Older plans are outside this lifecycle. */
148
- function readSourceReservations(runDirectories, excludingRunFile) {
178
+ /** Current archive plans and prepared assessments share one verified reservation inventory. */
179
+ function readRunReservations(runDirectories, excludingRunFile) {
149
180
  const reserved = new Set();
181
+ const assessmentKeys = new Set();
150
182
  for (const directory of new Set(runDirectories.map(dir => node_path_1.default.resolve(dir)))) {
151
183
  let names;
152
184
  try {
@@ -167,6 +199,23 @@ function readSourceReservations(runDirectories, excludingRunFile) {
167
199
  const journal = readRunJournal(file);
168
200
  if (journal.format === 'historical')
169
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
+ }
170
219
  for (const event of journal.events)
171
220
  if (event.payload.type === 'source-completion') {
172
221
  for (const source of event.payload.data) {
@@ -182,7 +231,10 @@ function readSourceReservations(runDirectories, excludingRunFile) {
182
231
  }
183
232
  }
184
233
  }
185
- return [...reserved].sort();
234
+ return { sources: [...reserved].sort(), assessmentKeys: [...assessmentKeys].sort() };
235
+ }
236
+ function readSourceReservations(runDirectories, excludingRunFile) {
237
+ return readRunReservations(runDirectories, excludingRunFile).sources;
186
238
  }
187
239
  exports.COMPLETION_START = '<!-- fraim-sleep-decisions:v1 -->';
188
240
  exports.COMPLETION_END = '<!-- /fraim-sleep-decisions -->';
@@ -233,7 +285,7 @@ function decodeCompletion(markdown, expected = {}) {
233
285
  catch {
234
286
  fail();
235
287
  }
236
- if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources', 'sourceParticipants']) || 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)
237
289
  || !['pending', 'complete'].includes(state.finalizationState) || !Array.isArray(state.decisions) || !Array.isArray(state.sourceDispositions) || !Array.isArray(state.cleanup))
238
290
  fail();
239
291
  if ((expected.runId && expected.runId !== state.runId) || (expected.owner && !sameOwner(expected.owner, state.owner)))
@@ -248,8 +300,31 @@ function decodeCompletion(markdown, expected = {}) {
248
300
  fail();
249
301
  if (state.processedSources !== undefined)
250
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
+ }
251
314
  if (state.sourceParticipants !== undefined)
252
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
+ }
253
328
  const ids = new Set();
254
329
  for (const item of state.decisions) {
255
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)
@@ -281,6 +356,50 @@ function decodeCompletion(markdown, expected = {}) {
281
356
  fail();
282
357
  }
283
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();
361
+ if (state.corpusAudit !== undefined) {
362
+ try {
363
+ (0, corpus_audit_1.validateCorpusAudit)(state.corpusAudit);
364
+ }
365
+ catch {
366
+ fail();
367
+ }
368
+ for (const item of state.corpusAudit.dispositions)
369
+ for (const binding of item.actionBindings) {
370
+ const decision = decisions.get(binding.actionId);
371
+ if (!decision || decision.digest !== binding.digest || decision.revision !== binding.revision || decision.decision.reviewer !== state.corpusAudit.reviewer)
372
+ fail();
373
+ }
374
+ }
375
+ if (state.reconciliationHistory !== undefined) {
376
+ if (!Array.isArray(state.reconciliationHistory))
377
+ fail();
378
+ const seen = new Set();
379
+ const validRef = (ref) => keys(ref, ['owner', 'relativePath', 'entryId', 'learningSubject']) && ownerValid(ref.owner)
380
+ && typeof ref.relativePath === 'string' && ref.relativePath.startsWith('learnings/') && !ref.relativePath.includes('\\') && !ref.relativePath.includes(':')
381
+ && !ref.relativePath.split('/').some((part) => !part || part === '.' || part === '..') && /^L-[a-f0-9]{10}$/.test(ref.entryId)
382
+ && (ref.learningSubject === undefined || text(ref.learningSubject));
383
+ for (const history of state.reconciliationHistory) {
384
+ const decision = decisions.get(history.actionId);
385
+ if (!keys(history, ['actionId', 'revision', 'digest', 'target', 'resultHash', 'occurrences']) || !decision || seen.has(history.actionId)
386
+ || decision.decision.action !== 'approve' || history.revision !== decision.revision || history.digest !== decision.digest
387
+ || !validRef(history.target) || canonicalJournal(history.target) !== canonicalJournal(decision.target) || !hash(history.resultHash)
388
+ || !Array.isArray(history.occurrences) || !history.occurrences.length)
389
+ fail();
390
+ seen.add(history.actionId);
391
+ const locations = new Set();
392
+ for (const occurrence of history.occurrences) {
393
+ if (!keys(occurrence, ['ref', 'sha256', 'before']) || !validRef(occurrence.ref) || occurrence.ref.entryId !== history.target.entryId
394
+ || !text(occurrence.before) || !hash(occurrence.sha256) || (0, node_crypto_1.createHash)('sha256').update(occurrence.before).digest('hex') !== occurrence.sha256)
395
+ fail();
396
+ const location = canonicalJournal([occurrence.ref.owner, occurrence.ref.relativePath]);
397
+ if (locations.has(location))
398
+ fail();
399
+ locations.add(location);
400
+ }
401
+ }
402
+ }
284
403
  const outcome = (item) => item.decision.action === 'approve' ? 'applied' : 'rejected';
285
404
  const dispositions = new Set();
286
405
  for (const source of state.sourceDispositions) {
@@ -0,0 +1,205 @@
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.corpusEntryKey = corpusEntryKey;
7
+ exports.collectLearningCorpus = collectLearningCorpus;
8
+ exports.validateCorpusAudit = validateCorpusAudit;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_crypto_1 = require("node:crypto");
12
+ const owner_identity_1 = require("./owner-identity");
13
+ const { learningEntryBlocks, learningEntryIdentity, isLearningEntryId } = require('./entry-format');
14
+ const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
15
+ ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item);
16
+ const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
17
+ const keys = (value, allowed) => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => allowed.includes(key));
18
+ function corpusEntryKey(entry) {
19
+ return (0, owner_identity_1.ownerKey)(entry.ref.owner) + ':' + entry.ref.relativePath + ':' + (entry.ref.entryId ?? `unassigned:${entry.occurrence}`);
20
+ }
21
+ function corpusFingerprint(homes, entries) { return hash(stable({ homes, entries })); }
22
+ /** Only persisted identity and bytes are mechanical; audience and semantic conflicts require review. */
23
+ function collectLearningCorpus(homes, subjects, readerRoots = []) {
24
+ const entries = [], errors = [], seen = new Set();
25
+ const identities = [...new Map(homes.map(home => [(0, owner_identity_1.ownerKey)(home.owner), home.owner])).values()].sort((a, b) => (0, owner_identity_1.ownerKey)(a).localeCompare((0, owner_identity_1.ownerKey)(b)));
26
+ const escaped = [...new Set([...subjects, 'org'])].map(subject => subject.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
27
+ const family = new RegExp(`^(?:${escaped.join('|')})-(?:[a-z][a-z-]*-)?(?:preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`);
28
+ const excluded = new Set(['raw', 'retrospectives', 'archive', 'review-queue', 'review-archive', 'review-evidence']);
29
+ for (const home of [...homes].sort((a, b) => (0, owner_identity_1.ownerKey)(a.owner).localeCompare((0, owner_identity_1.ownerKey)(b.owner)))) {
30
+ const walk = (directory) => {
31
+ try {
32
+ if (node_fs_1.default.lstatSync(directory).isSymbolicLink())
33
+ throw new Error('symlink-corpus-not-followed');
34
+ for (const item of node_fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
35
+ const file = node_path_1.default.join(directory, item.name);
36
+ if (item.isSymbolicLink()) {
37
+ if (!excluded.has(item.name))
38
+ errors.push({ path: file.replace(/\\/g, '/'), reason: 'symlink-corpus-not-followed' });
39
+ continue;
40
+ }
41
+ if (item.isDirectory()) {
42
+ if (!excluded.has(item.name))
43
+ walk(file);
44
+ continue;
45
+ }
46
+ if (!family.test(item.name))
47
+ continue;
48
+ try {
49
+ if (item.isSymbolicLink())
50
+ throw new Error('symlink-corpus-not-followed');
51
+ const real = node_fs_1.default.realpathSync(file);
52
+ const key = process.platform === 'win32' ? real.toLowerCase() : real;
53
+ if (seen.has(key))
54
+ continue;
55
+ seen.add(key);
56
+ const relativePath = node_path_1.default.relative(home.contentRoot, file).replace(/\\/g, '/');
57
+ (0, owner_identity_1.ownerArtifactPath)(home, relativePath);
58
+ const body = node_fs_1.default.readFileSync(file, 'utf8');
59
+ if (/^<{7}/m.test(body))
60
+ throw new Error('corpus-merge-conflict');
61
+ const blocks = learningEntryBlocks(body), ids = new Set();
62
+ let unassigned = 0;
63
+ for (const block of blocks) {
64
+ // Match the runtime reader's handwritten identity contract.
65
+ // Approved mutations still use the author's narrower generated-ID schema.
66
+ const { id, ambiguous } = learningEntryIdentity(block.text);
67
+ if (ambiguous || (id && ids.has(id))
68
+ || !/^#{2,6}\s+\[P-(CRITICAL|HIGH|MED|LOW)\]\s+\S/.test(block.text)) {
69
+ errors.push({ path: file.replace(/\\/g, '/'), reason: 'ambiguous-corpus-entry' });
70
+ continue;
71
+ }
72
+ if (id)
73
+ ids.add(id);
74
+ entries.push({ ref: { owner: home.owner, relativePath, ...(id ? { entryId: id } : {}) }, sha256: hash(block.text),
75
+ ...(!id ? { occurrence: unassigned++ } : {}) });
76
+ }
77
+ }
78
+ catch (error) {
79
+ errors.push({ path: file.replace(/\\/g, '/'), reason: error.code || error.message });
80
+ }
81
+ }
82
+ }
83
+ catch (error) {
84
+ if (error.code !== 'ENOENT')
85
+ errors.push({ path: directory.replace(/\\/g, '/'), reason: error.code || error.message });
86
+ }
87
+ };
88
+ try {
89
+ walk((0, owner_identity_1.ownerArtifactPath)(home, 'learnings'));
90
+ }
91
+ catch (error) {
92
+ errors.push({ path: home.contentRoot, reason: error.message });
93
+ }
94
+ }
95
+ // Legacy reader locations are explicitly discoverable but cannot acquire a
96
+ // writable OwnerRef just by being present. Report every unowned family file.
97
+ const inspectReaderRoot = (directory) => {
98
+ try {
99
+ if (node_fs_1.default.lstatSync(directory).isSymbolicLink())
100
+ throw new Error('symlink-corpus-not-followed');
101
+ for (const item of node_fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
102
+ const file = node_path_1.default.join(directory, item.name);
103
+ if (excluded.has(item.name))
104
+ continue;
105
+ if (item.isSymbolicLink()) {
106
+ errors.push({ path: file.replace(/\\/g, '/'), reason: 'symlink-corpus-not-followed' });
107
+ continue;
108
+ }
109
+ if (item.isDirectory()) {
110
+ inspectReaderRoot(file);
111
+ continue;
112
+ }
113
+ if (!family.test(item.name))
114
+ continue;
115
+ try {
116
+ const real = node_fs_1.default.realpathSync(file), key = process.platform === 'win32' ? real.toLowerCase() : real;
117
+ if (seen.has(key))
118
+ continue;
119
+ seen.add(key);
120
+ errors.push({ path: file.replace(/\\/g, '/'), reason: 'reader-live-home-unowned' });
121
+ }
122
+ catch (error) {
123
+ errors.push({ path: file.replace(/\\/g, '/'), reason: error.code || error.message });
124
+ }
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (error.code !== 'ENOENT')
129
+ errors.push({ path: directory.replace(/\\/g, '/'), reason: error.code || error.message });
130
+ }
131
+ };
132
+ for (const directory of [...new Set(readerRoots)].sort()) {
133
+ // An explicit configured alias of an owned learning root is already scanned.
134
+ try {
135
+ const key = (file) => { const real = node_fs_1.default.realpathSync(file); return process.platform === 'win32' ? real.toLowerCase() : real; };
136
+ const real = key(directory);
137
+ if (homes.some(home => { try {
138
+ return key(node_path_1.default.join(home.contentRoot, 'learnings')) === real;
139
+ }
140
+ catch {
141
+ return false;
142
+ } }))
143
+ continue;
144
+ }
145
+ catch { /* Missing/unreadable roots are handled by the scanner below. */ }
146
+ inspectReaderRoot(directory);
147
+ }
148
+ entries.sort((a, b) => corpusEntryKey(a).localeCompare(corpusEntryKey(b)));
149
+ const groups = new Map();
150
+ for (const entry of entries)
151
+ if (entry.ref.entryId)
152
+ groups.set(entry.ref.entryId, [...(groups.get(entry.ref.entryId) || []), entry]);
153
+ const duplicates = [...groups].filter(([, occurrences]) => occurrences.length > 1).map(([entryId, occurrences]) => ({ entryId, occurrences })).sort((a, b) => a.entryId.localeCompare(b.entryId));
154
+ return { schemaVersion: 1, homes: identities, entries, duplicates, fingerprint: corpusFingerprint(identities, entries), complete: !errors.length, errors };
155
+ }
156
+ /** Shared by journal replay and the strict compact receipt decoder. No filesystem access. */
157
+ function validateCorpusAudit(audit, reviewer) {
158
+ const fail = () => { throw new Error('invalid-corpus-audit'); };
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
+ || !Array.isArray(audit.snapshot.homes) || !Array.isArray(audit.snapshot.entries) || !Array.isArray(audit.dispositions))
163
+ fail();
164
+ const snapshot = audit.snapshot;
165
+ for (const owner of snapshot.homes)
166
+ (0, owner_identity_1.validateOwner)(owner);
167
+ if (new Set(snapshot.homes.map(owner_identity_1.ownerKey)).size !== snapshot.homes.length || snapshot.fingerprint !== corpusFingerprint(snapshot.homes, snapshot.entries))
168
+ fail();
169
+ const entries = new Map();
170
+ for (const entry of snapshot.entries) {
171
+ if (!keys(entry, ['ref', 'sha256', 'occurrence']) || !keys(entry.ref, ['owner', 'relativePath', 'entryId']))
172
+ fail();
173
+ (0, owner_identity_1.validateOwner)(entry.ref?.owner);
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 : !isLearningEntryId(entry.ref.entryId) || entry.occurrence !== undefined)
176
+ || !/^learnings\//.test(entry.ref.relativePath) || /[\\:]/.test(entry.ref.relativePath) || entry.ref.relativePath.split('/').some(part => !part || part === '.' || part === '..')
177
+ || !/^[a-f0-9]{64}$/.test(entry.sha256) || entries.has(corpusEntryKey(entry)))
178
+ fail();
179
+ entries.set(corpusEntryKey(entry), entry);
180
+ }
181
+ const groups = new Map();
182
+ for (const entry of snapshot.entries)
183
+ if (entry.ref.entryId)
184
+ groups.set(entry.ref.entryId, [...(groups.get(entry.ref.entryId) || []), entry]);
185
+ const duplicates = [...groups].filter(([, occurrences]) => occurrences.length > 1).map(([entryId, occurrences]) => ({ entryId, occurrences })).sort((a, b) => a.entryId.localeCompare(b.entryId));
186
+ if (stable(snapshot.duplicates) !== stable(duplicates))
187
+ fail();
188
+ if (audit.dispositions.length !== entries.size)
189
+ fail();
190
+ const seen = new Set();
191
+ for (const item of audit.dispositions) {
192
+ if (!keys(item, ['ref', 'sha256', 'occurrence', 'outcome', 'rationale', 'actionIds', 'actionBindings']) || !keys(item.ref, ['owner', 'relativePath', 'entryId']))
193
+ fail();
194
+ const key = corpusEntryKey(item);
195
+ if (seen.has(key) || entries.get(key)?.sha256 !== item.sha256 || entries.get(key)?.occurrence !== item.occurrence || !['retained', 'action'].includes(item.outcome)
196
+ || typeof item.rationale !== 'string' || !item.rationale.trim() || !Array.isArray(item.actionIds) || new Set(item.actionIds).size !== item.actionIds.length
197
+ || (item.outcome === 'retained' ? item.actionIds.length !== 0 : item.actionIds.length === 0)
198
+ || !Array.isArray(item.actionBindings) || item.actionBindings.length !== item.actionIds.length)
199
+ fail();
200
+ for (const [index, binding] of item.actionBindings.entries())
201
+ if (!keys(binding, ['actionId', 'revision', 'digest']) || binding.actionId !== item.actionIds[index] || !Number.isInteger(binding.revision) || binding.revision < 1 || !/^[a-f0-9]{64}$/.test(binding.digest))
202
+ fail();
203
+ seen.add(key);
204
+ }
205
+ }
@@ -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 };