@xaccefy/pi-casefile 0.7.3 → 0.7.4
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/package.json +1 -1
- package/src/ledger.ts +7 -1
- package/src/pipeline-submit.ts +12 -3
- package/src/scratchpad.ts +16 -3
package/package.json
CHANGED
package/src/ledger.ts
CHANGED
|
@@ -1577,7 +1577,13 @@ function buildScratchpadSection(caseId: string): string {
|
|
|
1577
1577
|
const resume = scratchpad_resume(entry.name);
|
|
1578
1578
|
if (!resume) continue;
|
|
1579
1579
|
const allIds = Object.values(resume.checkpoint.phase_ids ?? {}).flat() as string[];
|
|
1580
|
-
|
|
1580
|
+
// Gate on the case id appearing in phase_ids OR in any artifact filename —
|
|
1581
|
+
// checkpoint ids are often empty for recon/hunt, while artifact names like
|
|
1582
|
+
// skeptic_case_<id>.json / trace_case_<id>.json are equally valid evidence.
|
|
1583
|
+
const namedInArtifact = Object.values(resume.artifacts)
|
|
1584
|
+
.flat()
|
|
1585
|
+
.some((n) => n.includes(caseId));
|
|
1586
|
+
if (!allIds.includes(caseId) && !namedInArtifact) continue;
|
|
1581
1587
|
|
|
1582
1588
|
sections.push(`### Run: ${entry.name} (project root: ${resume.checkpoint.project_root})`);
|
|
1583
1589
|
for (const phase of CONTEXT_PHASES) {
|
package/src/pipeline-submit.ts
CHANGED
|
@@ -251,11 +251,20 @@ function parseOutput(output: unknown): { obj?: Record<string, unknown>; error?:
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
/** Stable repair-bucket key for a submission. */
|
|
254
|
+
/** Placeholder-y ids that carry no identity (observed in the wild: every
|
|
255
|
+
* submission in a run keyed "false"). Trusting them makes distinct findings
|
|
256
|
+
* share one key — one artifact name, one repair-budget bucket — so the last
|
|
257
|
+
* write clobbers the rest. Fall back to the content hash instead. */
|
|
258
|
+
const JUNK_ID_RE =
|
|
259
|
+
/^(false|true|null|none|undefined|n\/?a|na|unknown|missing|empty|todo|tbd|pending|not-set)$/i;
|
|
260
|
+
|
|
254
261
|
function submissionKey(stage: SubmitStage, obj: Record<string, unknown>): string {
|
|
262
|
+
const candidate =
|
|
263
|
+
(typeof obj.finding_id === "string" && obj.finding_id.trim()) ||
|
|
264
|
+
(typeof obj.title === "string" && obj.title.trim()) ||
|
|
265
|
+
(typeof obj.id === "string" && obj.id.trim());
|
|
255
266
|
const id =
|
|
256
|
-
|
|
257
|
-
(typeof obj.title === "string" && obj.title) ||
|
|
258
|
-
(typeof obj.id === "string" && obj.id);
|
|
267
|
+
candidate && candidate.length >= 3 && !JUNK_ID_RE.test(candidate) ? candidate : undefined;
|
|
259
268
|
const tail = id ?? createHash("sha1").update(JSON.stringify(obj)).digest("hex").slice(0, 8);
|
|
260
269
|
return `${stage}:${tail}`;
|
|
261
270
|
}
|
package/src/scratchpad.ts
CHANGED
|
@@ -150,6 +150,19 @@ export function getStatePath(runId: string, projectRoot?: string): string {
|
|
|
150
150
|
return join(getRunDir(runId, projectRoot), "state.json");
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Sanitize an artifact name into a safe filename. Same allowlist as run_ids,
|
|
155
|
+
* plus dot-only rejection: a name like `..` or `.` would otherwise let join()
|
|
156
|
+
* point at the phase/run directory itself (EISDIR crash on write/read).
|
|
157
|
+
*/
|
|
158
|
+
function sanitizeArtifactName(artifactName: string): string {
|
|
159
|
+
const safe = artifactName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
160
|
+
if (!safe || /^\.+$/.test(safe)) {
|
|
161
|
+
throw new Error(`Invalid artifact name: "${artifactName}" — nothing left after sanitization`);
|
|
162
|
+
}
|
|
163
|
+
return safe;
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
function emptyCheckpoint(runId: string, projectRoot: string): ScratchpadCheckpoint {
|
|
154
167
|
const now = new Date().toISOString();
|
|
155
168
|
return {
|
|
@@ -229,8 +242,8 @@ export function scratchpad_write(
|
|
|
229
242
|
const runDir = getRunDir(runId, root);
|
|
230
243
|
ensureRunDirs(runDir);
|
|
231
244
|
|
|
232
|
-
// Sanitize artifact name: no path traversal.
|
|
233
|
-
const safeName = artifactName
|
|
245
|
+
// Sanitize artifact name: no path traversal, no dot-only escape.
|
|
246
|
+
const safeName = sanitizeArtifactName(artifactName);
|
|
234
247
|
const dir = join(runDir, PHASE_DIRS[phase]);
|
|
235
248
|
const filePath = join(dir, safeName);
|
|
236
249
|
writeFileSync(filePath, content, "utf8");
|
|
@@ -247,7 +260,7 @@ export function scratchpad_read(
|
|
|
247
260
|
projectRoot?: string,
|
|
248
261
|
): string | null {
|
|
249
262
|
const root = projectRoot ?? detectWorkspaceRoot();
|
|
250
|
-
const safeName = artifactName
|
|
263
|
+
const safeName = sanitizeArtifactName(artifactName);
|
|
251
264
|
const filePath = join(getRunDir(runId, root), PHASE_DIRS[phase], safeName);
|
|
252
265
|
if (!existsSync(filePath)) return null;
|
|
253
266
|
return readFileSync(filePath, "utf8");
|