@unbrained/pm-cli 2026.8.24 → 2026.8.25
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +13 -0
- package/dist/cli-bundle/bundle-manifest.json +152 -152
- package/dist/cli-bundle/chunks/{chunk-ZAVCS2GH.js → chunk-244MI4GS.js} +3 -3
- package/dist/cli-bundle/chunks/{chunk-NXBFO63A.js → chunk-2F3LUFMW.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-DJNJBWAQ.js → chunk-5I5RWIJC.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-X7WWQDYQ.js → chunk-65MHLHAA.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-6C7GIMIL.js +13 -0
- package/dist/cli-bundle/chunks/{chunk-IW72J5FW.js → chunk-OWHNAR2B.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-TEQDR5KI.js → chunk-QKGMHGEI.js} +62 -62
- package/dist/cli-bundle/chunks/{chunk-L22NP5MA.js → chunk-QLUORNIB.js} +86 -86
- package/dist/cli-bundle/chunks/{chunk-AGUL3HUT.js → chunk-T2ENPRXF.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-I5Q7QSZX.js → chunk-TPQIBSL2.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-YQMYF3YD.js +35 -0
- package/dist/cli-bundle/chunks/{register-list-query-37A4P6O4.js → register-list-query-XVN2ZLI7.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-TBJDHYIT.js → register-mutation-QCKAEGIJ.js} +3 -3
- package/dist/cli-bundle/chunks/{register-operations-WRZNEBUM.js → register-operations-SDEAXE7E.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-H4PDLACD.js → register-setup-LXVBRCJ3.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-CWCN5RLO.js → chunk-2UIWOP3O.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-42S3GGZ7.js +50 -0
- package/dist/cli-bundle/focused-chunks/{chunk-BFA6KKCI.js → chunk-7I23XGWO.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-6Z24ZMU3.js → chunk-7YCDTCBC.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-O4ZXNJMT.js → chunk-A644DUFQ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-4WP7LNE3.js → chunk-GQR3WH3F.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-RASVBJX5.js → chunk-H5JZEIQV.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-IBZZZGK3.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +5 -0
- package/dist/cli-bundle/focused-chunks/{chunk-6MUTDXMP.js → chunk-N7W67YIG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-IMWTYKII.js → chunk-NOOZGIXP.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-WZLSSGEU.js → chunk-OHIHZ7HS.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-ONYQCALA.js +19 -0
- package/dist/cli-bundle/focused-chunks/{chunk-QBP5FSBX.js → chunk-OVJL6NZE.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-RAFKLNZX.js +16 -0
- package/dist/cli-bundle/focused-chunks/{chunk-GJR6YEMR.js → chunk-VXWATRFL.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-IZNRRCU3.js → chunk-XUQPEKRN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-MNPECIRL.js → chunk-YO3ZF3FI.js} +2 -2
- package/dist/cli-bundle/main.js +2 -2
- package/dist/cli-bundle/sdk-authoring.js +1 -1
- package/dist/cli-bundle/sdk-contracts.js +2 -2
- package/dist/cli-bundle/sdk-core.js +41 -41
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +32 -33
- package/dist/cli-bundle/sdk-query.js +1 -1
- package/dist/cli-bundle/sdk-runtime.js +1 -1
- package/dist/cli-bundle/sdk-testing.js +1 -1
- package/dist/cli-bundle/sdk.js +5 -5
- package/dist/core/diagnostics/remediation.js +17 -2
- package/dist/sdk/cli-contracts/runtime-contracts.d.ts +10 -0
- package/dist/sdk/cli-contracts/runtime-contracts.js +20 -3
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +49 -25
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +26 -2
- package/dist/sdk/governance/health.js +98 -11
- package/dist/sdk/governance/validate.js +15 -9
- package/dist/sdk/history-repair.d.ts +24 -0
- package/dist/sdk/history-repair.js +145 -4
- package/dist/sdk/merge/driver.js +25 -4
- package/dist/sdk/merge/receipt-file-boundary.d.ts +21 -0
- package/dist/sdk/merge/receipt-file-boundary.js +47 -0
- package/dist/sdk/merge/receipts.d.ts +24 -1
- package/dist/sdk/merge/receipts.js +342 -36
- package/dist/sdk/merge/reconcile.js +117 -47
- package/dist/sdk/read-output/continuation.d.ts +15 -2
- package/dist/sdk/read-output/continuation.js +86 -6
- package/dist/sdk/read-output-contracts.d.ts +4 -1
- package/dist/sdk/read-output-contracts.js +9 -6
- package/docs/MERGE_SAFETY.md +34 -5
- package/docs/OUTPUT_PROJECTION_CONTRACTS.md +13 -1
- package/docs/READ_OUTPUT_CONTRACTS.md +25 -2
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +7 -3
- package/marketplace.json +2 -2
- package/package.json +1 -1
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/package.json +1 -1
- package/packages/pm-kanban/package.json +1 -1
- package/packages/pm-lifecycle-hooks/package.json +1 -1
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/package.json +1 -1
- package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
- package/sdk/public-surface.json +136 -15
- package/dist/cli-bundle/chunks/chunk-MKE6MD2K.js +0 -36
- package/dist/cli-bundle/chunks/chunk-YOLLRCLY.js +0 -13
- package/dist/cli-bundle/focused-chunks/chunk-FCTT4CZU.js +0 -5
- package/dist/cli-bundle/focused-chunks/chunk-Q63PSLO4.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-QV5UP27O.js +0 -51
- package/dist/cli-bundle/focused-chunks/chunk-RPRLENVA.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-TOFVXBWA.js +0 -19
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Implements the pm history repair command surface and its agent-facing runtime behavior.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7a21ee76-ed53-57a0-9776-c787297ed7b6")}catch(e){}}();
|
|
8
8
|
import { assertInitializedTracker } from "./environment/tracker-preflight.js";
|
|
9
|
+
import { realpath } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
9
11
|
import jsonPatch from "fast-json-patch";
|
|
10
12
|
import { patchPathToChangedField } from "../core/history/history-diff.js";
|
|
11
13
|
import { pathExists, readFileIfExists } from "../core/fs/fs-utils.js";
|
|
@@ -19,11 +21,13 @@ import { EXIT_CODE } from "../core/shared/constants.js";
|
|
|
19
21
|
import { PmCliError } from "../core/shared/errors.js";
|
|
20
22
|
import { resolveAuthor } from "../core/shared/author.js";
|
|
21
23
|
import { nowIso } from "../core/shared/time.js";
|
|
24
|
+
import { sha256Hex, stableStringify } from "../core/shared/serialization.js";
|
|
22
25
|
import { getActiveExtensionRegistrations, runActiveOnWriteHooks, } from "../core/extensions/index.js";
|
|
23
26
|
import { listAllItemMetadataWithBody, readLocatedItem, } from "../core/store/item-store.js";
|
|
24
27
|
import { resolvePmRoot } from "../core/store/paths.js";
|
|
25
28
|
import { readSettings } from "../core/store/settings.js";
|
|
26
29
|
import { resolveHistorySubject } from "./history-redact.js";
|
|
30
|
+
import { summarizeMergeReceipt, } from "./merge/receipts.js";
|
|
27
31
|
import { listInvalidProvenanceHistoryStreamIds, normalizeInvalidHistoryProvenance, } from "./governance/provenance-health.js";
|
|
28
32
|
function replayFieldValue(document, field) {
|
|
29
33
|
return field === "body"
|
|
@@ -135,6 +139,130 @@ export function analyzeReconciliationDiscard(entries, finalReplay, currentItemRe
|
|
|
135
139
|
: null,
|
|
136
140
|
};
|
|
137
141
|
}
|
|
142
|
+
async function canonicalRealPath(value) {
|
|
143
|
+
try {
|
|
144
|
+
return await realpath(value);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function verifyReceiptAgainstSnapshot(params) {
|
|
151
|
+
if (params.receipt.evidence_source !== "clone_local")
|
|
152
|
+
return null;
|
|
153
|
+
const receiptPath = await canonicalRealPath(path.resolve(params.gitWorkspaceRoot, params.receipt.item_path));
|
|
154
|
+
if (receiptPath !== params.currentPath)
|
|
155
|
+
return null;
|
|
156
|
+
const declaredFields = [
|
|
157
|
+
...new Set([
|
|
158
|
+
...params.receipt.fields_from_theirs,
|
|
159
|
+
...params.receipt.union_fields,
|
|
160
|
+
...params.receipt.decisions.map((decision) => decision.field),
|
|
161
|
+
]),
|
|
162
|
+
].sort((left, right) => left.localeCompare(right));
|
|
163
|
+
const hashes = params.receipt.merged_field_hashes;
|
|
164
|
+
if (!hashes || declaredFields.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
const hashFields = Object.keys(hashes).sort((left, right) => left.localeCompare(right));
|
|
167
|
+
if (declaredFields.length !== hashFields.length ||
|
|
168
|
+
declaredFields.some((field, index) => field !== hashFields[index])) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const currentMetadata = params.currentItemReplay.metadata;
|
|
172
|
+
const valuesMatch = declaredFields.every((field) => {
|
|
173
|
+
const value = field === "body" ? params.currentItemReplay.body : currentMetadata[field];
|
|
174
|
+
return hashes[field] === sha256Hex(stableStringify(value));
|
|
175
|
+
});
|
|
176
|
+
return valuesMatch ? { receipt: params.receipt, declaredFields } : null;
|
|
177
|
+
}
|
|
178
|
+
async function verifyMergeReceiptProof(params) {
|
|
179
|
+
if (params.proof.gitWorkspaceRoot === null) {
|
|
180
|
+
return {
|
|
181
|
+
trusted: false,
|
|
182
|
+
reason: "git_workspace_unavailable",
|
|
183
|
+
receipt_ids: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (params.itemPath === null || params.currentItemReplay === null) {
|
|
187
|
+
return {
|
|
188
|
+
trusted: false,
|
|
189
|
+
reason: "item_path_unavailable",
|
|
190
|
+
receipt_ids: [],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const itemReceipts = params.proof.receipts.filter((receipt) => receipt.item_id === params.id);
|
|
194
|
+
if (itemReceipts.length === 0) {
|
|
195
|
+
return { trusted: false, reason: "no_item_receipts", receipt_ids: [] };
|
|
196
|
+
}
|
|
197
|
+
const currentPath = await realpath(params.itemPath);
|
|
198
|
+
const trustedReceipts = [];
|
|
199
|
+
for (const receipt of itemReceipts) {
|
|
200
|
+
const trusted = await verifyReceiptAgainstSnapshot({
|
|
201
|
+
receipt,
|
|
202
|
+
gitWorkspaceRoot: params.proof.gitWorkspaceRoot,
|
|
203
|
+
currentPath,
|
|
204
|
+
currentItemReplay: params.currentItemReplay,
|
|
205
|
+
});
|
|
206
|
+
if (trusted)
|
|
207
|
+
trustedReceipts.push(trusted);
|
|
208
|
+
}
|
|
209
|
+
const trustedFields = new Set(trustedReceipts.flatMap(({ declaredFields }) => declaredFields));
|
|
210
|
+
const reconciliationFields = params.reconciliation?.reconciled_fields;
|
|
211
|
+
if (trustedReceipts.length > 0 &&
|
|
212
|
+
(reconciliationFields === undefined ||
|
|
213
|
+
(reconciliationFields.length > 0 &&
|
|
214
|
+
reconciliationFields.every((field) => trustedFields.has(field))))) {
|
|
215
|
+
return {
|
|
216
|
+
trusted: true,
|
|
217
|
+
reason: "trusted_clone_local_driver_evidence",
|
|
218
|
+
receipt_ids: trustedReceipts.map(({ receipt }) => receipt.id),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
trusted: false,
|
|
223
|
+
reason: "no_receipt_set_proves_snapshot",
|
|
224
|
+
receipt_ids: [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function resolveHistoryRepairMergeEvidence(params) {
|
|
228
|
+
const subjectMergeReceipts = params.options.mergeReceiptProof?.receipts.filter((receipt) => receipt.item_id === params.subjectId) ?? [];
|
|
229
|
+
const mergeReceiptProof = params.options.mergeReceiptProof
|
|
230
|
+
? await verifyMergeReceiptProof({
|
|
231
|
+
proof: {
|
|
232
|
+
...params.options.mergeReceiptProof,
|
|
233
|
+
receipts: subjectMergeReceipts,
|
|
234
|
+
},
|
|
235
|
+
id: params.subjectId,
|
|
236
|
+
itemPath: params.itemReplayContext.currentItemPath,
|
|
237
|
+
currentItemReplay: params.itemReplayContext.currentItemReplay,
|
|
238
|
+
reconciliation: params.reconciliation,
|
|
239
|
+
})
|
|
240
|
+
: undefined;
|
|
241
|
+
if (mergeReceiptProof &&
|
|
242
|
+
!mergeReceiptProof.trusted &&
|
|
243
|
+
params.options.dryRun !== true &&
|
|
244
|
+
params.options.force !== true) {
|
|
245
|
+
throw new PmCliError(`Merge receipt evidence for ${params.subjectId} does not prove the exact item snapshot (${mergeReceiptProof.reason}).`, EXIT_CODE.CONFLICT, {
|
|
246
|
+
code: "merge_reconcile_receipt_evidence_untrusted",
|
|
247
|
+
required: "Use clone-local driver evidence whose canonical path, declared fields, and merged-value hashes match the current item, or review the mismatch and rerun with --force.",
|
|
248
|
+
recovery: { suggested_retry: "pm merge reconcile --dry-run" },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const provenMergeReceipts = mergeReceiptProof?.trusted
|
|
252
|
+
? subjectMergeReceipts.filter((receipt) => mergeReceiptProof.receipt_ids.includes(receipt.id))
|
|
253
|
+
: [];
|
|
254
|
+
return {
|
|
255
|
+
mergeReceiptProof,
|
|
256
|
+
effectiveAuditContext: provenMergeReceipts.length > 0 && params.options.force !== true
|
|
257
|
+
? {
|
|
258
|
+
...params.options.auditContext,
|
|
259
|
+
merge: {
|
|
260
|
+
receipts: provenMergeReceipts.map(summarizeMergeReceipt),
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
: params.options.auditContext,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
138
266
|
async function loadHistoryRepairItemReplay(subject, settings, historyEntries, itemHashVersion) {
|
|
139
267
|
const currentItemPath = subject.located?.itemPath ?? null;
|
|
140
268
|
const loadedItem = subject.located
|
|
@@ -275,6 +403,12 @@ export async function runHistoryRepair(id, options, global) {
|
|
|
275
403
|
const reconciliation = reconcileNeeded && itemReplayContext.currentItemReplay
|
|
276
404
|
? analyzeReconciliationDiscard(reanchor.entries, finalReplay, itemReplayContext.currentItemReplay)
|
|
277
405
|
: undefined;
|
|
406
|
+
const { mergeReceiptProof, effectiveAuditContext } = await resolveHistoryRepairMergeEvidence({
|
|
407
|
+
options,
|
|
408
|
+
subjectId: subject.id,
|
|
409
|
+
itemReplayContext,
|
|
410
|
+
reconciliation,
|
|
411
|
+
});
|
|
278
412
|
const changed = [
|
|
279
413
|
reanchor.entriesRehashed > 0,
|
|
280
414
|
reanchor.entriesPatchRepaired > 0,
|
|
@@ -301,10 +435,10 @@ export async function runHistoryRepair(id, options, global) {
|
|
|
301
435
|
auditOperation: options.auditOperation ?? "history_repair",
|
|
302
436
|
auditContext: provenanceNormalization.receipt.changed
|
|
303
437
|
? {
|
|
304
|
-
...
|
|
438
|
+
...effectiveAuditContext,
|
|
305
439
|
provenance_normalization: provenanceNormalization.receipt,
|
|
306
440
|
}
|
|
307
|
-
:
|
|
441
|
+
: effectiveAuditContext,
|
|
308
442
|
itemHashVersion,
|
|
309
443
|
explicitItemHashVersion: reanchor.explicitItemHashVersion,
|
|
310
444
|
});
|
|
@@ -360,6 +494,7 @@ export async function runHistoryRepair(id, options, global) {
|
|
|
360
494
|
...provenanceNormalization.receipt,
|
|
361
495
|
},
|
|
362
496
|
...(reconciliation ? { reconciliation } : {}),
|
|
497
|
+
...(mergeReceiptProof ? { merge_receipt_proof: mergeReceiptProof } : {}),
|
|
363
498
|
warnings: [...new Set(warnings)].sort((left, right) => left.localeCompare(right)),
|
|
364
499
|
generated_at: nowIso(),
|
|
365
500
|
};
|
|
@@ -397,6 +532,9 @@ export async function runHistoryRepairAll(options, global) {
|
|
|
397
532
|
...options,
|
|
398
533
|
auditContext: options.auditContextById?.[driftedId],
|
|
399
534
|
auditContextById: undefined,
|
|
535
|
+
mergeReceiptProof: options.mergeReceiptProofById?.[driftedId] ??
|
|
536
|
+
options.mergeReceiptProof,
|
|
537
|
+
mergeReceiptProofById: undefined,
|
|
400
538
|
}, global);
|
|
401
539
|
/* c8 ignore next -- mixed repaired/clean outcomes depend on live drift composition. */
|
|
402
540
|
const outcome = result.changed ? "repaired" : "skipped_clean";
|
|
@@ -411,6 +549,9 @@ export async function runHistoryRepairAll(options, global) {
|
|
|
411
549
|
...(result.reconciliation
|
|
412
550
|
? { reconciliation: result.reconciliation }
|
|
413
551
|
: {}),
|
|
552
|
+
...(result.merge_receipt_proof
|
|
553
|
+
? { merge_receipt_proof: result.merge_receipt_proof }
|
|
554
|
+
: {}),
|
|
414
555
|
warnings: result.warnings,
|
|
415
556
|
});
|
|
416
557
|
}
|
|
@@ -440,4 +581,4 @@ export async function runHistoryRepairAll(options, global) {
|
|
|
440
581
|
};
|
|
441
582
|
}
|
|
442
583
|
//# sourceMappingURL=history-repair.js.map
|
|
443
|
-
//# debugId=
|
|
584
|
+
//# debugId=7a21ee76-ed53-57a0-9776-c787297ed7b6
|
package/dist/sdk/merge/driver.js
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
9
9
|
* merge never corrupts tracker storage the way raw conflict markers do.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
12
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2c8046a5-06aa-58f8-b347-e757086082d1")}catch(e){}}();
|
|
13
13
|
import { readFile, writeFile } from "node:fs/promises";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { getActiveExtensionRegistrations } from "../../core/extensions/index.js";
|
|
16
16
|
import { collectRegisteredItemFieldNames } from "../../core/extensions/item-fields.js";
|
|
17
17
|
import { pathExists } from "../../core/fs/fs-utils.js";
|
|
18
|
+
import { parseItemDocument } from "../../core/item/item-format.js";
|
|
18
19
|
import { EXIT_CODE } from "../../core/shared/constants.js";
|
|
19
20
|
import { PmCliError } from "../../core/shared/errors.js";
|
|
21
|
+
import { sha256Hex, stableStringify } from "../../core/shared/serialization.js";
|
|
20
22
|
import { nowIso } from "../../core/shared/time.js";
|
|
21
23
|
import { getSettingsPath, resolvePmRoot } from "../../core/store/paths.js";
|
|
22
24
|
import { readSettings } from "../../core/store/settings.js";
|
|
@@ -122,10 +124,12 @@ export async function runMergeDriver(options, global) {
|
|
|
122
124
|
}
|
|
123
125
|
else if (artifact === "item") {
|
|
124
126
|
const settings = await loadOptionalSettings(global);
|
|
127
|
+
const itemFormat = itemFormatForPath(options.itemPath ?? outputPath, settings);
|
|
128
|
+
const extensionFieldNames = collectRegisteredItemFieldNames(getActiveExtensionRegistrations());
|
|
125
129
|
const itemMerge = mergeItemDocuments(baseRaw, oursRaw, theirsRaw, {
|
|
126
|
-
format:
|
|
130
|
+
format: itemFormat,
|
|
127
131
|
schema: settings?.schema,
|
|
128
|
-
extensionFieldNames
|
|
132
|
+
extensionFieldNames,
|
|
129
133
|
preferred,
|
|
130
134
|
conflictResolution: "stable_value_order",
|
|
131
135
|
});
|
|
@@ -137,6 +141,19 @@ export async function runMergeDriver(options, global) {
|
|
|
137
141
|
conflict_resolution: "stable_value_order",
|
|
138
142
|
};
|
|
139
143
|
if (options.itemPath !== undefined) {
|
|
144
|
+
const mergedDocument = parseItemDocument(merged, {
|
|
145
|
+
format: itemFormat,
|
|
146
|
+
schema: settings?.schema,
|
|
147
|
+
extensionFieldNames,
|
|
148
|
+
});
|
|
149
|
+
const mergedMetadata = mergedDocument.metadata;
|
|
150
|
+
const receiptFields = [
|
|
151
|
+
...new Set([
|
|
152
|
+
...itemMerge.fields_from_theirs,
|
|
153
|
+
...itemMerge.union_fields,
|
|
154
|
+
...itemMerge.conflict_decisions.map((decision) => decision.field),
|
|
155
|
+
]),
|
|
156
|
+
];
|
|
140
157
|
receiptInput = {
|
|
141
158
|
cwd: process.cwd(),
|
|
142
159
|
itemPath: options.itemPath,
|
|
@@ -144,6 +161,10 @@ export async function runMergeDriver(options, global) {
|
|
|
144
161
|
conflictResolution: "stable_value_order",
|
|
145
162
|
fieldsFromTheirs: itemMerge.fields_from_theirs,
|
|
146
163
|
unionFields: itemMerge.union_fields,
|
|
164
|
+
mergedFieldHashes: Object.fromEntries(receiptFields.map((field) => [
|
|
165
|
+
field,
|
|
166
|
+
sha256Hex(stableStringify(field === "body" ? mergedDocument.body : mergedMetadata[field])),
|
|
167
|
+
])),
|
|
147
168
|
decisions: itemMerge.conflict_decisions,
|
|
148
169
|
};
|
|
149
170
|
}
|
|
@@ -187,4 +208,4 @@ export async function runMergeDriver(options, global) {
|
|
|
187
208
|
};
|
|
188
209
|
}
|
|
189
210
|
//# sourceMappingURL=driver.js.map
|
|
190
|
-
//# debugId=
|
|
211
|
+
//# debugId=2c8046a5-06aa-58f8-b347-e757086082d1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/merge/receipt-file-boundary
|
|
3
|
+
*
|
|
4
|
+
* Reads untrusted receipt candidates through a bounded, no-follow file
|
|
5
|
+
* boundary whose race and short-read behavior can be exercised directly.
|
|
6
|
+
*/
|
|
7
|
+
import { type Stats } from "node:fs";
|
|
8
|
+
import { type FileHandle } from "node:fs/promises";
|
|
9
|
+
/** Minimal filesystem operations required by the receipt reader. */
|
|
10
|
+
export interface ReceiptFileBoundary {
|
|
11
|
+
/** Inspect the path without following its final symbolic link. */
|
|
12
|
+
lstat(receiptPath: string): Promise<Stats>;
|
|
13
|
+
/** Open the candidate and return the handle used for verified reads. */
|
|
14
|
+
open(receiptPath: string, flags: number): Promise<FileHandle>;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve the platform no-follow flag, falling back only where Node omits it. */
|
|
17
|
+
export declare function resolveReceiptNoFollowFlag(constants: {
|
|
18
|
+
O_NOFOLLOW?: number;
|
|
19
|
+
}): number;
|
|
20
|
+
/** Read one regular file only when its opened identity remains bounded and stable. */
|
|
21
|
+
export declare function readBoundedRegularFile(receiptPath: string, maximumBytes: number, boundary?: ReceiptFileBoundary): Promise<string | null>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sdk/merge/receipt-file-boundary
|
|
3
|
+
*
|
|
4
|
+
* Reads untrusted receipt candidates through a bounded, no-follow file
|
|
5
|
+
* boundary whose race and short-read behavior can be exercised directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6ece0e15-aff5-5e2d-b18a-16ffaa0fe2ea")}catch(e){}}();
|
|
9
|
+
import { constants as fsConstants } from "node:fs";
|
|
10
|
+
import { lstat, open } from "node:fs/promises";
|
|
11
|
+
const NODE_RECEIPT_FILE_BOUNDARY = { lstat, open };
|
|
12
|
+
/** Resolve the platform no-follow flag, falling back only where Node omits it. */
|
|
13
|
+
export function resolveReceiptNoFollowFlag(constants) {
|
|
14
|
+
return constants.O_NOFOLLOW ?? 0;
|
|
15
|
+
}
|
|
16
|
+
/** Read one regular file only when its opened identity remains bounded and stable. */
|
|
17
|
+
export async function readBoundedRegularFile(receiptPath, maximumBytes, boundary = NODE_RECEIPT_FILE_BOUNDARY) {
|
|
18
|
+
const pathStats = await boundary.lstat(receiptPath);
|
|
19
|
+
if (pathStats.isSymbolicLink() ||
|
|
20
|
+
!pathStats.isFile() ||
|
|
21
|
+
pathStats.size > maximumBytes) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const handle = await boundary.open(receiptPath, fsConstants.O_RDONLY | resolveReceiptNoFollowFlag(fsConstants));
|
|
25
|
+
try {
|
|
26
|
+
const openedStats = await handle.stat();
|
|
27
|
+
if (!openedStats.isFile() || openedStats.size > maximumBytes)
|
|
28
|
+
return null;
|
|
29
|
+
const buffer = Buffer.alloc(openedStats.size);
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset < buffer.length) {
|
|
32
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
33
|
+
if (bytesRead === 0)
|
|
34
|
+
break;
|
|
35
|
+
offset += bytesRead;
|
|
36
|
+
}
|
|
37
|
+
const finalStats = await handle.stat();
|
|
38
|
+
return finalStats.size === openedStats.size && offset === openedStats.size
|
|
39
|
+
? buffer.toString("utf8", 0, offset)
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await handle.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=receipt-file-boundary.js.map
|
|
47
|
+
//# debugId=6ece0e15-aff5-5e2d-b18a-16ffaa0fe2ea
|
|
@@ -19,6 +19,8 @@ export interface MergeDecisionReceipt {
|
|
|
19
19
|
fields_from_theirs: string[];
|
|
20
20
|
/** Collections combined from both branches. */
|
|
21
21
|
union_fields: string[];
|
|
22
|
+
/** SHA-256 hashes of the merged values for every field represented by this receipt. Absent on legacy receipts, which cannot prove drift attribution. */
|
|
23
|
+
merged_field_hashes?: Record<string, string>;
|
|
22
24
|
/** Full recoverable scalar decisions, retained only in the clone. */
|
|
23
25
|
decisions: ItemMergeConflictDecision[];
|
|
24
26
|
/** Whether a merge reconciliation history event consumed this receipt. */
|
|
@@ -29,6 +31,8 @@ export interface MergeDecisionReceipt {
|
|
|
29
31
|
reconciled_at?: string;
|
|
30
32
|
/** Whether decision values are recoverable locally or represented by hashes only. */
|
|
31
33
|
value_availability?: "clone_local" | "hash_only";
|
|
34
|
+
/** Runtime-only provenance assigned by the reader; serialized values are ignored. */
|
|
35
|
+
evidence_source?: "clone_local" | "durable";
|
|
32
36
|
}
|
|
33
37
|
/** Privacy-safe receipt summary suitable for committed history context. */
|
|
34
38
|
export interface MergeDecisionReceiptSummary {
|
|
@@ -66,6 +70,13 @@ export interface MergeReceiptReport {
|
|
|
66
70
|
/** ISO timestamp for the report. */
|
|
67
71
|
generated_at: string;
|
|
68
72
|
}
|
|
73
|
+
/** Internal integrity scan result used by health without exposing malformed receipt contents. */
|
|
74
|
+
export interface MergeReceiptEvidenceScan {
|
|
75
|
+
/** Valid receipts that passed bounded-file, schema, and identity validation. */
|
|
76
|
+
receipts: MergeDecisionReceipt[];
|
|
77
|
+
/** Number of JSON receipt candidates that could not be validated safely. */
|
|
78
|
+
invalid_evidence_count: number;
|
|
79
|
+
}
|
|
69
80
|
/** Convert a raw receipt into privacy-safe history context. */
|
|
70
81
|
export declare function summarizeMergeReceipt(receipt: MergeDecisionReceipt): MergeDecisionReceiptSummary;
|
|
71
82
|
/** Persist one item-driver outcome in the clone-local Git directory. */
|
|
@@ -76,8 +87,18 @@ export declare function writeMergeReceipt(params: {
|
|
|
76
87
|
conflictResolution?: "preferred_side" | "stable_value_order";
|
|
77
88
|
fieldsFromTheirs: string[];
|
|
78
89
|
unionFields: string[];
|
|
90
|
+
mergedFieldHashes?: Record<string, string>;
|
|
79
91
|
decisions: ItemMergeConflictDecision[];
|
|
80
92
|
}): Promise<MergeDecisionReceipt | null>;
|
|
93
|
+
/** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
|
|
94
|
+
export declare function inspectMergeReceiptEvidence(cwd: string, options?: {
|
|
95
|
+
/** Include receipts whose reconciliation state is already settled. */
|
|
96
|
+
includeReconciled?: boolean;
|
|
97
|
+
/** Include receipts that contain no discarded scalar values. */
|
|
98
|
+
includeLossless?: boolean;
|
|
99
|
+
/** Explicit tracker root used to locate durable receipt evidence. */
|
|
100
|
+
pmRoot?: string;
|
|
101
|
+
}): Promise<MergeReceiptEvidenceScan>;
|
|
81
102
|
/** Read clone-local receipts with explicit reconciled/lossless classification controls. */
|
|
82
103
|
export declare function listMergeReceipts(cwd: string, options?: {
|
|
83
104
|
includeReconciled?: boolean;
|
|
@@ -92,7 +113,9 @@ export declare function partitionMergeReceipts(receipts: MergeDecisionReceipt[])
|
|
|
92
113
|
lossless: MergeDecisionReceipt[];
|
|
93
114
|
};
|
|
94
115
|
/** Mark a receipt as represented by a committed merge history event. */
|
|
95
|
-
export declare function markMergeReceiptReconciled(cwd: string, receipt: MergeDecisionReceipt
|
|
116
|
+
export declare function markMergeReceiptReconciled(cwd: string, receipt: MergeDecisionReceipt, options?: {
|
|
117
|
+
requireExisting?: boolean;
|
|
118
|
+
}): Promise<void>;
|
|
96
119
|
/** Report pending or historical clone-local merge decisions. */
|
|
97
120
|
export declare function runMergeReceiptReport(options: {
|
|
98
121
|
/** Include receipts already represented by merge history events. */
|