@xaccefy/pi-casefile 0.8.0 → 0.8.1
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/index.ts +5 -0
- package/src/ledger.ts +79 -19
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1487,6 +1487,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1487
1487
|
handler: async (args, ctx) => {
|
|
1488
1488
|
const next = parseXpModeArg(args ?? "", readXpMode());
|
|
1489
1489
|
writeXpMode(next);
|
|
1490
|
+
// Re-enabling after a mid-session /xp off must re-inject the workflow
|
|
1491
|
+
// on the next prompt — otherwise workflowInjected (module-level, set on
|
|
1492
|
+
// first enable) stays true and the workflow never comes back until the
|
|
1493
|
+
// process restarts.
|
|
1494
|
+
if (next !== "off") workflowInjected = false;
|
|
1490
1495
|
ctx.ui.notify(
|
|
1491
1496
|
`Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
|
|
1492
1497
|
next === "on" ? "info" : "warning",
|
package/src/ledger.ts
CHANGED
|
@@ -530,6 +530,32 @@ function mapRow(
|
|
|
530
530
|
};
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
+
/** Map raw snake_case DB rows to their camelCase item types. */
|
|
534
|
+
function mapEvidenceRow(row: any): EvidenceItem {
|
|
535
|
+
return {
|
|
536
|
+
id: row.id,
|
|
537
|
+
caseId: row.case_id,
|
|
538
|
+
role: row.role,
|
|
539
|
+
artifactPath: row.artifact_path ?? undefined,
|
|
540
|
+
sha256: row.sha256 ?? undefined,
|
|
541
|
+
summary: row.summary,
|
|
542
|
+
createdAt: row.created_at,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function mapCoverageRow(row: any): CoverageItem {
|
|
547
|
+
return {
|
|
548
|
+
id: row.id,
|
|
549
|
+
caseId: row.case_id,
|
|
550
|
+
asset: row.asset,
|
|
551
|
+
class: row.class,
|
|
552
|
+
scope: row.scope,
|
|
553
|
+
note: row.note,
|
|
554
|
+
testedBy: row.tested_by ?? undefined,
|
|
555
|
+
createdAt: row.created_at,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
533
559
|
/** Batch-fetch per-case item tables (evidence / coverage) for a set of ids. */
|
|
534
560
|
function fetchItemMap<T extends { caseId: string }>(
|
|
535
561
|
db: DatabaseSync,
|
|
@@ -540,12 +566,14 @@ function fetchItemMap<T extends { caseId: string }>(
|
|
|
540
566
|
const placeholders = ids.map(() => "?").join(",");
|
|
541
567
|
const rows = db
|
|
542
568
|
.prepare(`SELECT * FROM ${table} WHERE case_id IN (${placeholders}) ORDER BY created_at`)
|
|
543
|
-
.all(...ids) as
|
|
569
|
+
.all(...ids) as any[];
|
|
570
|
+
const mapRow = table === "evidence_items" ? mapEvidenceRow : mapCoverageRow;
|
|
544
571
|
const map = new Map<string, T[]>();
|
|
545
572
|
for (const row of rows) {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
573
|
+
const item = mapRow(row) as unknown as T;
|
|
574
|
+
const bucket = map.get(item.caseId);
|
|
575
|
+
if (bucket) bucket.push(item);
|
|
576
|
+
else map.set(item.caseId, [item]);
|
|
549
577
|
}
|
|
550
578
|
return map;
|
|
551
579
|
}
|
|
@@ -603,12 +631,16 @@ export function getCaseById(id: string): CaseRecord | undefined {
|
|
|
603
631
|
|
|
604
632
|
const linkStmt = db.prepare("SELECT target_id, kind FROM case_links WHERE source_id = ?");
|
|
605
633
|
const links = linkStmt.all(id) as { target_id: string; kind: string }[];
|
|
606
|
-
const evidence =
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
634
|
+
const evidence = (
|
|
635
|
+
db
|
|
636
|
+
.prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
|
|
637
|
+
.all(id) as any[]
|
|
638
|
+
).map(mapEvidenceRow);
|
|
639
|
+
const coverage = (
|
|
640
|
+
db
|
|
641
|
+
.prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
|
|
642
|
+
.all(id) as any[]
|
|
643
|
+
).map(mapCoverageRow);
|
|
612
644
|
|
|
613
645
|
return mapRow(
|
|
614
646
|
row,
|
|
@@ -1238,9 +1270,11 @@ export function addEvidenceItemResult(
|
|
|
1238
1270
|
|
|
1239
1271
|
export function listEvidenceItems(caseId: string): EvidenceItem[] {
|
|
1240
1272
|
const db = getDb();
|
|
1241
|
-
return
|
|
1242
|
-
|
|
1243
|
-
|
|
1273
|
+
return (
|
|
1274
|
+
db
|
|
1275
|
+
.prepare("SELECT * FROM evidence_items WHERE case_id = ? ORDER BY created_at")
|
|
1276
|
+
.all(caseId) as any[]
|
|
1277
|
+
).map(mapEvidenceRow);
|
|
1244
1278
|
}
|
|
1245
1279
|
|
|
1246
1280
|
// ── Coverage items ──────────────────────────────────────────────────
|
|
@@ -1311,9 +1345,11 @@ export function recordCoverageResult(
|
|
|
1311
1345
|
|
|
1312
1346
|
export function listCoverage(caseId: string): CoverageItem[] {
|
|
1313
1347
|
const db = getDb();
|
|
1314
|
-
return
|
|
1315
|
-
|
|
1316
|
-
|
|
1348
|
+
return (
|
|
1349
|
+
db
|
|
1350
|
+
.prepare("SELECT * FROM coverage_items WHERE case_id = ? ORDER BY created_at")
|
|
1351
|
+
.all(caseId) as any[]
|
|
1352
|
+
).map(mapCoverageRow);
|
|
1317
1353
|
}
|
|
1318
1354
|
|
|
1319
1355
|
export type CoverageSummary = {
|
|
@@ -1699,13 +1735,26 @@ function hasChainClass(c: CaseRecord, re: RegExp): boolean {
|
|
|
1699
1735
|
return re.test(chainText(c));
|
|
1700
1736
|
}
|
|
1701
1737
|
|
|
1738
|
+
/** Reduce a target string to a bare hostname (strip scheme, port, path). */
|
|
1739
|
+
function normalizeTargetHost(target: string): string {
|
|
1740
|
+
let h = target
|
|
1741
|
+
.toLowerCase()
|
|
1742
|
+
.trim()
|
|
1743
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
1744
|
+
h = h.split("?")[0].split("/")[0].split(":")[0];
|
|
1745
|
+
return h.trim();
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1702
1748
|
/** Same asset or related (same eTLD+1) — chains only pair cases on one target. */
|
|
1703
1749
|
function sameAssetOrRelated(a: CaseRecord, b: CaseRecord): boolean {
|
|
1704
|
-
const ta = (a.target ?? "")
|
|
1705
|
-
const tb = (b.target ?? "")
|
|
1750
|
+
const ta = normalizeTargetHost(a.target ?? "");
|
|
1751
|
+
const tb = normalizeTargetHost(b.target ?? "");
|
|
1706
1752
|
if (!ta || !tb) return false;
|
|
1707
1753
|
if (ta === tb) return true;
|
|
1708
|
-
|
|
1754
|
+
// Subdomain relation requires a label boundary: "api.example.com" vs
|
|
1755
|
+
// "example.com" pair, but "myshop.io" vs "shop.io" do NOT — a bare
|
|
1756
|
+
// substring check pairs unrelated targets whose names merely overlap.
|
|
1757
|
+
if (ta.endsWith(`.${tb}`) || tb.endsWith(`.${ta}`)) return true;
|
|
1709
1758
|
return eTLDPlus1(ta) === eTLDPlus1(tb);
|
|
1710
1759
|
}
|
|
1711
1760
|
|
|
@@ -2122,6 +2171,17 @@ export function formatCaseDetail(record: CaseRecord): string {
|
|
|
2122
2171
|
display = (val as { id: string; kind: string }[])
|
|
2123
2172
|
.map((l) => `${l.id} (${l.kind})`)
|
|
2124
2173
|
.join(", ");
|
|
2174
|
+
} else if (key === "evidenceItems") {
|
|
2175
|
+
display = (val as EvidenceItem[])
|
|
2176
|
+
.map(
|
|
2177
|
+
(e) =>
|
|
2178
|
+
`[${e.role}] ${e.summary}${e.artifactPath ? ` — \`${e.artifactPath}\` sha256:\`${e.sha256?.slice(0, 12) ?? "?"}\`` : ""} (${e.createdAt})`,
|
|
2179
|
+
)
|
|
2180
|
+
.join("\n");
|
|
2181
|
+
} else if (key === "coverageItems") {
|
|
2182
|
+
display = (val as CoverageItem[])
|
|
2183
|
+
.map((c) => `[${c.scope}] ${c.asset} × ${c.class} — ${c.note}`)
|
|
2184
|
+
.join("\n");
|
|
2125
2185
|
} else if (Array.isArray(val)) {
|
|
2126
2186
|
display = val.join(", ");
|
|
2127
2187
|
} else if (typeof val === "object") {
|