fraim 2.0.338 → 2.0.340
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 +153 -0
- package/dist/src/cli/commands/cleanup-artifacts.js +2 -2
- package/dist/src/core/ai-mentor.js +1 -1
- package/dist/src/core/handoff-contracts.js +43 -16
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +36 -9
- package/dist/src/local-mcp-server/learning-context-builder.js +145 -27
- package/dist/src/local-mcp-server/learning-usage-analysis.js +79 -2
- package/dist/src/local-mcp-server/learning-usage-attestation.js +9 -2
- package/dist/src/local-mcp-server/learning-usage-command.js +127 -22
- package/dist/src/local-mcp-server/learning-usage-events.js +267 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +25 -21
- package/dist/src/local-mcp-server/learning-usage-store.js +626 -88
- package/dist/src/local-mcp-server/learning-usage-verify.js +207 -0
- package/dist/src/local-mcp-server/stdio-server.js +37 -3
- package/package.json +2 -1
|
@@ -0,0 +1,153 @@
|
|
|
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.COMPLETION_END = exports.COMPLETION_START = void 0;
|
|
7
|
+
exports.validateProcessedSources = validateProcessedSources;
|
|
8
|
+
exports.decodeCompletion = decodeCompletion;
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
function validateProcessedSources(sources) {
|
|
11
|
+
if (!Array.isArray(sources) || new Set(sources.map(item => item.path)).size !== sources.length || sources.some(item => !item || !node_path_1.default.isAbsolute(item.path) || !node_path_1.default.isAbsolute(item.destination) || !['raw', 'retrospective'].includes(item.kind)
|
|
12
|
+
|| !/^[a-f0-9]{64}$/.test(item.sha256) || !/^[a-f0-9]{64}$/.test(item.resultHash) || !/^\d{4}-\d{2}-\d{2}$/.test(item.synthesizedAt)))
|
|
13
|
+
throw new Error('invalid-processed-sources');
|
|
14
|
+
}
|
|
15
|
+
exports.COMPLETION_START = '<!-- fraim-sleep-decisions:v1 -->';
|
|
16
|
+
exports.COMPLETION_END = '<!-- /fraim-sleep-decisions -->';
|
|
17
|
+
const text = (value) => typeof value === 'string' && !!value.trim();
|
|
18
|
+
const hash = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
19
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
const keys = (value, allowed) => object(value) && Object.keys(value).every(key => allowed.includes(key));
|
|
21
|
+
const ownerValid = (value) => object(value) && ['Project', 'Manager', 'Org'].includes(value.level)
|
|
22
|
+
&& keys(value, ['level', 'path']) && text(value.path) && node_path_1.default.isAbsolute(value.path)
|
|
23
|
+
&& !/[\x00-\x1f]/.test(value.path) && value.path === (process.platform === 'win32' ? node_path_1.default.resolve(value.path).replace(/\\/g, '/').toLowerCase() : node_path_1.default.resolve(value.path));
|
|
24
|
+
const sameOwner = (a, b) => a.level === b.level && a.path === b.path;
|
|
25
|
+
function decodeCompletion(markdown, expected = {}) {
|
|
26
|
+
const fail = () => { throw new Error('invalid-completion-decisions'); };
|
|
27
|
+
if ((markdown.match(/<!--\s*fraim-sleep-decisions:/g) || []).length !== 1 || markdown.split(exports.COMPLETION_START).length !== 2 || markdown.split(exports.COMPLETION_END).length !== 2)
|
|
28
|
+
fail();
|
|
29
|
+
const start = markdown.indexOf(exports.COMPLETION_START) + exports.COMPLETION_START.length;
|
|
30
|
+
const end = markdown.indexOf(exports.COMPLETION_END);
|
|
31
|
+
if (end < start)
|
|
32
|
+
fail();
|
|
33
|
+
const block = markdown.slice(start, end).match(/^\s*```json\r?\n([\s\S]*?)\r?\n```\s*$/);
|
|
34
|
+
if (!block)
|
|
35
|
+
fail();
|
|
36
|
+
let state;
|
|
37
|
+
try {
|
|
38
|
+
state = JSON.parse(block[1]);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
fail();
|
|
42
|
+
}
|
|
43
|
+
if (!keys(state, ['schemaVersion', 'runId', 'owner', 'finalizationState', 'decisions', 'sourceDispositions', 'cleanup', 'reconciliations', 'retrospective', 'processedSources']) || state.schemaVersion !== 1 || !text(state.runId) || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(state.runId) || !ownerValid(state.owner)
|
|
44
|
+
|| !['pending', 'complete'].includes(state.finalizationState) || !Array.isArray(state.decisions) || !Array.isArray(state.sourceDispositions) || !Array.isArray(state.cleanup))
|
|
45
|
+
fail();
|
|
46
|
+
if ((expected.runId && expected.runId !== state.runId) || (expected.owner && !sameOwner(expected.owner, state.owner)))
|
|
47
|
+
fail();
|
|
48
|
+
if (state.retrospective !== undefined && (!keys(state.retrospective, ['relativePath', 'sha256'])
|
|
49
|
+
|| state.retrospective.relativePath !== `retrospectives/${state.runId}-sleep-postmortem.md` || !hash(state.retrospective.sha256)))
|
|
50
|
+
fail();
|
|
51
|
+
if (state.processedSources !== undefined)
|
|
52
|
+
validateProcessedSources(state.processedSources);
|
|
53
|
+
const ids = new Set();
|
|
54
|
+
for (const item of state.decisions) {
|
|
55
|
+
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)
|
|
56
|
+
|| !text(item.claimKey) || !text(item.evidenceFingerprint) || !Array.isArray(item.sourceRefs) || !item.sourceRefs.every(text) || (item.publicationRef !== undefined && !text(item.publicationRef)))
|
|
57
|
+
fail();
|
|
58
|
+
ids.add(item.actionId);
|
|
59
|
+
const decision = item.decision;
|
|
60
|
+
if (!keys(decision, ['action', 'revision', 'digest', 'reviewer', 'reason', 'revisitCondition', 'at']) || !['approve', 'reject'].includes(decision.action) || decision.revision !== item.revision || decision.digest !== item.digest
|
|
61
|
+
|| !text(decision.reviewer) || !text(decision.reason) || !text(decision.at) || !Number.isFinite(Date.parse(decision.at))
|
|
62
|
+
|| (decision.revisitCondition !== undefined && (decision.action !== 'reject' || !text(decision.revisitCondition))))
|
|
63
|
+
fail();
|
|
64
|
+
if ('content' in item || 'specification' in item || 'application' in item || 'preimage' in item)
|
|
65
|
+
fail();
|
|
66
|
+
if (item.target && (!keys(item.target, ['owner', 'relativePath', 'entryId', 'learningSubject']) || !ownerValid(item.target.owner) || !text(item.target.relativePath) || /^[\\/]|^[a-zA-Z]:/.test(item.target.relativePath) || item.target.relativePath.split('/').includes('..')))
|
|
67
|
+
fail();
|
|
68
|
+
if (item.dependencies && (!Array.isArray(item.dependencies) || item.dependencies.some((dependency) => !keys(dependency, ['actionId', 'outcome']) || !text(dependency.actionId) || !['applied', 'rejected'].includes(dependency.outcome))))
|
|
69
|
+
fail();
|
|
70
|
+
}
|
|
71
|
+
for (const source of state.sourceDispositions) {
|
|
72
|
+
if (!keys(source, ['sourceRef', 'sourceHash', 'outcome', 'actionId']) || !text(source.sourceRef) || !hash(source.sourceHash) || !['applied', 'rejected'].includes(source.outcome) || !ids.has(source.actionId))
|
|
73
|
+
fail();
|
|
74
|
+
}
|
|
75
|
+
const decisions = new Map(state.decisions.map((item) => [item.actionId, item]));
|
|
76
|
+
const outcome = (item) => item.decision.action === 'approve' ? 'applied' : 'rejected';
|
|
77
|
+
const dispositions = new Set();
|
|
78
|
+
for (const source of state.sourceDispositions) {
|
|
79
|
+
const action = decisions.get(source.actionId);
|
|
80
|
+
const key = JSON.stringify([source.actionId, source.sourceRef]);
|
|
81
|
+
if (dispositions.has(key) || !action.sourceRefs.includes(source.sourceRef) || source.outcome !== outcome(action))
|
|
82
|
+
fail();
|
|
83
|
+
dispositions.add(key);
|
|
84
|
+
}
|
|
85
|
+
for (const action of decisions.values()) {
|
|
86
|
+
if (action.target && !sameOwner(action.owner, action.target.owner))
|
|
87
|
+
fail();
|
|
88
|
+
if (new Set(action.sourceRefs).size !== action.sourceRefs.length || action.sourceRefs.some(ref => !dispositions.has(JSON.stringify([action.actionId, ref]))))
|
|
89
|
+
fail();
|
|
90
|
+
const dependencies = action.dependencies || [];
|
|
91
|
+
if (new Set(dependencies.map(dependency => dependency.actionId)).size !== dependencies.length)
|
|
92
|
+
fail();
|
|
93
|
+
for (const dependency of dependencies) {
|
|
94
|
+
const target = decisions.get(dependency.actionId);
|
|
95
|
+
if (!target || target.actionId === action.actionId || dependency.outcome !== outcome(target)
|
|
96
|
+
|| (action.decision.action === 'approve' && dependency.outcome !== 'applied'))
|
|
97
|
+
fail();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const checked = new Set();
|
|
101
|
+
const visiting = new Set();
|
|
102
|
+
const visit = (id) => {
|
|
103
|
+
if (visiting.has(id))
|
|
104
|
+
fail();
|
|
105
|
+
if (checked.has(id))
|
|
106
|
+
return;
|
|
107
|
+
visiting.add(id);
|
|
108
|
+
for (const dependency of decisions.get(id).dependencies || [])
|
|
109
|
+
visit(dependency.actionId);
|
|
110
|
+
visiting.delete(id);
|
|
111
|
+
checked.add(id);
|
|
112
|
+
};
|
|
113
|
+
for (const id of decisions.keys())
|
|
114
|
+
visit(id);
|
|
115
|
+
for (const item of state.cleanup) {
|
|
116
|
+
if (!keys(item, ['path', 'outcome', 'reason', 'sha256', 'location']) || !text(item.path) || !['pending', 'deleted', 'absent', 'retained'].includes(item.outcome) || (item.reason !== undefined && typeof item.reason !== 'string') || (item.sha256 !== undefined && !hash(item.sha256)))
|
|
117
|
+
fail();
|
|
118
|
+
if (item.location !== undefined) {
|
|
119
|
+
const location = item.location;
|
|
120
|
+
if (!keys(location, ['owner', 'relativePath']) || !ownerValid(location.owner)
|
|
121
|
+
|| !text(location.relativePath) || !/^learnings\/(?:review-queue|review-archive|review-evidence|archive)\//.test(location.relativePath)
|
|
122
|
+
|| /[\\:]/.test(location.relativePath) || location.relativePath.split('/').some((part) => !part || part === '.' || part === '..'))
|
|
123
|
+
fail();
|
|
124
|
+
}
|
|
125
|
+
if (state.finalizationState === 'complete' && item.outcome === 'pending')
|
|
126
|
+
fail();
|
|
127
|
+
}
|
|
128
|
+
if (state.reconciliations !== undefined) {
|
|
129
|
+
if (!Array.isArray(state.reconciliations))
|
|
130
|
+
fail();
|
|
131
|
+
const sources = new Set();
|
|
132
|
+
for (const item of state.reconciliations) {
|
|
133
|
+
if (!keys(item, ['sourceRef', 'sourceHash', 'owner', 'relativePath', 'actionIds', 'actionBindings', 'disposition', 'rationale', 'preservedDecisions', 'reviewer'])
|
|
134
|
+
|| !text(item.sourceRef) || sources.has(item.sourceRef) || !hash(item.sourceHash) || !ownerValid(item.owner)
|
|
135
|
+
|| !text(item.relativePath) || !/^learnings\/(?:review-queue|review-archive|review-evidence|archive)\//.test(item.relativePath)
|
|
136
|
+
|| /[\\:]/.test(item.relativePath) || item.relativePath.split('/').some((part) => !part || part === '.' || part === '..')
|
|
137
|
+
|| !['accounted', 'unrelated', 'raw-source'].includes(item.disposition) || !text(item.rationale) || !text(item.reviewer)
|
|
138
|
+
|| !Array.isArray(item.preservedDecisions) || !item.preservedDecisions.every(text)
|
|
139
|
+
|| !Array.isArray(item.actionIds) || new Set(item.actionIds).size !== item.actionIds.length
|
|
140
|
+
|| !Array.isArray(item.actionBindings) || item.actionBindings.length !== item.actionIds.length)
|
|
141
|
+
fail();
|
|
142
|
+
sources.add(item.sourceRef);
|
|
143
|
+
for (const [index, id] of item.actionIds.entries()) {
|
|
144
|
+
const decision = decisions.get(id);
|
|
145
|
+
const binding = item.actionBindings[index];
|
|
146
|
+
if (!decision || !keys(binding, ['actionId', 'revision', 'digest']) || binding.actionId !== id || binding.revision !== decision.revision || binding.digest !== decision.digest
|
|
147
|
+
|| !state.sourceDispositions.some((source) => source.actionId === id && source.sourceRef.replace(/\\/g, '/') === item.sourceRef.replace(/\\/g, '/') && source.sourceHash === item.sourceHash))
|
|
148
|
+
fail();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
@@ -22,10 +22,10 @@ function runCleanupArtifactsCommand(options) {
|
|
|
22
22
|
}
|
|
23
23
|
exports.cleanupArtifactsCommand = new commander_1.Command('cleanup-artifacts')
|
|
24
24
|
.alias('cleanup-artifact')
|
|
25
|
-
.description('Evaluate and optionally apply artifact retention cleanup for
|
|
25
|
+
.description('Evaluate and optionally apply artifact retention cleanup for a FRAIM workspace or pack root')
|
|
26
26
|
.option('--user <email>', 'Active user email used only for user-scoped cleanup categories')
|
|
27
27
|
.option('--workspace-root <path>', 'Workspace root containing fraim/config.json')
|
|
28
|
-
.option('--root <path>', '
|
|
28
|
+
.option('--root <path>', 'Workspace root or explicit Manager/Org pack root; uses that root\'s retention policy')
|
|
29
29
|
.option('--apply', 'Delete eligible files. Without this flag the command performs a dry run.')
|
|
30
30
|
.action((options) => {
|
|
31
31
|
try {
|
|
@@ -348,4 +348,4 @@ class AIMentor {
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
exports.AIMentor = AIMentor;
|
|
351
|
-
AIMentor.COMPACT_PHASE_AUTHORITY = '**Use only the phases defined in this workflow.**';
|
|
351
|
+
AIMentor.COMPACT_PHASE_AUTHORITY = '**Use only the phases defined in this workflow.** Before phase work, read `get_fraim_file({ path: "orchestration/phase-authority.md" })` for phase closure, approval gates, and resumption.';
|
|
@@ -27,6 +27,7 @@ exports.validateReviewActionCompleteness = validateReviewActionCompleteness;
|
|
|
27
27
|
exports.hasPhaseStarted = hasPhaseStarted;
|
|
28
28
|
exports.upsertPhaseStarted = upsertPhaseStarted;
|
|
29
29
|
exports.buildPhaseStartWarning = buildPhaseStartWarning;
|
|
30
|
+
exports.reviewHandoffNormalizationError = reviewHandoffNormalizationError;
|
|
30
31
|
exports.validateReviewHandoff = validateReviewHandoff;
|
|
31
32
|
exports.validateNextJobRecommendations = validateNextJobRecommendations;
|
|
32
33
|
exports.validateDelegationLedger = validateDelegationLedger;
|
|
@@ -255,23 +256,49 @@ function validateReviewArtifactEntry(artifact, index) {
|
|
|
255
256
|
return errors;
|
|
256
257
|
}
|
|
257
258
|
/**
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* Required minimum shape:
|
|
262
|
-
* {
|
|
263
|
-
* reviewRequired: boolean,
|
|
264
|
-
* artifacts: array,
|
|
265
|
-
* // when reviewRequired is true:
|
|
266
|
-
* reviewTarget: { type: "pull_request", label: string, url: string } | { type: "artifact_set", label: string },
|
|
267
|
-
* reviewActions: [{ kind: "approve", ... }, { kind: "request_changes", ... }],
|
|
268
|
-
* }
|
|
269
|
-
*
|
|
270
|
-
* Hub normalizers (server.ts + script.js) both read reviewTarget.type, not .kind.
|
|
271
|
-
* Using reviewTarget.kind produces a payload that passes validation but is silently
|
|
272
|
-
* un-renderable. This validator enforces the renderable contract so malformed payloads
|
|
273
|
-
* are rejected before phase advancement.
|
|
259
|
+
* Reports payloads the Hub cannot normalize, including artifact_set entries it
|
|
260
|
+
* would silently drop. This deliberately preserves the Hub's legacy defaults;
|
|
261
|
+
* it does not impose the stronger, phase-dependent submission contract.
|
|
274
262
|
*/
|
|
263
|
+
function reviewHandoffNormalizationError(value) {
|
|
264
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
265
|
+
return 'reviewHandoff must be an object';
|
|
266
|
+
}
|
|
267
|
+
const handoff = value;
|
|
268
|
+
if (typeof handoff.reviewRequired !== 'boolean') {
|
|
269
|
+
return 'reviewHandoff.reviewRequired must be a boolean';
|
|
270
|
+
}
|
|
271
|
+
// This is renderability, not the stronger submission-phase contract below.
|
|
272
|
+
// In particular legacy handoffs need not supply actions or an artifacts array.
|
|
273
|
+
if (!handoff.reviewRequired)
|
|
274
|
+
return null;
|
|
275
|
+
const target = handoff.reviewTarget;
|
|
276
|
+
const targetType = typeof target?.type === 'string' ? target.type.trim() : '';
|
|
277
|
+
if (targetType !== 'pull_request' && targetType !== 'artifact_set') {
|
|
278
|
+
return 'reviewHandoff.reviewTarget.type must be pull_request or artifact_set';
|
|
279
|
+
}
|
|
280
|
+
if (targetType === 'pull_request' && !isAbsoluteHttpUrl(target?.url)) {
|
|
281
|
+
return 'reviewHandoff.reviewTarget.url must be an absolute http or https URL';
|
|
282
|
+
}
|
|
283
|
+
// Match the host parser's object-artifact selection and Hub's path/URL
|
|
284
|
+
// normalization. A missing label/type is filled in by the Hub, not rejected.
|
|
285
|
+
const artifacts = Array.isArray(handoff.artifacts)
|
|
286
|
+
? handoff.artifacts.filter((artifact) => artifact && typeof artifact === 'object')
|
|
287
|
+
: [];
|
|
288
|
+
for (const [index, artifact] of artifacts.entries()) {
|
|
289
|
+
const hasPath = typeof artifact.path === 'string' && artifact.path.trim().length > 0;
|
|
290
|
+
const hasUrl = isAbsoluteHttpUrl(artifact.url);
|
|
291
|
+
if (targetType === 'pull_request') {
|
|
292
|
+
if (hasPath || hasUrl)
|
|
293
|
+
return 'reviewHandoff.artifacts must be empty for a pull_request target';
|
|
294
|
+
}
|
|
295
|
+
else if (!hasPath || hasUrl) {
|
|
296
|
+
return `reviewHandoff.artifacts[${index}] cannot be applied: an artifact_set requires a file path without an http or https URL`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
/** Validate the complete submission contract, including required actions. */
|
|
275
302
|
function validateReviewHandoff(value) {
|
|
276
303
|
if (value === undefined || value === null) {
|
|
277
304
|
return ['evidence.reviewHandoff is missing'];
|
|
@@ -28,9 +28,14 @@ exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
|
28
28
|
'learning_usage',
|
|
29
29
|
'hub_event_logs',
|
|
30
30
|
];
|
|
31
|
+
function contentRoot(workspaceRoot) {
|
|
32
|
+
const packContent = node_path_1.default.join(workspaceRoot, 'personalized-employee');
|
|
33
|
+
return node_fs_1.default.existsSync(packContent) ? packContent : node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee');
|
|
34
|
+
}
|
|
31
35
|
function readWorkspaceConfig(workspaceRoot) {
|
|
32
36
|
try {
|
|
33
|
-
const configPath = (
|
|
37
|
+
const configPath = contentRoot(workspaceRoot) === node_path_1.default.join(workspaceRoot, 'personalized-employee')
|
|
38
|
+
? node_path_1.default.join(workspaceRoot, 'config.json') : (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
|
|
34
39
|
if (!node_fs_1.default.existsSync(configPath))
|
|
35
40
|
return {};
|
|
36
41
|
return JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
|
|
@@ -143,7 +148,7 @@ function loadGitLastChangeDates(workspaceRoot, relativeDir) {
|
|
|
143
148
|
return dates;
|
|
144
149
|
}
|
|
145
150
|
function readFrontmatter(content) {
|
|
146
|
-
const match = content.match(
|
|
151
|
+
const match = content.match(/^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/);
|
|
147
152
|
if (!match)
|
|
148
153
|
return {};
|
|
149
154
|
const values = {};
|
|
@@ -167,8 +172,20 @@ function synthesizedDate(absolutePath) {
|
|
|
167
172
|
return undefined;
|
|
168
173
|
}
|
|
169
174
|
}
|
|
170
|
-
function userScoped(
|
|
171
|
-
|
|
175
|
+
function userScoped(filePath, activeUserEmail) {
|
|
176
|
+
try {
|
|
177
|
+
const body = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
178
|
+
const author = readFrontmatter(body).author;
|
|
179
|
+
if (author !== undefined) {
|
|
180
|
+
const value = author.replace(/^(?:"([^"]*)"|'([^']*)')$/, (_match, double, single) => double ?? single);
|
|
181
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) && value === activeUserEmail;
|
|
182
|
+
}
|
|
183
|
+
// Older artifacts used the author in their filename.
|
|
184
|
+
return node_path_1.default.basename(filePath).startsWith(`${activeUserEmail}-`);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
172
189
|
}
|
|
173
190
|
function evidenceEligibilityDate(workspaceRoot, absolutePath, gitDates) {
|
|
174
191
|
const relativePath = normalizeRelative(workspaceRoot, absolutePath);
|
|
@@ -187,17 +204,22 @@ function collectCandidates(workspaceRoot, options) {
|
|
|
187
204
|
eligibilityDate,
|
|
188
205
|
});
|
|
189
206
|
};
|
|
190
|
-
const
|
|
207
|
+
const content = contentRoot(workspaceRoot);
|
|
208
|
+
const archiveDir = node_path_1.default.join(content, 'learnings', 'archive');
|
|
191
209
|
for (const filePath of listFiles(archiveDir)) {
|
|
192
210
|
const fileName = node_path_1.default.basename(filePath);
|
|
193
|
-
if (!fileName.endsWith('.md') || !userScoped(
|
|
211
|
+
if (!fileName.endsWith('.md') || !userScoped(filePath, activeUserEmail))
|
|
194
212
|
continue;
|
|
195
213
|
add('learning_archive', filePath, true, 'Archived learning source for active user.', fileMtime(filePath));
|
|
196
214
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
215
|
+
const retrospectives = new Set([
|
|
216
|
+
node_path_1.default.join(workspaceRoot, 'docs', 'retrospectives'),
|
|
217
|
+
node_path_1.default.join(workspaceRoot, 'retrospectives'),
|
|
218
|
+
node_path_1.default.join(content, 'retrospectives'),
|
|
219
|
+
].flatMap(listFiles));
|
|
220
|
+
for (const filePath of retrospectives) {
|
|
199
221
|
const fileName = node_path_1.default.basename(filePath);
|
|
200
|
-
if (!fileName.endsWith('.md') || !userScoped(
|
|
222
|
+
if (!fileName.endsWith('.md') || !userScoped(filePath, activeUserEmail))
|
|
201
223
|
continue;
|
|
202
224
|
const date = synthesizedDate(filePath);
|
|
203
225
|
if (date)
|
|
@@ -209,6 +231,11 @@ function collectCandidates(workspaceRoot, options) {
|
|
|
209
231
|
const evidenceGitDates = loadGitLastChangeDates(workspaceRoot, 'docs/evidence');
|
|
210
232
|
for (const filePath of listFiles(evidenceDir)) {
|
|
211
233
|
const fileName = node_path_1.default.basename(filePath);
|
|
234
|
+
// A surviving run journal is unfinished recovery; completion removes it.
|
|
235
|
+
if (fileName.endsWith('.queue.jsonl')) {
|
|
236
|
+
add('evidence', filePath, false, 'Unfinished Sleep run journal; removed by run completion.');
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
212
239
|
const isMarkdown = fileName.endsWith('.md');
|
|
213
240
|
const isJson = fileName.endsWith('.json');
|
|
214
241
|
const eligibilityDate = evidenceEligibilityDate(workspaceRoot, filePath, evidenceGitDates);
|