fraim-framework 2.0.171 → 2.0.174
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/src/ai-hub/hosts.js +227 -6
- package/dist/src/ai-hub/server.js +1014 -35
- package/dist/src/cli/commands/add-ide.js +2 -0
- package/dist/src/cli/commands/cleanup-artifacts.js +39 -0
- package/dist/src/cli/commands/init-project.js +12 -5
- package/dist/src/cli/commands/sync.js +74 -7
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-detector.js +6 -0
- package/dist/src/cli/utils/agent-adapters.js +40 -18
- package/dist/src/cli/utils/fraim-gitignore.js +13 -0
- package/dist/src/cli/utils/remote-sync.js +129 -53
- package/dist/src/cli/utils/user-config.js +12 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/fraim-config-schema.generated.js +8 -2
- package/dist/src/core/utils/local-registry-resolver.js +26 -0
- package/dist/src/core/utils/project-fraim-paths.js +89 -2
- package/dist/src/first-run/session-service.js +9 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +298 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
- package/dist/src/local-mcp-server/stdio-server.js +42 -7
- package/package.json +5 -1
- package/public/ai-hub/index.html +205 -89
- package/public/ai-hub/review.css +12 -0
- package/public/ai-hub/script.js +1720 -240
- package/public/ai-hub/styles.css +473 -6
|
@@ -269,7 +269,8 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
269
269
|
"kind": "enum",
|
|
270
270
|
"values": [
|
|
271
271
|
"salesforce",
|
|
272
|
-
"customereq"
|
|
272
|
+
"customereq",
|
|
273
|
+
"attio"
|
|
273
274
|
]
|
|
274
275
|
}
|
|
275
276
|
}
|
|
@@ -457,7 +458,8 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
457
458
|
"kind": "enum",
|
|
458
459
|
"values": [
|
|
459
460
|
"servicenow",
|
|
460
|
-
"fixture"
|
|
461
|
+
"fixture",
|
|
462
|
+
"facebook_messenger"
|
|
461
463
|
]
|
|
462
464
|
},
|
|
463
465
|
"table": {
|
|
@@ -489,6 +491,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
489
491
|
},
|
|
490
492
|
"closeState": {
|
|
491
493
|
"kind": "string"
|
|
494
|
+
},
|
|
495
|
+
"accessScript": {
|
|
496
|
+
"kind": "string"
|
|
492
497
|
}
|
|
493
498
|
}
|
|
494
499
|
},
|
|
@@ -678,6 +683,7 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
678
683
|
"automation.support.queue.successState",
|
|
679
684
|
"automation.support.queue.failureState",
|
|
680
685
|
"automation.support.queue.closeState",
|
|
686
|
+
"automation.support.queue.accessScript",
|
|
681
687
|
"automation.support.requestTypes",
|
|
682
688
|
"automation.support.requestTypes.password_reset",
|
|
683
689
|
"automation.support.requestTypes.password_reset.mode",
|
|
@@ -225,6 +225,23 @@ class LocalRegistryResolver {
|
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
+
readWorkspaceRegistryFile(path) {
|
|
229
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
230
|
+
const registryPath = (0, path_1.join)(this.workspaceRoot, 'registry', ...normalizedPath.split('/'));
|
|
231
|
+
if (!(0, fs_1.existsSync)(registryPath)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const content = (0, fs_1.readFileSync)(registryPath, 'utf-8');
|
|
236
|
+
if (this.shouldFilter && this.shouldFilter(content)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return content;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
228
245
|
normalizeRegistryPath(path) {
|
|
229
246
|
const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
|
230
247
|
return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
|
|
@@ -379,6 +396,15 @@ class LocalRegistryResolver {
|
|
|
379
396
|
inherited: false
|
|
380
397
|
};
|
|
381
398
|
}
|
|
399
|
+
const workspaceRegistryContent = this.readWorkspaceRegistryFile(path);
|
|
400
|
+
if (workspaceRegistryContent !== null) {
|
|
401
|
+
return {
|
|
402
|
+
content: workspaceRegistryContent,
|
|
403
|
+
source: 'local',
|
|
404
|
+
personalized: false,
|
|
405
|
+
inherited: false
|
|
406
|
+
};
|
|
407
|
+
}
|
|
382
408
|
// No useful override or synced content, fetch from remote
|
|
383
409
|
try {
|
|
384
410
|
const rawContent = await this.fetchRemoteWithFallback(path);
|
|
@@ -8,6 +8,10 @@ exports.getWorkspaceFraimDir = getWorkspaceFraimDir;
|
|
|
8
8
|
exports.workspaceFraimExists = workspaceFraimExists;
|
|
9
9
|
exports.getWorkspaceConfigPath = getWorkspaceConfigPath;
|
|
10
10
|
exports.getWorkspaceFraimPath = getWorkspaceFraimPath;
|
|
11
|
+
exports.assertWorkspaceFraimRoot = assertWorkspaceFraimRoot;
|
|
12
|
+
exports.assertWorkspaceFraimPath = assertWorkspaceFraimPath;
|
|
13
|
+
exports.createWorkspaceFraimPathAsserter = createWorkspaceFraimPathAsserter;
|
|
14
|
+
exports.assertExactWorkspaceFraimPath = assertExactWorkspaceFraimPath;
|
|
11
15
|
exports.getWorkspaceFraimDisplayPath = getWorkspaceFraimDisplayPath;
|
|
12
16
|
exports.getUserFraimDisplayPath = getUserFraimDisplayPath;
|
|
13
17
|
exports.getUserFraimDirPath = getUserFraimDirPath;
|
|
@@ -35,8 +39,38 @@ function normalizeDisplayPath(fullPath) {
|
|
|
35
39
|
function getWorkspaceFraimDir(projectRoot = process.cwd()) {
|
|
36
40
|
return (0, path_1.join)(projectRoot, exports.WORKSPACE_FRAIM_DIRNAME);
|
|
37
41
|
}
|
|
42
|
+
function isPathInsideOrEqual(parentPath, childPath) {
|
|
43
|
+
const relativePath = (0, path_1.relative)(parentPath, childPath);
|
|
44
|
+
return relativePath === '' || (relativePath.length > 0 &&
|
|
45
|
+
!relativePath.startsWith('..') &&
|
|
46
|
+
!(0, path_1.isAbsolute)(relativePath));
|
|
47
|
+
}
|
|
48
|
+
function realpathIfExists(fullPath) {
|
|
49
|
+
try {
|
|
50
|
+
return fs_1.realpathSync.native(fullPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function deepestExistingAncestor(fullPath) {
|
|
57
|
+
let current = (0, path_1.resolve)(fullPath);
|
|
58
|
+
while (!(0, fs_1.existsSync)(current)) {
|
|
59
|
+
const parent = (0, path_1.dirname)(current);
|
|
60
|
+
if (parent === current) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
current = parent;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
38
67
|
function workspaceFraimExists(projectRoot = process.cwd()) {
|
|
39
|
-
|
|
68
|
+
try {
|
|
69
|
+
return (0, fs_1.readdirSync)(projectRoot, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name === exports.WORKSPACE_FRAIM_DIRNAME);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
40
74
|
}
|
|
41
75
|
function getWorkspaceConfigPath(projectRoot = process.cwd()) {
|
|
42
76
|
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), 'config.json');
|
|
@@ -44,6 +78,59 @@ function getWorkspaceConfigPath(projectRoot = process.cwd()) {
|
|
|
44
78
|
function getWorkspaceFraimPath(projectRoot = process.cwd(), ...parts) {
|
|
45
79
|
return (0, path_1.join)(getWorkspaceFraimDir(projectRoot), ...parts);
|
|
46
80
|
}
|
|
81
|
+
function assertWorkspaceFraimRoot(projectRoot = process.cwd()) {
|
|
82
|
+
const resolvedProjectRoot = (0, path_1.resolve)(projectRoot);
|
|
83
|
+
const fraimDir = getWorkspaceFraimDir(resolvedProjectRoot);
|
|
84
|
+
if (!workspaceFraimExists(resolvedProjectRoot)) {
|
|
85
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": expected exact lowercase fraim directory at "${fraimDir}".`);
|
|
86
|
+
}
|
|
87
|
+
const projectRootRealPath = realpathIfExists(resolvedProjectRoot);
|
|
88
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
89
|
+
if (projectRootRealPath &&
|
|
90
|
+
fraimRealPath &&
|
|
91
|
+
!isPathInsideOrEqual(projectRootRealPath, fraimRealPath)) {
|
|
92
|
+
throw new Error(`Refusing to use FRAIM workspace root "${resolvedProjectRoot}": "${fraimDir}" resolves outside the project root.`);
|
|
93
|
+
}
|
|
94
|
+
return fraimDir;
|
|
95
|
+
}
|
|
96
|
+
function assertWorkspaceFraimPath(projectRoot = process.cwd(), targetPath) {
|
|
97
|
+
createWorkspaceFraimPathAsserter(projectRoot)(targetPath);
|
|
98
|
+
}
|
|
99
|
+
function createWorkspaceFraimPathAsserter(projectRoot = process.cwd()) {
|
|
100
|
+
const fraimDir = assertWorkspaceFraimRoot(projectRoot);
|
|
101
|
+
const resolvedFraimDir = (0, path_1.resolve)(fraimDir);
|
|
102
|
+
const fraimRealPath = realpathIfExists(fraimDir);
|
|
103
|
+
const realpathCache = new Map();
|
|
104
|
+
const cachedRealpath = (fullPath) => {
|
|
105
|
+
const resolvedPath = (0, path_1.resolve)(fullPath);
|
|
106
|
+
if (!realpathCache.has(resolvedPath)) {
|
|
107
|
+
realpathCache.set(resolvedPath, realpathIfExists(resolvedPath));
|
|
108
|
+
}
|
|
109
|
+
return realpathCache.get(resolvedPath) ?? null;
|
|
110
|
+
};
|
|
111
|
+
return (targetPath) => {
|
|
112
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
113
|
+
if (resolvedTargetPath === resolvedFraimDir ||
|
|
114
|
+
!isPathInsideOrEqual(resolvedFraimDir, resolvedTargetPath)) {
|
|
115
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected a path under "${resolvedFraimDir}".`);
|
|
116
|
+
}
|
|
117
|
+
const existingAncestor = deepestExistingAncestor(resolvedTargetPath);
|
|
118
|
+
const ancestorRealPath = existingAncestor ? cachedRealpath(existingAncestor) : null;
|
|
119
|
+
if (fraimRealPath &&
|
|
120
|
+
ancestorRealPath &&
|
|
121
|
+
!isPathInsideOrEqual(fraimRealPath, ancestorRealPath)) {
|
|
122
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": existing path "${existingAncestor}" resolves outside "${fraimDir}".`);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function assertExactWorkspaceFraimPath(projectRoot = process.cwd(), targetPath, ...parts) {
|
|
127
|
+
const expectedPath = (0, path_1.resolve)(getWorkspaceFraimDir((0, path_1.resolve)(projectRoot)), ...parts);
|
|
128
|
+
const resolvedTargetPath = (0, path_1.resolve)(targetPath);
|
|
129
|
+
if (resolvedTargetPath !== expectedPath) {
|
|
130
|
+
throw new Error(`Refusing to use FRAIM workspace path "${resolvedTargetPath}": expected exact managed path "${expectedPath}".`);
|
|
131
|
+
}
|
|
132
|
+
assertWorkspaceFraimPath(projectRoot, targetPath);
|
|
133
|
+
}
|
|
47
134
|
function getWorkspaceFraimDisplayPath(relativePath = '') {
|
|
48
135
|
const normalized = normalizeRelativePath(relativePath);
|
|
49
136
|
return normalized.length > 0
|
|
@@ -120,7 +207,7 @@ function getConfiguredPortableLearningsDisplayPath(projectRoot = process.cwd())
|
|
|
120
207
|
function getEffectiveFraimDir(projectRoot = process.cwd(), userFraimDir) {
|
|
121
208
|
// 1. Check for project-level fraim/ directory
|
|
122
209
|
const projectFraimDir = getWorkspaceFraimDir(projectRoot);
|
|
123
|
-
if ((
|
|
210
|
+
if (workspaceFraimExists(projectRoot)) {
|
|
124
211
|
return projectFraimDir;
|
|
125
212
|
}
|
|
126
213
|
// 2. Fall back to user-level ~/.fraim/
|
|
@@ -547,6 +547,15 @@ class FirstRunSessionService {
|
|
|
547
547
|
this.persist();
|
|
548
548
|
return this.respond('Fake-mode fraim ok.', true);
|
|
549
549
|
}
|
|
550
|
+
// FRAIM_SKIP_SYNC signals an isolated/test environment. Skip all external
|
|
551
|
+
// writes (MCP config files, global config.json, slash commands) so tests
|
|
552
|
+
// that auto-trigger this path cannot corrupt the real user config.
|
|
553
|
+
if (process.env.FRAIM_SKIP_SYNC) {
|
|
554
|
+
row.status = 'ok';
|
|
555
|
+
row.verb = 'Ready.';
|
|
556
|
+
this.persist();
|
|
557
|
+
return this.respond('FRAIM configured.', true);
|
|
558
|
+
}
|
|
550
559
|
try {
|
|
551
560
|
if (!forceConfigure && !commandVersion('fraim')) {
|
|
552
561
|
const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
|
|
@@ -0,0 +1,298 @@
|
|
|
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.ARTIFACT_RETENTION_CATEGORIES = void 0;
|
|
7
|
+
exports.resolveArtifactRetentionConfig = resolveArtifactRetentionConfig;
|
|
8
|
+
exports.runArtifactCleanup = runArtifactCleanup;
|
|
9
|
+
exports.planArtifactCleanup = planArtifactCleanup;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
14
|
+
exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
15
|
+
'learning_archive',
|
|
16
|
+
'retrospectives',
|
|
17
|
+
'evidence',
|
|
18
|
+
'feedback',
|
|
19
|
+
'cleanup_manifests',
|
|
20
|
+
];
|
|
21
|
+
function readWorkspaceConfig(workspaceRoot) {
|
|
22
|
+
try {
|
|
23
|
+
const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
|
|
24
|
+
if (!node_fs_1.default.existsSync(configPath))
|
|
25
|
+
return {};
|
|
26
|
+
return JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function validRetentionValue(value) {
|
|
33
|
+
return Number.isInteger(value) && value >= -1;
|
|
34
|
+
}
|
|
35
|
+
function normalizeRelative(workspaceRoot, absolutePath) {
|
|
36
|
+
return node_path_1.default.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
|
|
37
|
+
}
|
|
38
|
+
function parseRetentionValue(category, value, fallback, invalidValues) {
|
|
39
|
+
if (validRetentionValue(value))
|
|
40
|
+
return value;
|
|
41
|
+
invalidValues.push({
|
|
42
|
+
category,
|
|
43
|
+
value,
|
|
44
|
+
fallback,
|
|
45
|
+
reason: 'Retention values must be integers greater than or equal to -1.',
|
|
46
|
+
});
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
function resolveArtifactRetentionConfig(workspaceRoot) {
|
|
50
|
+
const config = readWorkspaceConfig(workspaceRoot);
|
|
51
|
+
const configuredRetention = config?.artifact_retention_period;
|
|
52
|
+
const rawRetention = configuredRetention && typeof configuredRetention === 'object' && !Array.isArray(configuredRetention)
|
|
53
|
+
? configuredRetention
|
|
54
|
+
: {};
|
|
55
|
+
const invalidValues = [];
|
|
56
|
+
const rawDefault = rawRetention?.default;
|
|
57
|
+
const defaultValue = validRetentionValue(rawDefault)
|
|
58
|
+
? rawDefault
|
|
59
|
+
: parseRetentionValue('default', rawDefault, -1, invalidValues);
|
|
60
|
+
const values = {};
|
|
61
|
+
for (const category of exports.ARTIFACT_RETENTION_CATEGORIES) {
|
|
62
|
+
values[category] = category in rawRetention
|
|
63
|
+
? parseRetentionValue(category, rawRetention[category], defaultValue, invalidValues)
|
|
64
|
+
: defaultValue;
|
|
65
|
+
}
|
|
66
|
+
return { values, defaultValue, invalidValues };
|
|
67
|
+
}
|
|
68
|
+
function listFiles(dirPath) {
|
|
69
|
+
if (!node_fs_1.default.existsSync(dirPath))
|
|
70
|
+
return [];
|
|
71
|
+
const out = [];
|
|
72
|
+
const stack = [dirPath];
|
|
73
|
+
while (stack.length) {
|
|
74
|
+
const current = stack.pop();
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = node_fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = node_path_1.default.join(current, entry.name);
|
|
84
|
+
if (entry.isDirectory())
|
|
85
|
+
stack.push(fullPath);
|
|
86
|
+
else if (entry.isFile())
|
|
87
|
+
out.push(fullPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function fileMtime(absolutePath) {
|
|
93
|
+
try {
|
|
94
|
+
return node_fs_1.default.statSync(absolutePath).mtime;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function parseDate(value) {
|
|
101
|
+
if (!value)
|
|
102
|
+
return undefined;
|
|
103
|
+
const parsed = new Date(value);
|
|
104
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
105
|
+
}
|
|
106
|
+
function loadGitLastChangeDates(workspaceRoot, relativeDir) {
|
|
107
|
+
const dates = new Map();
|
|
108
|
+
try {
|
|
109
|
+
const output = (0, node_child_process_1.execFileSync)('git', ['log', '--format=__FRAIM_CLEANUP_DATE__%cI', '--name-only', '--', relativeDir], {
|
|
110
|
+
cwd: workspaceRoot,
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
113
|
+
timeout: 10_000,
|
|
114
|
+
maxBuffer: 80 * 1024 * 1024,
|
|
115
|
+
});
|
|
116
|
+
let currentDate;
|
|
117
|
+
for (const rawLine of output.split(/\r?\n/)) {
|
|
118
|
+
const line = rawLine.trim();
|
|
119
|
+
if (!line)
|
|
120
|
+
continue;
|
|
121
|
+
if (line.startsWith('__FRAIM_CLEANUP_DATE__')) {
|
|
122
|
+
currentDate = parseDate(line.slice('__FRAIM_CLEANUP_DATE__'.length));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const relativePath = line.replace(/\\/g, '/');
|
|
126
|
+
if (currentDate && !dates.has(relativePath))
|
|
127
|
+
dates.set(relativePath, currentDate);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return new Map();
|
|
132
|
+
}
|
|
133
|
+
return dates;
|
|
134
|
+
}
|
|
135
|
+
function readFrontmatter(content) {
|
|
136
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
137
|
+
if (!match)
|
|
138
|
+
return {};
|
|
139
|
+
const values = {};
|
|
140
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
141
|
+
const kv = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
142
|
+
if (kv)
|
|
143
|
+
values[kv[1]] = kv[2].trim();
|
|
144
|
+
}
|
|
145
|
+
return values;
|
|
146
|
+
}
|
|
147
|
+
function synthesizedDate(absolutePath) {
|
|
148
|
+
try {
|
|
149
|
+
const frontmatter = readFrontmatter(node_fs_1.default.readFileSync(absolutePath, 'utf8'));
|
|
150
|
+
const value = frontmatter.synthesized;
|
|
151
|
+
if (!value)
|
|
152
|
+
return undefined;
|
|
153
|
+
const parsed = new Date(value);
|
|
154
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function userScoped(fileName, activeUserEmail) {
|
|
161
|
+
return fileName.startsWith(`${activeUserEmail}-`);
|
|
162
|
+
}
|
|
163
|
+
function evidenceEligibilityDate(workspaceRoot, absolutePath, gitDates) {
|
|
164
|
+
const relativePath = normalizeRelative(workspaceRoot, absolutePath);
|
|
165
|
+
return gitDates.get(relativePath) ?? fileMtime(absolutePath);
|
|
166
|
+
}
|
|
167
|
+
function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
168
|
+
const candidates = [];
|
|
169
|
+
const add = (category, absolutePath, eligible, reason, eligibilityDate) => {
|
|
170
|
+
candidates.push({
|
|
171
|
+
category,
|
|
172
|
+
absolutePath,
|
|
173
|
+
relativePath: normalizeRelative(workspaceRoot, absolutePath),
|
|
174
|
+
eligible,
|
|
175
|
+
reason,
|
|
176
|
+
eligibilityDate,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const archiveDir = node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee', 'learnings', 'archive');
|
|
180
|
+
for (const filePath of listFiles(archiveDir)) {
|
|
181
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
182
|
+
if (!fileName.endsWith('.md') || !userScoped(fileName, activeUserEmail))
|
|
183
|
+
continue;
|
|
184
|
+
add('learning_archive', filePath, true, 'Archived learning source for active user.', fileMtime(filePath));
|
|
185
|
+
}
|
|
186
|
+
const retrospectivesDir = node_path_1.default.join(workspaceRoot, 'docs', 'retrospectives');
|
|
187
|
+
for (const filePath of listFiles(retrospectivesDir)) {
|
|
188
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
189
|
+
if (!fileName.endsWith('.md') || !userScoped(fileName, activeUserEmail))
|
|
190
|
+
continue;
|
|
191
|
+
const date = synthesizedDate(filePath);
|
|
192
|
+
if (date)
|
|
193
|
+
add('retrospectives', filePath, true, 'Retrospective has synthesized frontmatter.', date);
|
|
194
|
+
else
|
|
195
|
+
add('retrospectives', filePath, false, 'Retrospective has no synthesized frontmatter.');
|
|
196
|
+
}
|
|
197
|
+
const evidenceDir = node_path_1.default.join(workspaceRoot, 'docs', 'evidence');
|
|
198
|
+
const evidenceGitDates = loadGitLastChangeDates(workspaceRoot, 'docs/evidence');
|
|
199
|
+
for (const filePath of listFiles(evidenceDir)) {
|
|
200
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
201
|
+
const isMarkdown = fileName.endsWith('.md');
|
|
202
|
+
const isJson = fileName.endsWith('.json');
|
|
203
|
+
const eligibilityDate = evidenceEligibilityDate(workspaceRoot, filePath, evidenceGitDates);
|
|
204
|
+
if (fileName.includes('cleanup-manifest') || fileName.includes('cleanup_manifest')) {
|
|
205
|
+
if (isMarkdown || isJson) {
|
|
206
|
+
add('cleanup_manifests', filePath, true, 'Cleanup manifest file.', eligibilityDate);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (fileName.endsWith('-feedback.md')) {
|
|
214
|
+
add('feedback', filePath, true, 'Feedback evidence file.', eligibilityDate);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
218
|
+
}
|
|
219
|
+
return candidates;
|
|
220
|
+
}
|
|
221
|
+
function daysBetween(from, to) {
|
|
222
|
+
return Math.floor((to.getTime() - from.getTime()) / 86_400_000);
|
|
223
|
+
}
|
|
224
|
+
function shouldDelete(candidate, retentionValue, now) {
|
|
225
|
+
if (!candidate.eligible)
|
|
226
|
+
return { delete: false, reason: candidate.reason };
|
|
227
|
+
if (retentionValue === -1)
|
|
228
|
+
return { delete: false, reason: 'Retention value -1 means never cleanup.' };
|
|
229
|
+
if (!candidate.eligibilityDate || Number.isNaN(candidate.eligibilityDate.getTime())) {
|
|
230
|
+
return { delete: false, reason: 'Eligibility date is unavailable or invalid.' };
|
|
231
|
+
}
|
|
232
|
+
if (retentionValue === 0)
|
|
233
|
+
return { delete: true, reason: 'Retention value 0 means cleanup immediately when eligible.' };
|
|
234
|
+
const ageDays = daysBetween(candidate.eligibilityDate, now);
|
|
235
|
+
if (ageDays >= retentionValue) {
|
|
236
|
+
return { delete: true, reason: `Eligible artifact age ${ageDays} days meets retention ${retentionValue} days.` };
|
|
237
|
+
}
|
|
238
|
+
return { delete: false, reason: `Eligible artifact age ${ageDays} days is below retention ${retentionValue} days.` };
|
|
239
|
+
}
|
|
240
|
+
function incrementCount(report, action) {
|
|
241
|
+
if (action === 'would_delete')
|
|
242
|
+
report.counts.wouldDelete++;
|
|
243
|
+
else
|
|
244
|
+
report.counts[action]++;
|
|
245
|
+
}
|
|
246
|
+
function runArtifactCleanup(workspaceRoot, options) {
|
|
247
|
+
const now = options.now ?? new Date();
|
|
248
|
+
const retention = resolveArtifactRetentionConfig(workspaceRoot);
|
|
249
|
+
const candidates = collectCandidates(workspaceRoot, options.activeUserEmail);
|
|
250
|
+
const report = {
|
|
251
|
+
generatedAt: now.toISOString(),
|
|
252
|
+
apply: options.apply,
|
|
253
|
+
retention: retention.values,
|
|
254
|
+
invalidConfig: retention.invalidValues,
|
|
255
|
+
counts: { retained: 0, wouldDelete: 0, deleted: 0, skipped: 0, failed: 0 },
|
|
256
|
+
items: [],
|
|
257
|
+
};
|
|
258
|
+
for (const candidate of candidates) {
|
|
259
|
+
const retentionValue = retention.values[candidate.category];
|
|
260
|
+
const decision = shouldDelete(candidate, retentionValue, now);
|
|
261
|
+
let action;
|
|
262
|
+
let error;
|
|
263
|
+
if (!candidate.eligible) {
|
|
264
|
+
action = 'skipped';
|
|
265
|
+
}
|
|
266
|
+
else if (!decision.delete) {
|
|
267
|
+
action = 'retained';
|
|
268
|
+
}
|
|
269
|
+
else if (!options.apply) {
|
|
270
|
+
action = 'would_delete';
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
try {
|
|
274
|
+
node_fs_1.default.unlinkSync(candidate.absolutePath);
|
|
275
|
+
action = 'deleted';
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
action = 'failed';
|
|
279
|
+
error = err instanceof Error ? err.message : String(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
incrementCount(report, action);
|
|
283
|
+
report.items.push({
|
|
284
|
+
category: candidate.category,
|
|
285
|
+
path: candidate.relativePath,
|
|
286
|
+
action,
|
|
287
|
+
reason: error ? `Deletion failed: ${decision.reason}` : decision.reason,
|
|
288
|
+
retentionValue,
|
|
289
|
+
eligibilityDate: candidate.eligibilityDate?.toISOString().slice(0, 10),
|
|
290
|
+
cleanupDate: action === 'deleted' || action === 'would_delete' ? now.toISOString().slice(0, 10) : undefined,
|
|
291
|
+
error,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return report;
|
|
295
|
+
}
|
|
296
|
+
function planArtifactCleanup(workspaceRoot, options) {
|
|
297
|
+
return runArtifactCleanup(workspaceRoot, { ...options, apply: false });
|
|
298
|
+
}
|