pm-csv 2026.10.22 → 2026.10.23
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/CHANGELOG.md +21 -0
- package/README.md +43 -0
- package/dist/atomic-update.d.ts +3 -2
- package/dist/atomic-update.d.ts.map +1 -1
- package/dist/atomic-update.js +10 -4
- package/dist/atomic-update.js.map +1 -1
- package/dist/edit-refusal.d.ts +21 -0
- package/dist/edit-refusal.d.ts.map +1 -0
- package/dist/edit-refusal.js +21 -0
- package/dist/edit-refusal.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -2
- package/dist/index.js.map +1 -1
- package/dist/lease-guard.d.ts +14 -0
- package/dist/lease-guard.d.ts.map +1 -0
- package/dist/lease-guard.js +25 -0
- package/dist/lease-guard.js.map +1 -0
- package/dist/lineage.d.ts +72 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +137 -0
- package/dist/lineage.js.map +1 -0
- package/dist/roundtrip.d.ts +54 -0
- package/dist/roundtrip.d.ts.map +1 -0
- package/dist/roundtrip.js +255 -0
- package/dist/roundtrip.js.map +1 -0
- package/docs/row-lineage-validation.md +49 -0
- package/docs/row-lineage.md +87 -0
- package/docs/spreadsheet-roundtrip.md +146 -0
- package/docs/spreadsheet-validation.md +47 -0
- package/manifest.json +1 -1
- package/package.json +3 -1
package/dist/lineage.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { create, locateItem, normalizeItemId, PmCliError, readSettings } from "@unbrained/pm-cli/sdk";
|
|
5
|
+
import { serializeCSV } from "./index.js";
|
|
6
|
+
import { readUpdateDocument } from "./atomic-update.js";
|
|
7
|
+
/** Deliberately bounded import/diagnostic field surface; other columns never leave the input. */
|
|
8
|
+
export const LINEAGE_FIELDS = ["title", "body", "type", "status", "priority", "tags", "assignee", "sprint", "release"];
|
|
9
|
+
/** Stable actionable refusals never contain source values or filesystem paths. */
|
|
10
|
+
export class LineageRefusal extends PmCliError {
|
|
11
|
+
/** Stable machine-readable category. */
|
|
12
|
+
code;
|
|
13
|
+
/** Action required before retry. */
|
|
14
|
+
remedy;
|
|
15
|
+
constructor(code, remedy) {
|
|
16
|
+
super(`${code}: ${remedy}`, 2, { code, required: remedy, nextSteps: [remedy] });
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.remedy = remedy;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Versioned SHA-256 over JSON values; artifacts specify this canonicalization. */
|
|
22
|
+
export function lineageDigest(value) {
|
|
23
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
/** Refuse an invalid table or disclosure policy before any mutation occurs. */
|
|
26
|
+
function planMapping(table, options) {
|
|
27
|
+
const headers = table[0];
|
|
28
|
+
const fields = options.columns;
|
|
29
|
+
if (!headers?.length || table.length > 1001 || !fields.includes("title") || new Set(fields).size !== fields.length
|
|
30
|
+
|| fields.some(field => !LINEAGE_FIELDS.includes(field)) || new Set(headers).size !== headers.length) {
|
|
31
|
+
throw new LineageRefusal("csv_lineage_invalid", "Provide a header and at most 1000 rows; select distinct supported --columns including title and remove duplicate headers.");
|
|
32
|
+
}
|
|
33
|
+
const fieldMap = options.fieldMap ?? {};
|
|
34
|
+
if (Object.entries(fieldMap).some(([header, field]) => !headers.includes(header) || !fields.includes(field))) {
|
|
35
|
+
throw new LineageRefusal("csv_lineage_invalid", "Map only existing source headers to explicitly allowed columns.");
|
|
36
|
+
}
|
|
37
|
+
const mapping = headers.flatMap((header, index) => {
|
|
38
|
+
const field = Object.hasOwn(fieldMap, header) ? fieldMap[header] : header;
|
|
39
|
+
return fields.includes(field) ? [{ column: index + 1, field: field, header }] : [];
|
|
40
|
+
});
|
|
41
|
+
if (mapping.length !== fields.length || new Set(mapping.map(cell => cell.field)).size !== fields.length) {
|
|
42
|
+
throw new LineageRefusal("csv_lineage_invalid", "Supply exactly one source column for every allowed field; resolve missing or ambiguous mappings.");
|
|
43
|
+
}
|
|
44
|
+
return mapping;
|
|
45
|
+
}
|
|
46
|
+
/** Recover a committed native create by its stable key, including a crash before artifact export. */
|
|
47
|
+
async function existingRow(pmRoot, id, marker) {
|
|
48
|
+
if (!await locateItem(pmRoot, id))
|
|
49
|
+
return undefined;
|
|
50
|
+
const document = await readUpdateDocument(pmRoot, id);
|
|
51
|
+
return document.metadata.tags?.includes(marker) ? "replayed" : "identity_conflict";
|
|
52
|
+
}
|
|
53
|
+
/** Import bounded rows with native SDK validation/history/locking and stable explicit IDs.
|
|
54
|
+
* Rejected artifacts contain only allowed columns plus a stable retry key. Replaying
|
|
55
|
+
* corrected rejects cannot apply successful source rows because those rows are absent.
|
|
56
|
+
*/
|
|
57
|
+
export async function importLineage(pmRoot, table, options) {
|
|
58
|
+
const mapping = planMapping(table, options);
|
|
59
|
+
const sourceDigest = lineageDigest(table);
|
|
60
|
+
const mappingRevision = lineageDigest([1, mapping]);
|
|
61
|
+
const keyColumn = table[0].indexOf("_csv_key");
|
|
62
|
+
const keys = table.slice(1).map((row, index) => keyColumn < 0 ? lineageDigest([sourceDigest, index + 2]) : row[keyColumn]);
|
|
63
|
+
if (keys.some(key => !/^[a-f0-9]{64}$/.test(key)) || new Set(keys).size !== keys.length) {
|
|
64
|
+
throw new LineageRefusal("csv_lineage_invalid", "Keep distinct unchanged _csv_key values in the rejected CSV; use the original artifact if keys were lost.");
|
|
65
|
+
}
|
|
66
|
+
const settings = await readSettings(pmRoot);
|
|
67
|
+
const rows = [];
|
|
68
|
+
const rejected = [["_csv_key", ...mapping.map(cell => cell.field)]];
|
|
69
|
+
for (const [index, row] of table.slice(1).entries()) {
|
|
70
|
+
const key = keys[index];
|
|
71
|
+
const values = Object.fromEntries([...mapping].sort((left, right) => left.field.localeCompare(right.field)).map(cell => [cell.field, (row[cell.column - 1] ?? "").trim()]));
|
|
72
|
+
const marker = `csv-lineage:${lineageDigest([key, values])}`;
|
|
73
|
+
const id = normalizeItemId(`csv-${key}`, settings.id_prefix);
|
|
74
|
+
const record = { row: index + 2, key, outcome: "rejected", cells: [] };
|
|
75
|
+
rows.push(record);
|
|
76
|
+
if (row.length !== table[0].length || !values.title || (values.priority && !/^[0-4]$/.test(values.priority))) {
|
|
77
|
+
record.code = "invalid_row";
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
let outcome = await existingRow(pmRoot, id, marker);
|
|
81
|
+
if (!outcome) {
|
|
82
|
+
try {
|
|
83
|
+
await create({ ...Object.fromEntries(Object.entries(values).filter(([, value]) => value !== "")), id, tags: [values.tags, marker].filter(Boolean).join(",") }, { pmRoot, author: options.author ?? "pm-csv" });
|
|
84
|
+
record.outcome = "imported";
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// A racing writer may have won the same native create; inspect durable state.
|
|
88
|
+
outcome = await existingRow(pmRoot, id, marker);
|
|
89
|
+
record.code = outcome ? undefined : "create_refused";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (outcome === "identity_conflict")
|
|
93
|
+
record.code = outcome;
|
|
94
|
+
if (outcome === "replayed")
|
|
95
|
+
record.outcome = outcome;
|
|
96
|
+
if (record.outcome !== "rejected") {
|
|
97
|
+
record.item = id;
|
|
98
|
+
const document = await readUpdateDocument(pmRoot, id);
|
|
99
|
+
record.cells = mapping.map(cell => ({ column: cell.column, field: cell.field,
|
|
100
|
+
sourceDigest: lineageDigest(row[cell.column - 1]),
|
|
101
|
+
valueDigest: lineageDigest(cell.field === "body" ? document.body : document.metadata[cell.field] ?? null),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (record.outcome === "rejected")
|
|
106
|
+
rejected.push([key, ...mapping.map(cell => row[cell.column - 1] ?? "")]);
|
|
107
|
+
}
|
|
108
|
+
const base = { version: 1, sourceDigest, mappingRevision, mapping, rows, remedies: {
|
|
109
|
+
invalid_row: "Correct the title, priority (0-4), or row width in rejected.csv, then retry that file.",
|
|
110
|
+
create_refused: "Check the tracker schema, required type fields, terminal-status reason, permissions and pm health; correct rejected.csv and retry.",
|
|
111
|
+
identity_conflict: "This key already belongs to different imported values. Restore the unchanged rejected artifact or use pm update for intentional changes.",
|
|
112
|
+
}, rejectedDigest: lineageDigest(rejected) };
|
|
113
|
+
const manifest = { ...base, digest: lineageDigest(base) };
|
|
114
|
+
return { manifest, rejected, receipt: { rows: rows.length,
|
|
115
|
+
imported: rows.filter(row => row.outcome === "imported").length,
|
|
116
|
+
replayed: rows.filter(row => row.outcome === "replayed").length,
|
|
117
|
+
rejected: rejected.length - 1, manifest: manifest.digest,
|
|
118
|
+
} };
|
|
119
|
+
}
|
|
120
|
+
/** Publish only new artifacts; exact-byte checksums complement parsed-content digests. */
|
|
121
|
+
export function writeLineageBundle(output, bundle) {
|
|
122
|
+
const rejectedCsv = serializeCSV(bundle.rejected, ",") + "\n";
|
|
123
|
+
try {
|
|
124
|
+
const manifest = JSON.stringify(bundle.manifest);
|
|
125
|
+
const checksums = {
|
|
126
|
+
"rejected.csv": createHash("sha256").update(rejectedCsv).digest("hex"),
|
|
127
|
+
"manifest.json": createHash("sha256").update(manifest).digest("hex"),
|
|
128
|
+
};
|
|
129
|
+
writeFileSync(join(output, "rejected.csv"), rejectedCsv, { flag: "wx", mode: 0o600 });
|
|
130
|
+
writeFileSync(join(output, "manifest.json"), manifest, { flag: "wx", mode: 0o600 });
|
|
131
|
+
writeFileSync(join(output, "checksums.json"), JSON.stringify(checksums), { flag: "wx", mode: 0o600 });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new LineageRefusal("csv_bundle_io", "Keep partial artifacts; rerun the unchanged input into a new directory to recover committed rows by stable identity.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=lineage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineage.js","sourceRoot":"","sources":["../lineage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CommitWorkspaceTransactionOptions } from "@unbrained/pm-cli/sdk";
|
|
2
|
+
export { EditRefusal } from "./edit-refusal.ts";
|
|
3
|
+
export type { EditConflict, EditRefusalCode } from "./edit-refusal.ts";
|
|
4
|
+
/** Versioned, deliberately scalar spreadsheet patch surface; status is read-only. */
|
|
5
|
+
export declare const EDIT_FIELDS: readonly ["title", "body", "priority", "assignee", "sprint", "release"];
|
|
6
|
+
/** Editable cell names accepted by the session protocol. */
|
|
7
|
+
export type EditField = typeof EDIT_FIELDS[number];
|
|
8
|
+
/** Baseline for one stable item key. Values are exact spreadsheet strings. */
|
|
9
|
+
export interface EditSessionRow {
|
|
10
|
+
id: string;
|
|
11
|
+
revision: string;
|
|
12
|
+
itemDigest: string;
|
|
13
|
+
status: string;
|
|
14
|
+
values: Record<EditField, string>;
|
|
15
|
+
digests: Record<EditField, string>;
|
|
16
|
+
}
|
|
17
|
+
/** Portable immutable baseline; retain this alongside the edited CSV. */
|
|
18
|
+
export interface EditSession {
|
|
19
|
+
version: 1;
|
|
20
|
+
id: string;
|
|
21
|
+
rows: EditSessionRow[];
|
|
22
|
+
digest: string;
|
|
23
|
+
}
|
|
24
|
+
/** Bounded agent receipt for both dry runs and successful application. */
|
|
25
|
+
export interface EditReceipt {
|
|
26
|
+
rows: number;
|
|
27
|
+
changed: number;
|
|
28
|
+
unchanged: number;
|
|
29
|
+
concurrent: number;
|
|
30
|
+
applied: number;
|
|
31
|
+
transaction?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Fixed schema makes altered/missing/duplicate metadata columns fail closed. */
|
|
34
|
+
export declare function editSessionHeader(): string[];
|
|
35
|
+
/** Export explicit stable keys and digests without creating tracker metadata. */
|
|
36
|
+
export declare function exportEditSession(pmRoot: string, ids: readonly string[]): Promise<{
|
|
37
|
+
session: EditSession;
|
|
38
|
+
table: string[][];
|
|
39
|
+
}>;
|
|
40
|
+
/** Validate an untrusted sidecar structurally as well as by its content digest. */
|
|
41
|
+
export declare function parseEditSession(input: unknown): EditSession;
|
|
42
|
+
/**
|
|
43
|
+
* Three-way patch using stable item keys, field-level conflict checks and the existing SDK
|
|
44
|
+
* transaction/CSV compensation. Preflight refusals create no transaction or
|
|
45
|
+
* item/history writes. Field checks run under each native item lease; protection
|
|
46
|
+
* after lease expiry needs host write fencing. Prior rows are compensated on
|
|
47
|
+
* ordinary failure, which explicitly requires reconciliation.
|
|
48
|
+
*/
|
|
49
|
+
export declare function applyEditSession(pmRoot: string, input: unknown, table: string[][], options: {
|
|
50
|
+
author: string;
|
|
51
|
+
dryRun?: boolean;
|
|
52
|
+
onTransition?: CommitWorkspaceTransactionOptions["onTransition"];
|
|
53
|
+
}): Promise<EditReceipt>;
|
|
54
|
+
//# sourceMappingURL=roundtrip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roundtrip.d.ts","sourceRoot":"","sources":["../roundtrip.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iCAAiC,EAA0C,MAAM,uBAAuB,CAAC;AAIvH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAGvE,qFAAqF;AACrF,eAAO,MAAM,WAAW,YAAI,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAU,CAAC;AACnG,4DAA4D;AAC5D,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;AACnD,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;CACpC;AACD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgDD,iFAAiF;AACjF,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAE5C;AAYD,iFAAiF;AACjF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;CAAE,CAAC,CAYpI;AAED,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAe5D;AA4FD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,iCAAiC,CAAC,cAAc,CAAC,CAAA;CAAE,GACjK,OAAO,CAAC,WAAW,CAAC,CA6CtB"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { acquireLock, commitWorkspaceTransaction, getHistoryPath, mutateItem, readHistoryEntries, readSettings, verifyHistoryEntries, } from "@unbrained/pm-cli/sdk";
|
|
3
|
+
import { EditRefusal } from "./edit-refusal.js";
|
|
4
|
+
import { assertEditLease, guardEditMutation } from "./lease-guard.js";
|
|
5
|
+
export { EditRefusal } from "./edit-refusal.js";
|
|
6
|
+
import { captureUpdateCompensation, compensateUpdate, inspectUpdate, readUpdateDocument } from "./atomic-update.js";
|
|
7
|
+
/** Versioned, deliberately scalar spreadsheet patch surface; status is read-only. */
|
|
8
|
+
export const EDIT_FIELDS = ["title", "body", "priority", "assignee", "sprint", "release"];
|
|
9
|
+
/** Hash versioned JSON tuples rather than ambiguous concatenated strings. */
|
|
10
|
+
function digest(value) {
|
|
11
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
/** Render supported scalar values exactly as the spreadsheet sees them. */
|
|
14
|
+
function cells(document) {
|
|
15
|
+
return Object.fromEntries(EDIT_FIELDS.map(field => [field, String(field === "body" ? document.body : document.metadata[field] ?? "")]));
|
|
16
|
+
}
|
|
17
|
+
/** Read an item and its verified history while holding its native SDK lock. */
|
|
18
|
+
async function snapshot(pmRoot, id) {
|
|
19
|
+
const settings = await readSettings(pmRoot);
|
|
20
|
+
const release = await acquireLock(pmRoot, id, settings.locks.ttl_seconds, "pm-csv", false, false, settings.locks.wait_ms);
|
|
21
|
+
const startedAt = performance.now();
|
|
22
|
+
let row;
|
|
23
|
+
try {
|
|
24
|
+
const document = await readUpdateDocument(pmRoot, id);
|
|
25
|
+
row = await documentRow(pmRoot, id, document);
|
|
26
|
+
assertEditLease(performance.now() - startedAt, settings.locks.ttl_seconds);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof EditRefusal)
|
|
30
|
+
throw error;
|
|
31
|
+
const refusal = new EditRefusal("csv_session_invalid", "Verify that every exported item still exists and is readable; export a fresh session after resolving missing items.", [{ id, fields: ["item"] }]);
|
|
32
|
+
refusal.cause = error;
|
|
33
|
+
throw refusal;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await release();
|
|
37
|
+
}
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
/** Bind the full item and history revision to the exported scalar baseline. */
|
|
41
|
+
async function documentRow(pmRoot, id, document) {
|
|
42
|
+
if (document.metadata.id !== id) {
|
|
43
|
+
throw new EditRefusal("csv_session_invalid", "Use complete canonical item IDs from pm list; abbreviated keys cannot identify an edit session.");
|
|
44
|
+
}
|
|
45
|
+
const history = await readHistoryEntries(getHistoryPath(pmRoot, id), id);
|
|
46
|
+
if (!verifyHistoryEntries(history, document).ok) {
|
|
47
|
+
throw new EditRefusal("csv_session_invalid", "Verify item history with pm health before exporting or applying a session.", [{ id, fields: ["history"] }]);
|
|
48
|
+
}
|
|
49
|
+
const values = cells(document);
|
|
50
|
+
return {
|
|
51
|
+
id, revision: digest(history), itemDigest: digest(document), status: document.metadata.status, values,
|
|
52
|
+
digests: Object.fromEntries(EDIT_FIELDS.map(field => [field, digest([field, values[field]])])),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Fixed schema makes altered/missing/duplicate metadata columns fail closed. */
|
|
56
|
+
export function editSessionHeader() {
|
|
57
|
+
return ["_session", "id", "_revision", "_item_digest", "status", ...EDIT_FIELDS.flatMap(field => [field, `_digest_${field}`])];
|
|
58
|
+
}
|
|
59
|
+
/** Escape formula triggers and literal apostrophes without changing baseline values. */
|
|
60
|
+
function toCell(value) {
|
|
61
|
+
return /^[=+\-@\t\r']/.test(value) ? `'${value}` : value;
|
|
62
|
+
}
|
|
63
|
+
/** Remove only a recognized display escape before comparison and persistence. */
|
|
64
|
+
function fromCell(cell) {
|
|
65
|
+
return cell.startsWith("'") && /^[=+\-@\t\r']/.test(cell.slice(1)) ? cell.slice(1) : cell;
|
|
66
|
+
}
|
|
67
|
+
/** Export explicit stable keys and digests without creating tracker metadata. */
|
|
68
|
+
export async function exportEditSession(pmRoot, ids) {
|
|
69
|
+
if (ids.length > 1000 || new Set(ids).size !== ids.length || ids.some(id => !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(id))) {
|
|
70
|
+
throw new EditRefusal("csv_session_invalid", "Export at most 1000 distinct canonical item IDs; split larger selections.");
|
|
71
|
+
}
|
|
72
|
+
const rows = [];
|
|
73
|
+
for (const id of ids)
|
|
74
|
+
rows.push(await snapshot(pmRoot, id));
|
|
75
|
+
const base = { version: 1, id: randomUUID(), rows };
|
|
76
|
+
const session = { ...base, digest: digest(base) };
|
|
77
|
+
return { session, table: [editSessionHeader(), ...rows.map(row => [
|
|
78
|
+
session.digest, row.id, row.revision, row.itemDigest, row.status,
|
|
79
|
+
...EDIT_FIELDS.flatMap(field => [toCell(row.values[field]), row.digests[field]]),
|
|
80
|
+
])] };
|
|
81
|
+
}
|
|
82
|
+
/** Validate an untrusted sidecar structurally as well as by its content digest. */
|
|
83
|
+
export function parseEditSession(input) {
|
|
84
|
+
try {
|
|
85
|
+
const session = input;
|
|
86
|
+
if (session.version !== 1 || typeof session.id !== "string" || !Array.isArray(session.rows) || session.rows.length > 1000
|
|
87
|
+
|| new Set(session.rows.map(row => row.id)).size !== session.rows.length
|
|
88
|
+
|| session.digest !== digest({ version: session.version, id: session.id, rows: session.rows }))
|
|
89
|
+
throw new Error();
|
|
90
|
+
for (const row of session.rows) {
|
|
91
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(row.id) || typeof row.status !== "string"
|
|
92
|
+
|| !/^[a-f0-9]{64}$/.test(row.revision) || !/^[a-f0-9]{64}$/.test(row.itemDigest)
|
|
93
|
+
|| EDIT_FIELDS.some(field => typeof row.values[field] !== "string" || row.digests[field] !== digest([field, row.values[field]])))
|
|
94
|
+
throw new Error();
|
|
95
|
+
}
|
|
96
|
+
return structuredClone(session);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new EditRefusal("csv_session_invalid", "Use the unchanged session JSON from csv checkout; export a fresh session if it was lost or edited.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Parse all edited rows before reading or writing tracker state. */
|
|
103
|
+
function plans(session, table) {
|
|
104
|
+
if (!Array.isArray(table) || table.some(row => !Array.isArray(row) || row.some(cell => typeof cell !== "string"))) {
|
|
105
|
+
throw new EditRefusal("csv_edit_invalid", "Provide a table of CSV string cells, including the checkout header.");
|
|
106
|
+
}
|
|
107
|
+
const header = table[0];
|
|
108
|
+
if (JSON.stringify(header) !== JSON.stringify(editSessionHeader())) {
|
|
109
|
+
throw new EditRefusal("csv_edit_invalid", "Keep the checkout column names and order, including all digest columns.");
|
|
110
|
+
}
|
|
111
|
+
const byId = new Map(session.rows.map(row => [row.id, row]));
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
return table.slice(1).map(row => {
|
|
114
|
+
const base = byId.get(row[1]);
|
|
115
|
+
if (row.length !== header.length || !base || seen.has(row[1]) || row[0] !== session.digest
|
|
116
|
+
|| row[2] !== base.revision || row[3] !== base.itemDigest
|
|
117
|
+
|| EDIT_FIELDS.some((field, index) => row[6 + index * 2] !== base.digests[field])) {
|
|
118
|
+
throw new EditRefusal("csv_edit_invalid", "Restore item/revision/field digests and unique IDs from the checkout; never paste rows from another session.");
|
|
119
|
+
}
|
|
120
|
+
seen.add(base.id);
|
|
121
|
+
if (row[4] !== base.status) {
|
|
122
|
+
throw new EditRefusal("csv_edit_invalid", "Status is read-only. Use pm close or pm reopen for lifecycle transitions, then export a fresh session.");
|
|
123
|
+
}
|
|
124
|
+
const desired = Object.fromEntries(EDIT_FIELDS.map((field, index) => [field, fromCell(row[5 + index * 2])]));
|
|
125
|
+
// Spreadsheet saves may convert embedded newlines in either direction.
|
|
126
|
+
for (const field of EDIT_FIELDS) {
|
|
127
|
+
if (desired[field].replace(/\r\n/g, "\n") === base.values[field].replace(/\r\n/g, "\n"))
|
|
128
|
+
desired[field] = base.values[field];
|
|
129
|
+
}
|
|
130
|
+
// The host canonical document contract trims trailing body whitespace.
|
|
131
|
+
desired.body = desired.body.trimEnd();
|
|
132
|
+
const fields = EDIT_FIELDS.filter(field => desired[field] !== base.values[field]);
|
|
133
|
+
if (!desired.title.trim() || !/^[0-4]$/.test(desired.priority)
|
|
134
|
+
|| fields.some(field => field !== "body" && desired[field] !== desired[field].trim())) {
|
|
135
|
+
throw new EditRefusal("csv_edit_invalid", "Provide a nonblank title, priority 0..4, and trim scalar cells; body follows host trailing-whitespace normalization.");
|
|
136
|
+
}
|
|
137
|
+
return { base, desired, fields };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Detect divergence only in edited fields; equal desired values are convergent. */
|
|
141
|
+
function conflicts(plan, current) {
|
|
142
|
+
return plan.fields.filter(field => current.values[field] !== plan.base.values[field] && current.values[field] !== plan.desired[field]);
|
|
143
|
+
}
|
|
144
|
+
/** Check and apply a field patch under the host item lease; write-time fencing remains a host prerequisite. */
|
|
145
|
+
async function applyPlan(pmRoot, plan, marker, author) {
|
|
146
|
+
const settings = await readSettings(pmRoot);
|
|
147
|
+
const result = await mutateItem({ pmRoot, settings, id: plan.base.id, op: "update", author,
|
|
148
|
+
message: `${marker}:apply`, skipNoop: true,
|
|
149
|
+
mutate: guardEditMutation(settings.locks.ttl_seconds, async (document) => {
|
|
150
|
+
const current = await documentRow(pmRoot, plan.base.id, document);
|
|
151
|
+
const divergent = conflicts(plan, current);
|
|
152
|
+
if (divergent.length)
|
|
153
|
+
throw new EditRefusal("csv_edit_conflict", "Re-export and reconcile the edited fields; no conflicting value was overwritten.", [{ id: plan.base.id, fields: divergent }]);
|
|
154
|
+
const changedFields = [];
|
|
155
|
+
for (const field of plan.fields) {
|
|
156
|
+
if (current.values[field] === plan.desired[field])
|
|
157
|
+
continue;
|
|
158
|
+
const value = plan.desired[field];
|
|
159
|
+
if (field === "body")
|
|
160
|
+
document.body = value;
|
|
161
|
+
else if (field === "priority")
|
|
162
|
+
document.metadata.priority = Number(value);
|
|
163
|
+
else if (field === "title")
|
|
164
|
+
document.metadata.title = value;
|
|
165
|
+
else if (value === "")
|
|
166
|
+
delete document.metadata[field];
|
|
167
|
+
else
|
|
168
|
+
document.metadata[field] = value;
|
|
169
|
+
changedFields.push(field);
|
|
170
|
+
}
|
|
171
|
+
return { changedFields };
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
return result.changedFields.length > 0;
|
|
175
|
+
}
|
|
176
|
+
/** Collect conflict keys across cause/aggregate graphs, visiting shared or cyclic errors once. */
|
|
177
|
+
function findConflicts(error) {
|
|
178
|
+
const pending = [error];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const found = [];
|
|
181
|
+
for (const current of pending) {
|
|
182
|
+
if (!(current instanceof Error) || seen.has(current))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(current);
|
|
185
|
+
if (current instanceof EditRefusal)
|
|
186
|
+
found.push(...current.conflicts);
|
|
187
|
+
pending.push(current.cause);
|
|
188
|
+
if (current instanceof AggregateError)
|
|
189
|
+
pending.push(...current.errors);
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Three-way patch using stable item keys, field-level conflict checks and the existing SDK
|
|
195
|
+
* transaction/CSV compensation. Preflight refusals create no transaction or
|
|
196
|
+
* item/history writes. Field checks run under each native item lease; protection
|
|
197
|
+
* after lease expiry needs host write fencing. Prior rows are compensated on
|
|
198
|
+
* ordinary failure, which explicitly requires reconciliation.
|
|
199
|
+
*/
|
|
200
|
+
export async function applyEditSession(pmRoot, input, table, options) {
|
|
201
|
+
const session = parseEditSession(input);
|
|
202
|
+
const planned = plans(session, table);
|
|
203
|
+
const transaction = `csv-edit-${digest([session.digest, table])}`;
|
|
204
|
+
const edited = planned.filter(plan => plan.fields.length > 0);
|
|
205
|
+
const found = [];
|
|
206
|
+
let concurrent = 0;
|
|
207
|
+
let pending = 0;
|
|
208
|
+
for (const plan of planned) {
|
|
209
|
+
const current = await snapshot(pmRoot, plan.base.id);
|
|
210
|
+
if (current.revision !== plan.base.revision || current.itemDigest !== plan.base.itemDigest)
|
|
211
|
+
concurrent++;
|
|
212
|
+
const divergent = conflicts(plan, current);
|
|
213
|
+
const editIndex = edited.indexOf(plan);
|
|
214
|
+
if (editIndex >= 0 && (await inspectUpdate(pmRoot, plan.base.id, `csv-txrow:${transaction}#${editIndex}`, false)).state === "applied") {
|
|
215
|
+
for (const field of plan.fields)
|
|
216
|
+
if (current.values[field] !== plan.desired[field] && !divergent.includes(field))
|
|
217
|
+
divergent.push(field);
|
|
218
|
+
}
|
|
219
|
+
if (divergent.length)
|
|
220
|
+
found.push({ id: plan.base.id, fields: divergent });
|
|
221
|
+
if (plan.fields.some(field => current.values[field] !== plan.desired[field]))
|
|
222
|
+
pending++;
|
|
223
|
+
}
|
|
224
|
+
if (found.length)
|
|
225
|
+
throw new EditRefusal("csv_edit_conflict", "Export a fresh session and reconcile the reported fields before applying; this preflight wrote zero items.", found);
|
|
226
|
+
const receipt = { rows: planned.length, changed: pending, unchanged: planned.length - pending, concurrent, applied: 0 };
|
|
227
|
+
if (options.dryRun || !pending)
|
|
228
|
+
return receipt;
|
|
229
|
+
// Bind the journal to the exact selected rows and edits, independent of file path.
|
|
230
|
+
let applied = 0;
|
|
231
|
+
const steps = edited.map((plan, index) => {
|
|
232
|
+
const marker = `csv-txrow:${transaction}#${index}`;
|
|
233
|
+
return {
|
|
234
|
+
id: `${index}-${plan.base.id}`,
|
|
235
|
+
inspect: () => inspectUpdate(pmRoot, plan.base.id, marker, false),
|
|
236
|
+
prepareCompensation: () => captureUpdateCompensation(pmRoot, plan.base.id, marker, options.author),
|
|
237
|
+
apply: async () => {
|
|
238
|
+
if (await applyPlan(pmRoot, plan, marker, options.author))
|
|
239
|
+
applied++;
|
|
240
|
+
return plan.base.id;
|
|
241
|
+
},
|
|
242
|
+
compensate: data => compensateUpdate(pmRoot, plan.base.id, marker, options.author, data),
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
try {
|
|
246
|
+
await commitWorkspaceTransaction({ pmRoot, transactionId: transaction, author: options.author, steps, onTransition: options.onTransition });
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const refusal = new EditRefusal("csv_edit_apply_failed", `Inspect and reconcile transaction ${transaction} before retrying. Item fields were checked before persistence; the host must keep its item lease valid through the write. Reverse compensation was attempted and may have refused a concurrent writer.`, findConflicts(error));
|
|
250
|
+
refusal.cause = error;
|
|
251
|
+
throw refusal;
|
|
252
|
+
}
|
|
253
|
+
return { ...receipt, applied, transaction };
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=roundtrip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roundtrip.js","sourceRoot":"","sources":["../roundtrip.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Public tracker validation
|
|
2
|
+
|
|
3
|
+
Source: public `unbraind/pm-csv`, main commit
|
|
4
|
+
`d5dbdb0a52e3e25ee79d95b98cf0b7585cd5602d`.
|
|
5
|
+
|
|
6
|
+
A Git archive of `.agents/pm` was extracted to a non-git scratch directory, then
|
|
7
|
+
all source files and directories were made read-only. The SDK certified all
|
|
8
|
+
120 items through `listAllComplete`; 24 titles/priorities were selected. Every
|
|
9
|
+
sixth input priority was changed to 9 to exercise deterministic rejection. The
|
|
10
|
+
separate destination was a disposable non-git tracker. Git was absent from
|
|
11
|
+
`PATH` during every import/retry.
|
|
12
|
+
|
|
13
|
+
| Measurement | Result |
|
|
14
|
+
| --- | ---: |
|
|
15
|
+
| Public items read with completeness proof | 120 |
|
|
16
|
+
| Selected input rows | 24 |
|
|
17
|
+
| Initial imported / rejected | 20 / 4 |
|
|
18
|
+
| Corrected rejected rows imported | 4 |
|
|
19
|
+
| Unchanged corrected rows replayed / newly imported | 4 / 0 |
|
|
20
|
+
| Original successful histories unchanged | 20 / 20 |
|
|
21
|
+
| Initial imported cell lineage records | 40 |
|
|
22
|
+
| Disallowed columns excluded | 1 / 1 |
|
|
23
|
+
| Exact-byte artifact checksums verified | 2 / 2 |
|
|
24
|
+
| Initial JSON receipt | 129 bytes |
|
|
25
|
+
| Import, correction, replay and integrity checks | 10,166 ms |
|
|
26
|
+
| Read-only source snapshot unchanged | yes |
|
|
27
|
+
|
|
28
|
+
Reproduce using a read-only public tracker copy:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
node scripts/validate-row-lineage.ts "$PUBLIC_TRACKER_COPY"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The script hashes the entire source before and after, checks its read-only
|
|
35
|
+
permissions, certifies complete SDK reads, creates a fresh non-git target,
|
|
36
|
+
validates the import and corrected retry, verifies successful history bytes,
|
|
37
|
+
checks file checksums and excluded-column sentinels, and prints aggregate JSON.
|
|
38
|
+
It removes only its disposable target. No public source item content is included
|
|
39
|
+
in this report. Timing measures this run and does not establish throughput.
|
|
40
|
+
|
|
41
|
+
Behavioral revert proofs (`node --test --test-name-pattern='cell lineage excludes' test/lineage.test.ts`):
|
|
42
|
+
|
|
43
|
+
- Reverting only `importLineage`'s rejected-row filter to include successful rows
|
|
44
|
+
failed the receipt/retry assertion (2 rejected instead of 1).
|
|
45
|
+
- Reverting only that function's allowed-column projection to copy raw rows
|
|
46
|
+
failed the excluded-column sentinel assertion.
|
|
47
|
+
|
|
48
|
+
Both controls were restored. The regular targeted suite passes with the
|
|
49
|
+
production implementation; CI supplies the full npm and Bun release evidence.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Row lineage and rejected-row retry
|
|
2
|
+
|
|
3
|
+
`pm csv bundle input.csv --columns title,priority --output first-bundle`
|
|
4
|
+
imports a bounded UTF-8 CSV and creates three private files:
|
|
5
|
+
|
|
6
|
+
- `manifest.json`: version 1, SHA-256 source and mapping revision, one logical
|
|
7
|
+
record number per row (header is record 1), one-based source column numbers,
|
|
8
|
+
permitted source headers and destination fields, source-cell and observed
|
|
9
|
+
persisted-value hashes, stable row keys, outcomes and item IDs.
|
|
10
|
+
- `rejected.csv`: only rejected rows, the explicitly allowed fields, and an
|
|
11
|
+
opaque `_csv_key` column. It never includes successful rows or other columns.
|
|
12
|
+
- `checksums.json`: SHA-256 of the exact bytes of the other two files.
|
|
13
|
+
|
|
14
|
+
Correct values in `rejected.csv`, keep its `_csv_key` cells unchanged, then run:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pm csv bundle first-bundle/rejected.csv --columns title,priority --output retry-bundle
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Successful original rows are absent from the retry input. Native SDK explicit
|
|
21
|
+
item IDs and an import fingerprint tag make unchanged retries idempotent,
|
|
22
|
+
including recovery after a process interruption before artifact export. Changed
|
|
23
|
+
values for a previously committed key refuse with `identity_conflict`; use
|
|
24
|
+
`pm update` for intentional changes. Do not delete provenance tags. Different
|
|
25
|
+
input files without retained retry keys represent separate imports. This command
|
|
26
|
+
creates items; use `csv apply` for edits to existing tracker items.
|
|
27
|
+
|
|
28
|
+
Example input (the `internal` column is excluded from both import and artifacts):
|
|
29
|
+
|
|
30
|
+
```csv
|
|
31
|
+
Summary,priority,internal
|
|
32
|
+
Accepted task,2,exclude this column
|
|
33
|
+
,9,exclude this column too
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pm csv bundle input.csv --map Summary=title --columns title,priority --output first-bundle
|
|
38
|
+
# receipt: rows=2 imported=1 replayed=0 rejected=1 manifest=<sha256>
|
|
39
|
+
# In rejected.csv, supply the title and change priority to an integer from 0 to 4.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The host renders the small receipt in its normal TOON format; `--json` returns
|
|
43
|
+
JSON. Large per-cell details stay in the manifest. A nonempty rejection count is
|
|
44
|
+
partial success, so automation must check `rejected` before continuing. Structural
|
|
45
|
+
and I/O refusals are typed `LineageRefusal` errors with `code`, `remedy`, exit code
|
|
46
|
+
2 and SDK recovery context. Per-row codes and remedies live in the manifest;
|
|
47
|
+
raw SDK errors and disallowed cell values are never copied into diagnostics.
|
|
48
|
+
|
|
49
|
+
Supported fields are `title`, `body`, `type`, `status`, `priority`, `tags`,
|
|
50
|
+
`assignee`, `sprint`, `release`. `--columns` is an explicit disclosure policy:
|
|
51
|
+
only those fields are imported or exported. Missing/ambiguous mappings and
|
|
52
|
+
unknown policy fields refuse before writes. Values are trimmed and native SDK
|
|
53
|
+
schema, lifecycle and governance rules apply. Terminal states needing a reason
|
|
54
|
+
or custom type requirements may be rejected; choose supported input values or
|
|
55
|
+
use the native lifecycle commands. Row width, missing titles and invalid
|
|
56
|
+
priorities reject the row. There are at most 1000 data rows per call.
|
|
57
|
+
|
|
58
|
+
Hash rules: `lineageDigest(value)` is SHA-256 of UTF-8 `JSON.stringify(value)`.
|
|
59
|
+
The source digest hashes the entire parsed table, including excluded columns,
|
|
60
|
+
without exporting their values or names. The mapping revision hashes
|
|
61
|
+
`[1, mapping]`, including ordered permitted headers/columns/fields. Each source
|
|
62
|
+
cell is hashed before trimming; each persisted value is hashed from the native
|
|
63
|
+
item document observed after import/replay (missing values use JSON null).
|
|
64
|
+
Hashes are integrity checks, not encryption or a signature. Preserve the
|
|
65
|
+
original manifest for the original observation if the tracker later changes.
|
|
66
|
+
The manifest digest hashes all fields except `digest`; `rejectedDigest` hashes
|
|
67
|
+
the parsed retry table. File checksums additionally detect byte-level changes.
|
|
68
|
+
Correcting retry cells intentionally changes checksums and creates a new manifest.
|
|
69
|
+
A retry manifest describes its own input coordinates and mapping revision; the
|
|
70
|
+
stable row key links back to the previous manifest.
|
|
71
|
+
|
|
72
|
+
SDK example:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { importLineage, parseCSV } from "pm-csv";
|
|
76
|
+
const bundle = await importLineage(pmRoot, parseCSV(csvText), {
|
|
77
|
+
columns: ["title", "priority"], fieldMap: { Summary: "title" }, author: "csv-agent",
|
|
78
|
+
});
|
|
79
|
+
console.log(bundle.receipt);
|
|
80
|
+
// Persist bundle.manifest and serializeCSV(bundle.rejected, ",") as private artifacts.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
No Git command is needed: native SDK create, schema validation, locks, history
|
|
84
|
+
and explicit IDs own tracker behavior. Keep the output directory private. The
|
|
85
|
+
CLI reserves a new directory before mutation, refuses existing directories, and
|
|
86
|
+
never overwrites artifacts. If final writes fail, retain partial files and rerun
|
|
87
|
+
unchanged input into a fresh directory; stable keys recover committed rows.
|