fraim 2.0.344 → 2.0.345

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.
@@ -22,7 +22,7 @@ function safePart(value) {
22
22
  return Boolean(value) && value !== '.' && value !== '..' && !/[\\/:*?"<>|\x00-\x1f]/.test(value)
23
23
  && value.trim() === value && !/[. ]$/.test(value);
24
24
  }
25
- /** Explicit paths keep parsing pure; otherwise resolve the owning configuration. Callers guard physical source containment. */
25
+ /** Resolve only within the supplied owner. Callers guard physical source containment. */
26
26
  function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, configuredPaths) {
27
27
  const invalid = (reason) => ({ status: 'invalid', reason });
28
28
  const normalized = content.replace(/^\uFEFF/, '');
@@ -49,7 +49,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
49
49
  for (const line of fm[1].split(/\r?\n/)) {
50
50
  const match = /^([A-Za-z][A-Za-z0-9]*):[ \t]*(.*)$/.exec(line);
51
51
  if (!match) {
52
- if (/^\s*["']?(artifactKind|author|date|job|runId|usageSourceRef|synthesized)["']?\s*:/.test(line))
52
+ if (/^\s*["']?(artifactKind|author|date|job|runId|sourceId|usageSourceRef|synthesized)["']?\s*:/.test(line))
53
53
  malformedField = true;
54
54
  continue;
55
55
  }
@@ -76,7 +76,7 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
76
76
  return invalid('Unsupported metadata key spelling');
77
77
  if (duplicates.size)
78
78
  return invalid(`Duplicate metadata: ${[...duplicates].join(', ')}`);
79
- for (const field of ['author', 'date', 'job', 'runId', 'usageSourceRef', 'synthesized']) {
79
+ for (const field of ['author', 'date', 'job', 'runId', 'sourceId', 'usageSourceRef', 'synthesized']) {
80
80
  if (!Object.prototype.hasOwnProperty.call(fields, field))
81
81
  return invalid(`Missing metadata: ${field}`);
82
82
  }
@@ -110,13 +110,22 @@ function parseLearningSourceRecord(content, filePath, homeRoot, contentRoot, con
110
110
  return invalid('Source is outside its configured learning location');
111
111
  if (state === 'archived' && !fields.synthesized)
112
112
  return invalid('Archive has no successful processing date');
113
- if (!node_path_1.default.isAbsolute(fields.usageSourceRef) || /[\x00-\x1f]/.test(fields.usageSourceRef)
114
- || fields.usageSourceRef.includes('\\')
115
- || fields.usageSourceRef !== node_path_1.default.resolve(fields.usageSourceRef).replace(/\\/g, '/')
116
- || normalizeLearningSourcePath(fields.usageSourceRef) !== normalizeLearningSourcePath(pendingPath)) {
117
- return invalid('usageSourceRef must be the normalized absolute original pending path');
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');
118
125
  }
119
126
  return { status: 'valid', record: { artifactKind: fields.artifactKind, kind, author: fields.author,
120
127
  date: fields.date, job: fields.job, runId: fields.runId, usageSourceRef: fields.usageSourceRef,
128
+ usageIdentity: `learning-source:${fields.sourceId}`,
129
+ sourceId: fields.sourceId,
121
130
  synthesized: fields.synthesized, pendingPath, archivePath, state } };
122
131
  }
@@ -213,14 +213,14 @@ function runBackfill(workspaceRoot, userId, options) {
213
213
  const { record } = parsed;
214
214
  if (record.kind !== 'retrospective' || record.author !== userId)
215
215
  continue;
216
- if (seen.has(record.usageSourceRef))
216
+ if (seen.has(record.usageIdentity))
217
217
  continue;
218
- seen.add(record.usageSourceRef);
218
+ seen.add(record.usageIdentity);
219
219
  report.filesScanned++;
220
220
  if (record.synthesized)
221
221
  report.synthesizedFilesRead++;
222
222
  const result = recordAttestationsFromRetrospective(workspaceRoot, userId, {
223
- content: parsed.content, source: record.usageSourceRef, job: record.job, date: record.date,
223
+ content: parsed.content, source: record.usageIdentity, job: record.job, date: record.date,
224
224
  agent: options.agent, model: options.model, legacy: false,
225
225
  limits, now: options.now, storePath: options.storePath, integrityMode: 'flag',
226
226
  });
@@ -135,7 +135,7 @@ const recordFiringsCommand = addCommonOptions(new commander_1.Command('record-fi
135
135
  throw new Error(`Retrospective not found: ${filePath}`);
136
136
  }
137
137
  const { content, record } = (0, learning_usage_attestation_1.readRetrospectiveForAttestation)(workspaceRoot, user, filePath);
138
- const source = record.usageSourceRef;
138
+ const source = record.usageIdentity;
139
139
  const result = (0, learning_usage_attestation_1.recordAttestationsFromRetrospective)(workspaceRoot, user, {
140
140
  content,
141
141
  source,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.344",
3
+ "version": "2.0.345",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {