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
@@ -0,0 +1,131 @@
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.normalizeLearningSourcePath = normalizeLearningSourcePath;
7
+ exports.parseLearningSourceRecord = parseLearningSourceRecord;
8
+ /** Metadata and configured location contract for future learning inputs. */
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const artifact_paths_1 = require("../artifact-paths");
11
+ function normalizeLearningSourcePath(value) {
12
+ const normalized = node_path_1.default.resolve(value).replace(/\\/g, '/');
13
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
14
+ }
15
+ function validDate(value) {
16
+ if (!/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z)?$/.test(value))
17
+ return false;
18
+ const date = new Date(value);
19
+ return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value.slice(0, 10);
20
+ }
21
+ function safePart(value) {
22
+ return Boolean(value) && value !== '.' && value !== '..' && !/[\\/:*?"<>|\x00-\x1f]/.test(value)
23
+ && value.trim() === value && !/[. ]$/.test(value);
24
+ }
25
+ /** Resolve only within the supplied owner. Callers guard physical source containment. */
26
+ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, configuredPaths) {
27
+ const invalid = (reason) => ({ status: 'invalid', reason });
28
+ const normalized = content.replace(/^\uFEFF/, '');
29
+ const fm = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(normalized);
30
+ if (!fm) {
31
+ // Diagnose interrupted new records only in the leading metadata block.
32
+ // Never search document bodies for a marker that could be an example.
33
+ if (/^---\r?\n/.test(normalized)) {
34
+ for (const line of normalized.split(/\r?\n/).slice(1)) {
35
+ if (!line.trim())
36
+ continue;
37
+ if (!/^[ \t]*["']?[A-Za-z][A-Za-z0-9]*["']?[ \t]*:/.test(line))
38
+ break;
39
+ if (/^[ \t]*["']?artifactKind["']?[ \t]*:[ \t]*["']?learning-(?:raw|retrospective)["']?[ \t]*(?:#.*)?$/.test(line)) {
40
+ return invalid('Unclosed typed learning frontmatter');
41
+ }
42
+ }
43
+ }
44
+ return { status: 'unsupported', reason: 'No typed learning frontmatter' };
45
+ }
46
+ const fields = {};
47
+ const duplicates = new Set();
48
+ let malformedField = false;
49
+ for (const line of fm[1].split(/\r?\n/)) {
50
+ const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
51
+ if (!match) {
52
+ if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|usageSourceRef|synthesized)["']?\s*:/.test(line))
53
+ malformedField = true;
54
+ continue;
55
+ }
56
+ if (Object.prototype.hasOwnProperty.call(fields, match[1]))
57
+ duplicates.add(match[1]);
58
+ let value = match[2].trim();
59
+ if (!value.startsWith('"') && !value.startsWith("'"))
60
+ value = value.replace(/(?:^|\s+)#.*$/, '').trim();
61
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
62
+ value = value.slice(1, -1);
63
+ }
64
+ fields[match[1]] = value;
65
+ }
66
+ if (!fields.artifactKind)
67
+ return malformedField || Object.prototype.hasOwnProperty.call(fields, 'artifactKind')
68
+ ? invalid('Unsupported metadata key spelling')
69
+ : { status: 'unsupported', reason: 'No typed learning artifactKind' };
70
+ if (fields.artifactKind !== 'learning-raw' && fields.artifactKind !== 'learning-retrospective') {
71
+ return fields.artifactKind.startsWith('learning')
72
+ ? invalid('Invalid learning artifactKind')
73
+ : { status: 'unsupported', reason: 'Unsupported artifactKind' };
74
+ }
75
+ if (malformedField)
76
+ return invalid('Unsupported metadata key spelling');
77
+ if (duplicates.size)
78
+ return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
79
+ for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'usageSourceRef', 'synthesized']) {
80
+ if (!Object.prototype.hasOwnProperty.call(fields, field))
81
+ return invalid(`Missing metadata: ${field}`);
82
+ }
83
+ if (![fields.author, fields.job, fields.runId].every(safePart))
84
+ return invalid('Unsafe or empty author, job or runId');
85
+ if (!validDate(fields.date) || (fields.synthesized && !validDate(fields.synthesized)))
86
+ return invalid('Invalid date or synthesized date');
87
+ if (!node_path_1.default.isAbsolute(homeRoot) || !node_path_1.default.isAbsolute(contentRoot) || !node_path_1.default.isAbsolute(filePath))
88
+ return invalid('Absolute configured root and source path required');
89
+ const kind = fields.artifactKind === 'learning-raw' ? 'raw' : 'retrospective';
90
+ const basename = node_path_1.default.basename(filePath);
91
+ const prefix = `${fields.author}-${fields.runId}-`;
92
+ const nameKey = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
93
+ if (!nameKey(basename).startsWith(nameKey(prefix)) || !basename.endsWith('.md') || basename.length <= prefix.length + 3
94
+ || (kind === 'retrospective' && nameKey(basename) !== nameKey(`${prefix}${fields.job}-retrospective.md`))) {
95
+ return invalid('Filename does not match recorded author, runId and kind');
96
+ }
97
+ let paths;
98
+ try {
99
+ paths = configuredPaths ?? (0, artifact_paths_1.resolveLearningPaths)(homeRoot, contentRoot);
100
+ }
101
+ catch (error) {
102
+ return invalid(error instanceof Error ? error.message : 'Invalid artifact configuration');
103
+ }
104
+ const pendingPath = node_path_1.default.resolve(kind === 'raw' ? paths.raw : paths.retrospectives, basename).replace(/\\/g, '/');
105
+ const archivePath = node_path_1.default.resolve(kind === 'raw' ? paths.archiveRaw : paths.archiveRetrospectives, basename).replace(/\\/g, '/');
106
+ const current = normalizeLearningSourcePath(filePath);
107
+ const state = current === normalizeLearningSourcePath(pendingPath) ? 'pending'
108
+ : current === normalizeLearningSourcePath(archivePath) ? 'archived' : null;
109
+ if (!state)
110
+ return invalid('Source is outside its configured learning location');
111
+ if (state === 'archived' && !fields.synthesized)
112
+ return invalid('Archive has no successful processing date');
113
+ const reference = fields.usageSourceRef;
114
+ const relative = node_path_1.default.relative(homeRoot, pendingPath).replace(/\\/g, '/');
115
+ if (!relative || relative.split('/').some(part => !part || part === '.' || part === '..') || node_path_1.default.isAbsolute(relative))
116
+ return invalid('Source escapes its owning root');
117
+ if (!reference || /[\x00-\x1f]/.test(reference) || reference.includes('\\'))
118
+ return invalid('Invalid usageSourceRef');
119
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(fields.sourceId)) {
120
+ return invalid('Sources require an immutable sourceId UUID');
121
+ }
122
+ if (node_path_1.default.posix.isAbsolute(reference) || node_path_1.default.win32.isAbsolute(reference) || reference.includes(':')
123
+ || reference.split('/').some(part => !part || part === '.' || part === '..') || reference !== relative) {
124
+ return invalid('usageSourceRef must name the owner-relative pending path');
125
+ }
126
+ return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
127
+ date: fields.date, job: fields.job, runId: fields.runId, usageSourceRef: fields.usageSourceRef,
128
+ usageIdentity: `learning-source:${fields.sourceId}`,
129
+ sourceId: fields.sourceId,
130
+ synthesized: fields.synthesized, pendingPath, archivePath, state } };
131
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.learningCommand = void 0;
4
+ const commander_1 = require("commander");
5
+ const inventory_1 = require("../learning/inventory");
6
+ const queue_operations_1 = require("../learning/queue-operations");
7
+ const usage_1 = require("../learning/usage");
8
+ exports.learningCommand = new commander_1.Command('learning')
9
+ .description('Run FRAIM learning operations through the packaged CLI');
10
+ exports.learningCommand
11
+ .command('usage')
12
+ .description('Inspect historical outcomes, verify recovery, stamp IDs, or prune retained details')
13
+ .helpOption(false)
14
+ .allowUnknownOption(true)
15
+ .allowExcessArguments(true)
16
+ .action((_options, cmd) => {
17
+ try {
18
+ (0, usage_1.runUsageCommand)(cmd.args);
19
+ }
20
+ catch (err) {
21
+ process.stderr.write((err instanceof Error ? err.message : String(err)) + '\n');
22
+ process.exitCode = 1;
23
+ }
24
+ });
25
+ exports.learningCommand
26
+ .command('inventory')
27
+ .description('Discover and inventory learning sources for a project owner')
28
+ .option('--projectPath <path>', 'Project root path (defaults to current directory)')
29
+ .option('--owner <email>', 'Owner email address')
30
+ .option('--runId <id>', 'Org run ID for cross-participant inventory')
31
+ .option('--json', 'Output JSON (always true; flag kept for script compatibility)')
32
+ .action((options) => {
33
+ try {
34
+ const projectPath = options.projectPath || process.cwd();
35
+ const owner = options.owner || '';
36
+ if (!owner)
37
+ throw new Error('session-principal-required: pass --owner <email>');
38
+ const manifest = (0, inventory_1.discoverLearningSources)(projectPath, owner, options.runId);
39
+ process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
40
+ if (!manifest.complete)
41
+ process.exitCode = 2;
42
+ }
43
+ catch (err) {
44
+ const message = err instanceof Error ? err.message : String(err);
45
+ process.stderr.write(message + '\n');
46
+ process.exit(1);
47
+ }
48
+ });
49
+ exports.learningCommand
50
+ .command('queue')
51
+ .description('Propose, apply, assess outcomes, or inspect a learning run')
52
+ .allowUnknownOption(true)
53
+ .allowExcessArguments(true)
54
+ .action((_options, cmd) => {
55
+ // All flags (--action, --owner, --runId, etc.) are unknown to this command
56
+ // and land in cmd.args as a flat list, preserving order.
57
+ (0, queue_operations_1.runQueueCommand)(cmd.args)
58
+ .then(result => { process.stdout.write(JSON.stringify(result, null, 2) + '\n'); })
59
+ .catch(err => {
60
+ const message = err instanceof Error ? err.message : String(err);
61
+ process.stderr.write(message + '\n');
62
+ process.exit(1);
63
+ });
64
+ });
@@ -343,6 +343,57 @@ function checkNodeScriptDependencies() {
343
343
  }
