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.
@@ -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 the current FRAIM workspace')
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>', 'Alias for --workspace-root')
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
- * Validates evidence.reviewHandoff. Returns null if valid, or an array of
259
- * human-readable error strings describing what is wrong.
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 = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
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(/^---\r?\n([\s\S]*?)\r?\n---/);
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(fileName, activeUserEmail) {
171
- return fileName.startsWith(`${activeUserEmail}-`);
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 archiveDir = node_path_1.default.join(workspaceRoot, 'fraim', 'personalized-employee', 'learnings', 'archive');
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(fileName, activeUserEmail))
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 retrospectivesDir = node_path_1.default.join(workspaceRoot, 'docs', 'retrospectives');
198
- for (const filePath of listFiles(retrospectivesDir)) {
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(fileName, activeUserEmail))
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);