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
|
-
/**
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
216
|
+
if (seen.has(record.usageIdentity))
|
|
217
217
|
continue;
|
|
218
|
-
seen.add(record.
|
|
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.
|
|
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.
|
|
138
|
+
const source = record.usageIdentity;
|
|
139
139
|
const result = (0, learning_usage_attestation_1.recordAttestationsFromRetrospective)(workspaceRoot, user, {
|
|
140
140
|
content,
|
|
141
141
|
source,
|