344
344
  };
345
345
  }
346
+ /**
347
+ * Verify the packaged `fraim learning` CLI route is reachable by exercising a
348
+ * bounded read-only operation through the static-import path introduced in
349
+ * issue #2024. Exercises actual module loading, not just command registration.
350
+ */
351
+ function checkLearningCliRoute() {
352
+ return {
353
+ name: 'fraim learning CLI route reachable',
354
+ category: 'scripts',
355
+ critical: false,
356
+ run: async () => {
357
+ const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'fraim-doctor-'));
358
+ try {
359
+ const result = (0, child_process_1.spawnSync)(process.execPath, [process.argv[1], 'learning', 'queue', '--action', 'list', '--owner', 'doctor-check', '--projectPath', tmpDir], {
360
+ timeout: 10000,
361
+ encoding: 'utf8',
362
+ windowsHide: true,
363
+ // Isolate from the real Manager home so an existing queue does not
364
+ // produce a run-context-mismatch false failure.
365
+ env: { ...process.env, FRAIM_USER_DIR: tmpDir },
366
+ });
367
+ // Verify the operation, not merely a launcher that happens to exit 0.
368
+ // This isolated home has no runs; an error/partial result is not healthy.
369
+ let validEmptyList = false;
370
+ try {
371
+ const payload = JSON.parse(result.stdout);
372
+ validEmptyList = result.status === 0 && Array.isArray(payload.runs) && payload.runs.length === 0;
373
+ }
374
+ catch { /* Missing or malformed output is a failed probe. */ }
375
+ if (validEmptyList) {
376
+ return {
377
+ status: 'passed',
378
+ message: 'fraim learning CLI route reachable (packaged static-import path active)',
379
+ };
380
+ }
381
+ return {
382
+ status: 'warning',
383
+ message: 'fraim learning subcommand unavailable — learning operations may fail on this installation',
384
+ suggestion: 'Run fraim sync to update your local installation, or reinstall fraim from your package manager.',
385
+ details: { exitCode: result.status, stderr: result.stderr },
386
+ };
387
+ }
388
+ finally {
389
+ try {
390
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
391
+ }
392
+ catch { /* best-effort cleanup */ }
393
+ }
394
+ },
395
+ };
396
+ }
346
397
  /**
347
398
  * Get all scripts checks
348
399
  */
@@ -352,6 +403,7 @@ function getScriptsChecks() {
352
403
  checkScriptsSynced(),
353
404
  checkScriptsExecutable(),
354
405
  checkNodeScriptDependencies(),
355
- checkPythonAvailability()
406
+ checkPythonAvailability(),
407
+ checkLearningCliRoute(),
356
408
  ];
357
409
  }
@@ -23,6 +23,7 @@ const workspace_config_1 = require("./commands/workspace-config");
23
23
  const org_1 = require("./commands/org");
24
24
  const manager_1 = require("./commands/manager");
25
25
  const cleanup_artifacts_1 = require("./commands/cleanup-artifacts");
26
+ const learning_1 = require("./commands/learning");
26
27
  const fs_1 = __importDefault(require("fs"));
27
28
  const path_1 = __importDefault(require("path"));
28
29
  const program = new commander_1.Command();
@@ -68,4 +69,5 @@ program.addCommand(workspace_config_1.workspaceConfigCommand);
68
69
  program.addCommand(org_1.orgCommand);
69
70
  program.addCommand(manager_1.managerCommand);
70
71
  program.addCommand(cleanup_artifacts_1.cleanupArtifactsCommand);
72
+ program.addCommand(learning_1.learningCommand);
71
73
  program.parse(process.argv);
