@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.
Files changed (95) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +13 -0
  3. package/dist/cli-bundle/bundle-manifest.json +152 -152
  4. package/dist/cli-bundle/chunks/{chunk-ZAVCS2GH.js → chunk-244MI4GS.js} +3 -3
  5. package/dist/cli-bundle/chunks/{chunk-NXBFO63A.js → chunk-2F3LUFMW.js} +2 -2
  6. package/dist/cli-bundle/chunks/{chunk-DJNJBWAQ.js → chunk-5I5RWIJC.js} +2 -2
  7. package/dist/cli-bundle/chunks/{chunk-X7WWQDYQ.js → chunk-65MHLHAA.js} +2 -2
  8. package/dist/cli-bundle/chunks/chunk-6C7GIMIL.js +13 -0
  9. package/dist/cli-bundle/chunks/{chunk-IW72J5FW.js → chunk-OWHNAR2B.js} +2 -2
  10. package/dist/cli-bundle/chunks/{chunk-TEQDR5KI.js → chunk-QKGMHGEI.js} +62 -62
  11. package/dist/cli-bundle/chunks/{chunk-L22NP5MA.js → chunk-QLUORNIB.js} +86 -86
  12. package/dist/cli-bundle/chunks/{chunk-AGUL3HUT.js → chunk-T2ENPRXF.js} +2 -2
  13. package/dist/cli-bundle/chunks/{chunk-I5Q7QSZX.js → chunk-TPQIBSL2.js} +2 -2
  14. package/dist/cli-bundle/chunks/chunk-YQMYF3YD.js +35 -0
  15. package/dist/cli-bundle/chunks/{register-list-query-37A4P6O4.js → register-list-query-XVN2ZLI7.js} +2 -2
  16. package/dist/cli-bundle/chunks/{register-mutation-TBJDHYIT.js → register-mutation-QCKAEGIJ.js} +3 -3
  17. package/dist/cli-bundle/chunks/{register-operations-WRZNEBUM.js → register-operations-SDEAXE7E.js} +2 -2
  18. package/dist/cli-bundle/chunks/{register-setup-H4PDLACD.js → register-setup-LXVBRCJ3.js} +2 -2
  19. package/dist/cli-bundle/focused-chunks/{chunk-CWCN5RLO.js → chunk-2UIWOP3O.js} +2 -2
  20. package/dist/cli-bundle/focused-chunks/chunk-42S3GGZ7.js +50 -0
  21. package/dist/cli-bundle/focused-chunks/{chunk-BFA6KKCI.js → chunk-7I23XGWO.js} +2 -2
  22. package/dist/cli-bundle/focused-chunks/{chunk-6Z24ZMU3.js → chunk-7YCDTCBC.js} +2 -2
  23. package/dist/cli-bundle/focused-chunks/{chunk-O4ZXNJMT.js → chunk-A644DUFQ.js} +2 -2
  24. package/dist/cli-bundle/focused-chunks/{chunk-4WP7LNE3.js → chunk-GQR3WH3F.js} +2 -2
  25. package/dist/cli-bundle/focused-chunks/{chunk-RASVBJX5.js → chunk-H5JZEIQV.js} +2 -2
  26. package/dist/cli-bundle/focused-chunks/chunk-IBZZZGK3.js +2 -0
  27. package/dist/cli-bundle/focused-chunks/chunk-LMKG3DFE.js +5 -0
  28. package/dist/cli-bundle/focused-chunks/{chunk-6MUTDXMP.js → chunk-N7W67YIG.js} +2 -2
  29. package/dist/cli-bundle/focused-chunks/{chunk-IMWTYKII.js → chunk-NOOZGIXP.js} +2 -2
  30. package/dist/cli-bundle/focused-chunks/{chunk-WZLSSGEU.js → chunk-OHIHZ7HS.js} +2 -2
  31. package/dist/cli-bundle/focused-chunks/chunk-ONYQCALA.js +19 -0
  32. package/dist/cli-bundle/focused-chunks/{chunk-QBP5FSBX.js → chunk-OVJL6NZE.js} +2 -2
  33. package/dist/cli-bundle/focused-chunks/chunk-RAFKLNZX.js +16 -0
  34. package/dist/cli-bundle/focused-chunks/{chunk-GJR6YEMR.js → chunk-VXWATRFL.js} +2 -2
  35. package/dist/cli-bundle/focused-chunks/{chunk-IZNRRCU3.js → chunk-XUQPEKRN.js} +2 -2
  36. package/dist/cli-bundle/focused-chunks/{chunk-MNPECIRL.js → chunk-YO3ZF3FI.js} +2 -2
  37. package/dist/cli-bundle/main.js +2 -2
  38. package/dist/cli-bundle/sdk-authoring.js +1 -1
  39. package/dist/cli-bundle/sdk-contracts.js +2 -2
  40. package/dist/cli-bundle/sdk-core.js +41 -41
  41. package/dist/cli-bundle/sdk-governance.js +1 -1
  42. package/dist/cli-bundle/sdk-graph.js +1 -1
  43. package/dist/cli-bundle/sdk-merge.js +32 -33
  44. package/dist/cli-bundle/sdk-query.js +1 -1
  45. package/dist/cli-bundle/sdk-runtime.js +1 -1
  46. package/dist/cli-bundle/sdk-testing.js +1 -1
  47. package/dist/cli-bundle/sdk.js +5 -5
  48. package/dist/core/diagnostics/remediation.js +17 -2
  49. package/dist/sdk/cli-contracts/runtime-contracts.d.ts +10 -0
  50. package/dist/sdk/cli-contracts/runtime-contracts.js +20 -3
  51. package/dist/sdk/generated/generated-error-code-catalog-part-1.js +49 -25
  52. package/dist/sdk/generated/generated-error-code-catalog-part-2.js +26 -2
  53. package/dist/sdk/governance/health.js +98 -11
  54. package/dist/sdk/governance/validate.js +15 -9
  55. package/dist/sdk/history-repair.d.ts +24 -0
  56. package/dist/sdk/history-repair.js +145 -4
  57. package/dist/sdk/merge/driver.js +25 -4
  58. package/dist/sdk/merge/receipt-file-boundary.d.ts +21 -0
  59. package/dist/sdk/merge/receipt-file-boundary.js +47 -0
  60. package/dist/sdk/merge/receipts.d.ts +24 -1
  61. package/dist/sdk/merge/receipts.js +342 -36
  62. package/dist/sdk/merge/reconcile.js +117 -47
  63. package/dist/sdk/read-output/continuation.d.ts +15 -2
  64. package/dist/sdk/read-output/continuation.js +86 -6
  65. package/dist/sdk/read-output-contracts.d.ts +4 -1
  66. package/dist/sdk/read-output-contracts.js +9 -6
  67. package/docs/MERGE_SAFETY.md +34 -5
  68. package/docs/OUTPUT_PROJECTION_CONTRACTS.md +13 -1
  69. package/docs/READ_OUTPUT_CONTRACTS.md +25 -2
  70. package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +7 -3
  71. package/marketplace.json +2 -2
  72. package/package.json +1 -1
  73. package/packages/pm-beads/package.json +1 -1
  74. package/packages/pm-calendar/package.json +1 -1
  75. package/packages/pm-command-kit/package.json +1 -1
  76. package/packages/pm-digital-twin/package.json +1 -1
  77. package/packages/pm-governance-audit/package.json +1 -1
  78. package/packages/pm-guide-shell/package.json +1 -1
  79. package/packages/pm-kanban/package.json +1 -1
  80. package/packages/pm-lifecycle-hooks/package.json +1 -1
  81. package/packages/pm-linked-test-adapters/package.json +1 -1
  82. package/packages/pm-search-advanced/package.json +1 -1
  83. package/packages/pm-templates/package.json +1 -1
  84. package/packages/pm-todos/package.json +1 -1
  85. package/packages/pm-vcs/package.json +1 -1
  86. package/plugins/pm-claude/.claude-plugin/plugin.json +1 -1
  87. package/plugins/pm-codex/.codex-plugin/plugin.json +1 -1
  88. package/sdk/public-surface.json +136 -15
  89. package/dist/cli-bundle/chunks/chunk-MKE6MD2K.js +0 -36
  90. package/dist/cli-bundle/chunks/chunk-YOLLRCLY.js +0 -13
  91. package/dist/cli-bundle/focused-chunks/chunk-FCTT4CZU.js +0 -5
  92. package/dist/cli-bundle/focused-chunks/chunk-Q63PSLO4.js +0 -2
  93. package/dist/cli-bundle/focused-chunks/chunk-QV5UP27O.js +0 -51
  94. package/dist/cli-bundle/focused-chunks/chunk-RPRLENVA.js +0 -16
  95. 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]="d201aa9f-1ed3-5655-8c06-e5729557b86a")}catch(e){}}();
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
- ...options.auditContext,
438
+ ...effectiveAuditContext,
305
439
  provenance_normalization: provenanceNormalization.receipt,
306
440
  }
307
- : options.auditContext,
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=d201aa9f-1ed3-5655-8c06-e5729557b86a
584
+ //# debugId=7a21ee76-ed53-57a0-9776-c787297ed7b6
@@ -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]="8d79e3a4-6104-5855-837f-69a041b87b27")}catch(e){}}();
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: itemFormatForPath(options.itemPath ?? outputPath, settings),
130
+ format: itemFormat,
127
131
  schema: settings?.schema,
128
- extensionFieldNames: collectRegisteredItemFieldNames(getActiveExtensionRegistrations()),
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=8d79e3a4-6104-5855-837f-69a041b87b27
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): Promise<void>;
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. */