@@ -0,0 +1,285 @@
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.discoverLearningSources = discoverLearningSources;
7
+ /** Deterministic, read-only synthesis input inventory for the packaged CLI. */
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_crypto_1 = require("node:crypto");
11
+ const queue_operations_1 = require("./queue-operations");
12
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
13
+ const pack_home_1 = require("../utils/pack-home");
14
+ const user_config_1 = require("../utils/user-config");
15
+ const run_record_1 = require("../../../registry/scripts/learning/run-record");
16
+ const completion_state_1 = require("../../../registry/scripts/learning/completion-state");
17
+ const artifact_paths_1 = require("../../../registry/scripts/artifact-paths");
18
+ const local_runtime_1 = require("../../../registry/scripts/learning-usage/local-runtime");
19
+ const learning_source_record_1 = require("../../../registry/scripts/learning-usage/learning-source-record");
20
+ const corpus_audit_1 = require("../../../registry/scripts/learning/corpus-audit");
21
+ const digest = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
22
+ const canonical = (file) => {
23
+ let resolved = node_path_1.default.resolve(file);
24
+ try {
25
+ resolved = node_fs_1.default.realpathSync.native(resolved);
26
+ }
27
+ catch { /* Missing paths remain explicit. */ }
28
+ return resolved.replace(/\\/g, '/');
29
+ };
30
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
31
+ function discoverLearningSources(projectPath, owner, orgRunId) {
32
+ if (!/^[a-zA-Z0-9@._+-]+$/.test(owner) || owner === '.' || owner === '..')
33
+ throw new Error('invalid-owner');
34
+ projectPath = canonical(projectPath);
35
+ const errors = [];
36
+ const directories = [];
37
+ const files = new Map();
38
+ const homes = [];
39
+ const reservationHomes = [];
40
+ const configFile = node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json');
41
+ let configError;
42
+ let config = {};
43
+ try {
44
+ config = (0, local_runtime_1.readJsonIfObject)(configFile, { strict: true });
45
+ }
46
+ catch (error) {
47
+ if (error.code !== 'ENOENT') {
48
+ configError = 'unreadable-or-malformed-home-config';
49
+ errors.push({ path: canonical(configFile), reason: configError });
50
+ }
51
+ }
52
+ for (const level of ['Manager', 'Project', 'Org']) {
53
+ const layer = level.toLowerCase();
54
+ const home = level === 'Project'
55
+ ? { root: projectPath, contentRoot: (0, project_fraim_paths_1.getWorkspaceFraimPath)(projectPath, 'personalized-employee'), backend: 'project' }
56
+ : (0, pack_home_1.resolvePackHome)(layer, { migrateLegacy: false });
57
+ const record = { level, root: canonical(home.root), contentRoot: canonical(home.contentRoot), backend: home.backend, status: 'available', ...(home.legacyRoot ? { legacyRoot: canonical(home.legacyRoot) } : {}) };
58
+ const configured = config?.[level === 'Manager' ? 'managerStorage' : 'organization'];
59
+ const invalidBackend = configured != null && (!(0, user_config_1.getLayerBackend)(layer)
60
+ || (configured.localPath != null && (typeof configured.localPath !== 'string' || !configured.localPath.trim())));
61
+ if (level !== 'Project' && (configError || invalidBackend || (0, user_config_1.isLayerMisconfigured)(layer))) {
62
+ record.status = 'unavailable';
63
+ record.reason = configError || 'misconfigured-home';
64
+ }
65
+ else {
66
+ try {
67
+ if (!node_fs_1.default.statSync(home.root).isDirectory())
68
+ throw new Error('home-not-directory');
69
+ }
70
+ catch (error) {
71
+ record.status = error.code === 'ENOENT' ? 'missing' : 'unavailable';
72
+ record.reason = error.code || error.message;
73
+ }
74
+ }
75
+ if (record.status === 'available')
76
+ reservationHomes.push(record);
77
+ if (record.status !== 'unavailable') {
78
+ try {
79
+ record.artifactPaths = (0, artifact_paths_1.resolveArtifactPaths)(record.root, record.contentRoot);
80
+ }
81
+ catch (error) {
82
+ record.status = 'unavailable';
83
+ record.reason = error.message;
84
+ errors.push({ path: canonical(node_path_1.default.join(node_path_1.default.dirname(record.contentRoot), 'config.json')), reason: error.message });
85
+ }
86
+ }
87
+ homes.push(record);
88
+ }
89
+ let orgContext;
90
+ const participantOwners = new Map();
91
+ if (orgRunId !== undefined) {
92
+ const orgHome = homes.find(home => home.level === 'Org');
93
+ if (!orgHome?.artifactPaths || orgHome.status !== 'available')
94
+ throw new Error('org-run-home-unavailable');
95
+ for (const home of homes.filter(home => home.status === 'available'))
96
+ participantOwners.set(home.level, (0, queue_operations_1.resolveQueueHome)(projectPath, home.level).owner);
97
+ orgContext = new run_record_1.RunRecord((0, artifact_paths_1.resolveRunJournalDirectory)(orgHome.root), orgRunId, participantOwners.get('Org'), owner, node_path_1.default.join(orgHome.artifactPaths.evidence, 'sleep')).read();
98
+ for (const grant of orgContext.sourceParticipants ?? []) {
99
+ if (participantOwners.get(grant.owner.level)?.path !== grant.owner.path)
100
+ throw new Error('source-participant-home-changed');
101
+ }
102
+ }
103
+ const runDirectories = (home) => [(0, artifact_paths_1.resolveRunJournalDirectory)(home.root),
104
+ ...(home.level === 'Project' ? [(0, artifact_paths_1.resolveRunJournalDirectory)((0, project_fraim_paths_1.getWorkspaceFraimPath)(projectPath))] : [])];
105
+ const reservations = new Set();
106
+ let reservationFailure;
107
+ for (const directory of reservationHomes.flatMap(runDirectories)) {
108
+ try {
109
+ (0, completion_state_1.readSourceReservations)([directory]).forEach(file => reservations.add(file));
110
+ }
111
+ catch (error) {
112
+ reservationFailure = 'source-reservations-unavailable';
113
+ errors.push({ path: canonical(directory), reason: error.message });
114
+ }
115
+ }
116
+ const read = (file, level, role) => {
117
+ const key = canonical(file);
118
+ const previous = files.get(key);
119
+ if (previous) {
120
+ if (!previous.levels.includes(level))
121
+ previous.levels.push(level);
122
+ if (!previous.roles.includes(role))
123
+ previous.roles.push(role);
124
+ return;
125
+ }
126
+ const record = { path: key, levels: [level], roles: [role], owner: null, readStatus: 'error', processing: 'error', sourceRefs: [] };
127
+ files.set(key, record);
128
+ try {
129
+ if (node_fs_1.default.lstatSync(file).isSymbolicLink())
130
+ throw new Error('symlink-source-not-followed');
131
+ const bytes = node_fs_1.default.readFileSync(file);
132
+ record.sha256 = digest(bytes);
133
+ record.readStatus = 'read';
134
+ const content = bytes.toString('utf8');
135
+ record.processing = role === 'raw' || role === 'retrospective' ? 'eligible' : 'context';
136
+ if (role === 'run-history' || role === 'completion-history') {
137
+ try {
138
+ const state = role === 'run-history' ? (0, run_record_1.readSupportedRunRecord)(file) : (0, run_record_1.decodeCompletion)(content, { runId: node_path_1.default.basename(file).replace(/-completion\.md$/, '') });
139
+ if (!state) {
140
+ record.processing = 'excluded';
141
+ record.reason = 'unsupported-run-history';
142
+ return;
143
+ }
144
+ record.owner = 'reviewer' in state ? state.reviewer : null;
145
+ if (state.owner.level === 'Manager' && record.owner !== null && record.owner !== owner) {
146
+ record.processing = 'excluded';
147
+ record.reason = 'other-author';
148
+ }
149
+ const actions = 'actions' in state ? Object.values(state.actions) : state.decisions;
150
+ record.pendingReviewActionIds = 'pendingReviewActionIds' in state ? state.pendingReviewActionIds : [];
151
+ if (actions.some(action => action.decision?.action === 'reject'))
152
+ record.roles.push('rejection-history');
153
+ record.sourceRefs = [...new Set([...actions.flatMap(action => action.sourceRefs), ...state.sourceDispositions.map(source => source.sourceRef)])].sort(compare);
154
+ }
155
+ catch (error) {
156
+ if (role === 'run-history')
157
+ throw error;
158
+ record.processing = 'excluded';
159
+ record.reason = 'unsupported-run-history';
160
+ }
161
+ }
162
+ else if (['raw', 'retrospective', 'archived-raw', 'archived-retrospective'].includes(role)) {
163
+ const home = homes.find(home => home.level === level);
164
+ const parsed = (0, learning_source_record_1.parseLearningSourceRecord)(content, file, home.root, home.contentRoot, home.artifactPaths);
165
+ if (parsed.status !== 'valid') {
166
+ record.processing = parsed.status === 'unsupported' ? 'excluded' : 'error';
167
+ record.reason = parsed.status === 'unsupported' ? 'unsupported-learning-source' : parsed.reason;
168
+ if (parsed.status === 'invalid')
169
+ errors.push({ path: key, reason: parsed.reason });
170
+ }
171
+ else {
172
+ const metadata = parsed.record;
173
+ record.owner = metadata.author;
174
+ record.sourceRefs = [metadata.usageIdentity];
175
+ if (orgContext ? !(0, completion_state_1.sourceAuthorAllowed)(orgContext, metadata.author, participantOwners.get(home.level)) : metadata.author !== owner) {
176
+ record.processing = 'excluded';
177
+ record.reason = 'other-author';
178
+ }
179
+ else if (metadata.state === 'archived')
180
+ record.processing = 'context';
181
+ else if (reservations.has((0, completion_state_1.sourcePathKey)(file))) {
182
+ record.processing = 'excluded';
183
+ record.reason = 'source-reserved-by-pending-run';
184
+ }
185
+ else if (metadata.synthesized) {
186
+ record.processing = 'excluded';
187
+ record.reason = 'already-synthesized';
188
+ }
189
+ else if (reservationFailure)
190
+ record.completionDeferredReason = reservationFailure;
191
+ }
192
+ }
193
+ else if (role === 'manager-feedback') {
194
+ const header = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
195
+ if (!header || !/^artifactKind: work-evidence\s*$/m.test(header) || !/^containsFeedback: true\s*$/m.test(header)) {
196
+ record.processing = 'excluded';
197
+ record.reason = 'not-feedback-evidence';
198
+ }
199
+ }
200
+ else
201
+ record.owner = node_path_1.default.basename(file).startsWith('org-') ? 'org' : owner;
202
+ if (record.processing !== 'excluded') {
203
+ record.content = content;
204
+ record.sourceRefs = [...new Set([...record.sourceRefs, ...content.matchAll(/https:\/\/github\.com\/[^\s)<>"']+\/(?:pull|issues)\/\d+(?:#[^\s)<>"']+)?/g)].map(value => typeof value === 'string' ? value : value[0]))].sort(compare);
205
+ }
206
+ else
207
+ record.sourceRefs = [];
208
+ }
209
+ catch (error) {
210
+ record.processing = 'error';
211
+ record.reason = error.code || error.message;
212
+ errors.push({ path: key, reason: record.reason });
213
+ }
214
+ };
215
+ const walk = (directory, level, select, excludeDirectories = []) => {
216
+ const state = { path: canonical(directory), level, status: 'scanned' };
217
+ directories.push(state);
218
+ try {
219
+ if (node_fs_1.default.lstatSync(directory).isSymbolicLink())
220
+ throw new Error('symlink-directory-not-followed');
221
+ for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => compare(a.name, b.name))) {
222
+ const file = node_path_1.default.join(directory, entry.name);
223
+ if (entry.isDirectory()) {
224
+ if (!excludeDirectories.includes(entry.name) && !excludeDirectories.includes(canonical(file)))
225
+ walk(file, level, select, excludeDirectories);
226
+ }
227
+ else {
228
+ const role = select(entry.name, file);
229
+ if (role)
230
+ read(file, level, role);
231
+ }
232
+ }
233
+ }
234
+ catch (error) {
235
+ state.status = error.code === 'ENOENT' ? 'missing' : 'error';
236
+ if (state.status === 'error')
237
+ errors.push({ path: state.path, reason: error.code || error.message });
238
+ }
239
+ };
240
+ const escapedOwner = owner.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
+ const family = new RegExp(`^(?:${escapedOwner}|org)-(?:[a-z][a-z-]*-)?(?:preferences|mistake-patterns|validated-patterns|manager-coaching)(\\.cold)?\\.md$`);
242
+ for (const home of homes) {
243
+ if (home.status !== 'available')
244
+ continue;
245
+ const learning = node_path_1.default.join(home.contentRoot, 'learnings');
246
+ const outputs = home.artifactPaths;
247
+ walk(learning, home.level, name => name === `${owner}-rejected-proposals.md` ? 'rejection-history'
248
+ : family.test(name) ? (name.endsWith('.cold.md') ? 'cold-family' : 'active-family') : undefined, ['raw', 'retrospectives', 'archive', 'review-queue', 'review-archive', 'review-evidence', ...[outputs.raw, outputs.retrospectives, node_path_1.default.dirname(outputs.archiveRaw)].map(canonical)]);
249
+ walk(outputs.archiveRaw, home.level, name => name.endsWith('.md') ? 'archived-raw' : undefined);
250
+ walk(outputs.archiveRetrospectives, home.level, name => name.endsWith('.md') ? 'archived-retrospective' : undefined);
251
+ const reviewRole = (name) => name.endsWith('.queue.jsonl') ? 'run-history' : name.endsWith('-completion.md') ? 'completion-history' : undefined;
252
+ for (const directory of runDirectories(home))
253
+ walk(directory, home.level, reviewRole);
254
+ walk(node_path_1.default.join(outputs.evidence, 'sleep'), home.level, reviewRole);
255
+ walk(outputs.evidence, home.level, name => name.endsWith('.md') && !name.endsWith('-completion.md') ? 'manager-feedback' : undefined);
256
+ walk(outputs.retrospectives, home.level, name => name.endsWith('.md') ? 'retrospective' : undefined);
257
+ walk(outputs.raw, home.level, name => name.endsWith('.md') ? 'raw' : undefined);
258
+ }
259
+ const corpusRecords = homes.filter(home => home.level !== 'Org' || config.organization || orgRunId !== undefined);
260
+ const corpusHomes = corpusRecords.filter(home => home.status === 'available').flatMap(home => {
261
+ try {
262
+ return [(0, queue_operations_1.resolveQueueHome)(projectPath, home.level)];
263
+ }
264
+ catch (error) {
265
+ errors.push({ path: home.root, reason: error.message });
266
+ return [];
267
+ }
268
+ });
269
+ const corpus = (0, corpus_audit_1.collectLearningCorpus)(corpusHomes, [owner, ...(orgContext?.sourceParticipants?.map(grant => grant.author) || [])], (0, queue_operations_1.resolveReaderCorpusRoots)(projectPath));
270
+ for (const issue of corpus.errors)
271
+ if (issue.reason === 'reader-live-home-unowned') {
272
+ read(issue.path, 'Manager', issue.path.endsWith('.cold.md') ? 'cold-family' : 'active-family');
273
+ }
274
+ if (corpusRecords.some(home => home.status !== 'available') || corpusHomes.length !== corpusRecords.length)
275
+ corpus.complete = false;
276
+ const manifest = {
277
+ schemaVersion: 1, owner, projectPath, homes,
278
+ corpus,
279
+ ...(orgContext ? { orgRunId: orgContext.runId, sourceParticipants: orgContext.sourceParticipants ?? [] } : {}),
280
+ directories: directories.sort((a, b) => compare(a.path, b.path) || compare(a.level, b.level)),
281
+ files: [...files.values()].map(file => ({ ...file, levels: file.levels.sort(compare), roles: file.roles.sort(compare) })).sort((a, b) => compare(a.path, b.path)),
282
+ errors: errors.sort((a, b) => compare(a.path, b.path) || compare(a.reason, b.reason)),
283
+ };
284
+ return { ...manifest, manifestHash: digest(JSON.stringify(manifest)), complete: !errors.length && homes.every(home => home.status === 'available') && corpus.complete };
285
+ }