@unbrained/pm-cli 2026.9.3 → 2026.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +27 -0
- package/dist/cli-bundle/bundle-manifest.json +146 -146
- package/dist/cli-bundle/chunks/{chunk-E5RBBCXO.js → chunk-6XSKPNCF.js} +75 -75
- package/dist/cli-bundle/chunks/chunk-A24WAVE6.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-DKGP3QLE.js → chunk-HI6BHPCU.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-Y4PJ6HPK.js → chunk-I2AS367G.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-IEHHE6GA.js → chunk-OGBVU67G.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-EDPIS7EB.js → chunk-RI4XNOYS.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-TXW6VL33.js +38 -0
- package/dist/cli-bundle/chunks/{chunk-LENC656S.js → chunk-V72UV7QS.js} +76 -79
- package/dist/cli-bundle/chunks/chunk-VGLHECAP.js +9 -0
- package/dist/cli-bundle/chunks/{chunk-34HASX3S.js → chunk-X6CMGV6L.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-UPPM2FOZ.js → chunk-YW4QK2G7.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-W2ZTOMKP.js → register-list-query-Z5YYSJJG.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-RVND5NCG.js → register-mutation-ELSP6I4I.js} +2 -2
- package/dist/cli-bundle/chunks/{register-operations-TTROIVC4.js → register-operations-OH7R3ZT4.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-D3ZFHY3L.js → register-setup-E2AE72QG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-Q45NHPF2.js → chunk-25UJBH4M.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-4HJFY4MB.js → chunk-3646BZ4W.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-TXE4TSHM.js → chunk-4YRAVI43.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-M5M6SVHS.js → chunk-53IHPICX.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-5P4HVOWG.js +176 -0
- package/dist/cli-bundle/focused-chunks/chunk-636VVQ7F.js +54 -0
- package/dist/cli-bundle/focused-chunks/{chunk-D7KXFBXV.js → chunk-6PWPQKDN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-S27XMBIN.js → chunk-7N2TV7WZ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-5RFXUPN5.js → chunk-AJR3OP2E.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-KZ2NB3ET.js → chunk-B3A6MJYE.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-BWKHEIVF.js → chunk-BFDDJ2T2.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-4WR46RMG.js → chunk-BHUIRZWQ.js} +8 -8
- package/dist/cli-bundle/focused-chunks/{chunk-4RRHC3S4.js → chunk-JADHHPY6.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-GT7MXDLW.js → chunk-MHKUIPJW.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-NBMJD6MK.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-NSKBLHVI.js +20 -0
- package/dist/cli-bundle/focused-chunks/{chunk-OL5EH6I3.js → chunk-P2GOAOQN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-UCLO3XDU.js → chunk-YARWJM2B.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 +1 -1
- package/dist/cli-bundle/sdk-core.js +28 -31
- 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 +42 -37
- 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 +10 -10
- package/dist/core/diagnostics/remediation.js +4 -4
- package/dist/core/history/history.d.ts +37 -1
- package/dist/core/history/history.js +278 -58
- package/dist/core/history/replay.js +68 -34
- package/dist/core/lock/lock.d.ts +10 -0
- package/dist/core/lock/lock.js +42 -3
- package/dist/core/shared/author.d.ts +3 -1
- package/dist/core/shared/author.js +111 -23
- package/dist/sdk/environment/agent-environment.d.ts +296 -0
- package/dist/sdk/environment/agent-environment.js +467 -0
- package/dist/sdk/environment/project-runtime-compatibility.d.ts +19 -1
- package/dist/sdk/environment/project-runtime-compatibility.js +118 -21
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +14 -2
- package/dist/sdk/generated/generated-error-code-catalog-part-2.js +14 -14
- package/dist/sdk/governance/health.js +13 -44
- package/dist/sdk/governance/merge-receipt-history.d.ts +62 -0
- package/dist/sdk/governance/merge-receipt-history.js +207 -0
- package/dist/sdk/governance/provenance-health.js +9 -3
- package/dist/sdk/history-compact.js +14 -5
- package/dist/sdk/history-redact.js +58 -23
- package/dist/sdk/history-repair.js +16 -6
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +4 -2
- package/dist/sdk/merge/driver.d.ts +3 -1
- package/dist/sdk/merge/driver.js +9 -9
- package/dist/sdk/merge/index.d.ts +1 -1
- package/dist/sdk/merge/index.js +3 -3
- package/dist/sdk/merge/receipts.d.ts +17 -6
- package/dist/sdk/merge/receipts.js +246 -75
- package/dist/sdk/merge/reconcile.d.ts +9 -7
- package/dist/sdk/merge/reconcile.js +175 -12
- package/dist/sdk/merge/three-way.d.ts +22 -2
- package/dist/sdk/merge/three-way.js +92 -25
- package/dist/sdk/runtime-primitives.d.ts +2 -2
- package/dist/sdk/runtime-primitives.js +4 -4
- package/dist/types.d.ts +25 -0
- package/dist/types.js +2 -2
- package/docs/AGENT_ENVIRONMENTS.md +47 -0
- package/docs/AGENT_PROVENANCE_ADR.md +17 -2
- package/docs/CONTEXT_INTEGRITY_CONTRACTS.md +38 -1
- package/docs/MERGE_SAFETY.md +119 -23
- package/docs/README.md +1 -0
- package/docs/SDK_CONTEXT_EVIDENCE_CONTRACTS.md +24 -2
- package/docs/SDK_RUNTIME_BOUNDARIES.md +17 -1
- package/docs/examples/sdk-agent-environment/README.md +14 -0
- package/docs/examples/sdk-agent-environment/package.json +8 -0
- package/docs/examples/sdk-agent-environment/run-agent-environment.mjs +154 -0
- package/docs/generated/REFUSAL_CLOSURE_CENSUS.md +4 -3
- package/marketplace.json +2 -2
- package/package.json +2 -2
- 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 +534 -24
- package/dist/cli-bundle/chunks/chunk-AY324UJ5.js +0 -9
- package/dist/cli-bundle/chunks/chunk-L7B7343B.js +0 -8
- package/dist/cli-bundle/chunks/chunk-LISESA7W.js +0 -35
- package/dist/cli-bundle/focused-chunks/chunk-3ZBI35HU.js +0 -176
- package/dist/cli-bundle/focused-chunks/chunk-GHCZ3YFR.js +0 -20
- package/dist/cli-bundle/focused-chunks/chunk-KBLZRU62.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-QKHCSUGJ.js +0 -51
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* merge-configuration installer. The CLI `pm merge` command is a shim over
|
|
7
7
|
* this module so packages and hosts can reuse the same merge behavior.
|
|
8
8
|
*/
|
|
9
|
-
export { ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, type HistoryMergeResult, type HistoryMergeStrategy, type ItemDocumentMergeResult, type JsonDocumentMergeResult, type ItemMergeConflictDecision, type MergePreferredSide, type RelationshipStreamMergeResult, } from "./three-way.js";
|
|
9
|
+
export { ITEM_SCALAR_MISSING_VALUE, ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, encodeItemScalarDecisionValue, hashItemScalarDecisionValue, isItemScalarMissingValue, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, type HistoryMergeResult, type HistoryMergeStrategy, type ItemDocumentMergeResult, type JsonDocumentMergeResult, type ItemMergeConflictDecision, type ItemScalarConflictResolution, type ItemScalarResolutionBasis, type MergePreferredSide, type RelationshipStreamMergeResult, } from "./three-way.js";
|
|
10
10
|
export { inspectMergeReceiptEvidence, listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptEvidenceReport, runMergeReceiptReport, summarizeMergeReceipt, type MergeDecisionReceipt, type MergeDecisionReceiptSummary, type MergeReceiptEvidenceReport, type MergeReceiptEvidenceScan, type MergeReceiptInvalidEvidence, type MergeReceiptReport, } from "./receipts.js";
|
|
11
11
|
export { MERGE_DRIVER_ARTIFACT_VALUES, runMergeDriver, type MergeDriverArtifact, type MergeDriverOptions, type MergeDriverResult, } from "./driver.js";
|
|
12
12
|
export { auditMergeAttributeFence, auditMergeDriverConfiguration, buildMergeAttributePatterns, findGitWorkspaceRoot, PM_GITATTRIBUTES_END, PM_GITATTRIBUTES_START, PM_GITATTRIBUTES_V2_END, PM_GITATTRIBUTES_V2_START, refreshMergeAttributeFenceIfInstalled, resolveMergeInstallContext, runMergeInstall, type MergeFenceAuditResult, type MergeDriverConfigurationAuditResult, type MergeFenceRefreshOutcome, type MergeInstallOptions, type MergeInstallContextResolution, type MergeInstallResult, } from "./install.js";
|
package/dist/sdk/merge/index.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* this module so packages and hosts can reuse the same merge behavior.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
!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]="
|
|
11
|
-
export { ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, } from "./three-way.js";
|
|
10
|
+
!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]="925be497-bdd3-527b-9a88-cf9c9f682ab2")}catch(e){}}();
|
|
11
|
+
export { ITEM_SCALAR_MISSING_VALUE, ITEM_LATEST_TIMESTAMP_FIELDS, ITEM_UNION_COLLECTION_FIELDS, encodeItemScalarDecisionValue, hashItemScalarDecisionValue, isItemScalarMissingValue, mergeHistoryStreams, mergeItemDocuments, mergeJsonDocuments, mergeRelationshipEventStreams, } from "./three-way.js";
|
|
12
12
|
export { inspectMergeReceiptEvidence, listMergeReceipts, markMergeReceiptReconciled, runMergeReceiptEvidenceReport, runMergeReceiptReport, summarizeMergeReceipt, } from "./receipts.js";
|
|
13
13
|
export { MERGE_DRIVER_ARTIFACT_VALUES, runMergeDriver, } from "./driver.js";
|
|
14
14
|
export { auditMergeAttributeFence, auditMergeDriverConfiguration, buildMergeAttributePatterns, findGitWorkspaceRoot, PM_GITATTRIBUTES_END, PM_GITATTRIBUTES_START, PM_GITATTRIBUTES_V2_END, PM_GITATTRIBUTES_V2_START, refreshMergeAttributeFenceIfInstalled, resolveMergeInstallContext, runMergeInstall, } from "./install.js";
|
|
15
15
|
export { runMergeReconcile, } from "./reconcile.js";
|
|
16
16
|
//# sourceMappingURL=index.js.map
|
|
17
|
-
//# debugId=
|
|
17
|
+
//# debugId=925be497-bdd3-527b-9a88-cf9c9f682ab2
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
|
-
import type
|
|
2
|
+
import { type ItemMergeConflictDecision, type ItemScalarConflictResolution, type MergePreferredSide } from "./three-way.js";
|
|
3
3
|
/** Return whether a receipt id is safe to expose in cleartext diagnostics. */
|
|
4
4
|
export declare function isSafeReceiptId(value: string): boolean;
|
|
5
|
+
declare const DURABLE_VALUE_POLICY: "bounded_non_sensitive_scalars_v1";
|
|
5
6
|
/** One clone-local merge receipt with recoverable branch values. */
|
|
6
7
|
export interface MergeDecisionReceipt {
|
|
7
8
|
/** Receipt schema version. */
|
|
@@ -14,10 +15,12 @@ export interface MergeDecisionReceipt {
|
|
|
14
15
|
item_id: string;
|
|
15
16
|
/** Side requested by the caller; stable-value decisions can retain either side. */
|
|
16
17
|
requested_preference?: MergePreferredSide;
|
|
18
|
+
/** Whether requested preference participates in item scalar selection. */
|
|
19
|
+
requested_preference_applied?: boolean;
|
|
17
20
|
/** Legacy schema-v1 key accepted while reading older clone-local receipts. */
|
|
18
21
|
preferred?: MergePreferredSide;
|
|
19
22
|
/** Scalar-conflict selection contract used by the item driver. */
|
|
20
|
-
conflict_resolution:
|
|
23
|
+
conflict_resolution: ItemScalarConflictResolution;
|
|
21
24
|
/** Fields selected cleanly from the other branch. */
|
|
22
25
|
fields_from_theirs: string[];
|
|
23
26
|
/** Collections combined from both branches. */
|
|
@@ -33,7 +36,9 @@ export interface MergeDecisionReceipt {
|
|
|
33
36
|
/** Reconciliation timestamp, when consumed. */
|
|
34
37
|
reconciled_at?: string;
|
|
35
38
|
/** Whether decision values are recoverable locally or represented by hashes only. */
|
|
36
|
-
value_availability?: "clone_local" | "hash_only";
|
|
39
|
+
value_availability?: "clone_local" | "hash_only" | "bounded_inline" | "mixed";
|
|
40
|
+
/** Versioned rule governing which durable scalar previews may be committed. */
|
|
41
|
+
value_policy?: typeof DURABLE_VALUE_POLICY;
|
|
37
42
|
/** Runtime-only provenance assigned by the reader; serialized values are ignored. */
|
|
38
43
|
evidence_source?: "clone_local" | "durable";
|
|
39
44
|
}
|
|
@@ -53,8 +58,10 @@ export interface MergeDecisionReceiptSummary {
|
|
|
53
58
|
union_fields: string[];
|
|
54
59
|
/** Side requested by the caller; decision hashes prove the actual retained values. */
|
|
55
60
|
requested_preference: MergePreferredSide;
|
|
61
|
+
/** Whether requested preference participated in item scalar selection. */
|
|
62
|
+
requested_preference_applied: boolean;
|
|
56
63
|
/** Scalar-conflict selection contract used by the item driver. */
|
|
57
|
-
conflict_resolution:
|
|
64
|
+
conflict_resolution: ItemScalarConflictResolution;
|
|
58
65
|
/** Hashes proving the retained and discarded values without publishing them. */
|
|
59
66
|
decisions: Array<{
|
|
60
67
|
field: string;
|
|
@@ -112,12 +119,15 @@ export interface MergeReceiptInvalidEvidence {
|
|
|
112
119
|
/** Evidence store that produced the rejected candidate. */
|
|
113
120
|
evidence_source: "clone_local" | "durable" | "clone_local_and_durable";
|
|
114
121
|
/** Stable failure class suitable for remediation routing and graph analytics. */
|
|
115
|
-
reason: "directory_unreadable" | "candidate_not_bounded_regular_file" | "candidate_unreadable" | "candidate_invalid_json" | "schema_or_identity_invalid" | "copy_provenance_mismatch";
|
|
122
|
+
reason: "directory_unreadable" | "candidate_not_bounded_regular_file" | "candidate_unreadable" | "candidate_invalid_json" | "schema_or_identity_invalid" | "schema_invalid" | "identity_invalid" | "copy_provenance_mismatch";
|
|
123
|
+
/** Exact validation boundary when schema or identity evidence is rejected. */
|
|
124
|
+
validation_error?: "required_fields" | "receipt_id" | "item_id" | "item_path" | "filename" | "collections" | "enums" | "timestamps" | "durable_decisions";
|
|
116
125
|
/** Receipt identity when the bounded filename itself is a valid receipt id. */
|
|
117
126
|
receipt_id?: string;
|
|
118
127
|
/** SHA-256 locator for an unsafe or malformed candidate filename. */
|
|
119
128
|
candidate_name_hash?: string;
|
|
120
129
|
}
|
|
130
|
+
/** Revalidate one receipt and stage its atomic reconciled-state replacement. */
|
|
121
131
|
declare function prepareReceiptSettlement(params: {
|
|
122
132
|
receiptPath: string;
|
|
123
133
|
receiptId: string;
|
|
@@ -136,12 +146,13 @@ export declare function writeMergeReceipt(params: {
|
|
|
136
146
|
cwd: string;
|
|
137
147
|
itemPath: string;
|
|
138
148
|
preferred: MergePreferredSide;
|
|
139
|
-
conflictResolution?:
|
|
149
|
+
conflictResolution?: ItemScalarConflictResolution;
|
|
140
150
|
fieldsFromTheirs: string[];
|
|
141
151
|
unionFields: string[];
|
|
142
152
|
mergedFieldHashes?: Record<string, string>;
|
|
143
153
|
decisions: ItemMergeConflictDecision[];
|
|
144
154
|
}): Promise<MergeDecisionReceipt | null>;
|
|
155
|
+
/** Distinguish a missing optional receipt store from an unreadable one. */
|
|
145
156
|
declare function receiptDirectoryFailureMeansAbsent(directory: string, error: unknown, inspectAncestor?: (ancestor: string) => Promise<Pick<Stats, "isDirectory">>): Promise<boolean>;
|
|
146
157
|
/** Inspect valid receipts and count invalid evidence without returning untrusted file contents. */
|
|
147
158
|
export declare function inspectMergeReceiptEvidence(cwd: string, options?: {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* value for the coordinator that performed the merge.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
!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]="
|
|
10
|
+
!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]="dfdd6f10-e11f-59e8-a16f-507dc40d5b31")}catch(e){}}();
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { readdir, stat } from "node:fs/promises";
|
|
@@ -16,6 +16,7 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import { ensureDir, isFileAbsentError, isFileMissingError, pathExists, writeFileAtomic, } from "../../core/fs/fs-utils.js";
|
|
17
17
|
import { sha256Hex, stableStringify } from "../../core/shared/serialization.js";
|
|
18
18
|
import { isRfc3339DateTime, nowIso } from "../../core/shared/time.js";
|
|
19
|
+
import { hashItemScalarDecisionValue, isItemScalarMissingValue, } from "./three-way.js";
|
|
19
20
|
import { readBoundedRegularFile } from "./receipt-file-boundary.js";
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
21
22
|
const RECEIPT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
@@ -31,6 +32,7 @@ const RECEIPT_KEYS = new Set([
|
|
|
31
32
|
"item_path",
|
|
32
33
|
"item_id",
|
|
33
34
|
"requested_preference",
|
|
35
|
+
"requested_preference_applied",
|
|
34
36
|
"preferred",
|
|
35
37
|
"conflict_resolution",
|
|
36
38
|
"fields_from_theirs",
|
|
@@ -41,6 +43,7 @@ const RECEIPT_KEYS = new Set([
|
|
|
41
43
|
"created_at",
|
|
42
44
|
"reconciled_at",
|
|
43
45
|
"value_availability",
|
|
46
|
+
"value_policy",
|
|
44
47
|
"evidence_source",
|
|
45
48
|
]);
|
|
46
49
|
/** Return whether a receipt id is safe to expose in cleartext diagnostics. */
|
|
@@ -54,7 +57,33 @@ const RECEIPT_DECISION_KEYS = new Set([
|
|
|
54
57
|
"theirs",
|
|
55
58
|
"retained",
|
|
56
59
|
"discarded",
|
|
60
|
+
"retained_side",
|
|
61
|
+
"resolution_basis",
|
|
57
62
|
]);
|
|
63
|
+
const DURABLE_VALUE_POLICY = "bounded_non_sensitive_scalars_v1";
|
|
64
|
+
const REVIEWABLE_BUILTIN_STATUSES = new Set([
|
|
65
|
+
"open",
|
|
66
|
+
"in_progress",
|
|
67
|
+
"blocked",
|
|
68
|
+
"closed",
|
|
69
|
+
"canceled",
|
|
70
|
+
]);
|
|
71
|
+
const REVIEWABLE_ORDINAL_FIELDS = new Set(["risk", "confidence", "severity"]);
|
|
72
|
+
const REVIEWABLE_ORDINAL_VALUES = new Set([
|
|
73
|
+
"low",
|
|
74
|
+
"medium",
|
|
75
|
+
"high",
|
|
76
|
+
"critical",
|
|
77
|
+
]);
|
|
78
|
+
const REVIEWABLE_DURABLE_VALUES_BY_FIELD = new Map([
|
|
79
|
+
["priority", new Set([0, 1, 2, 3, 4].map(stableStringify))],
|
|
80
|
+
["status", new Set([...REVIEWABLE_BUILTIN_STATUSES].map(stableStringify))],
|
|
81
|
+
...[...REVIEWABLE_ORDINAL_FIELDS].map((field) => [
|
|
82
|
+
field,
|
|
83
|
+
new Set([...REVIEWABLE_ORDINAL_VALUES].map(stableStringify)),
|
|
84
|
+
]),
|
|
85
|
+
]);
|
|
86
|
+
/** Resolve the clone-local receipt store without crossing a Git worktree boundary. */
|
|
58
87
|
async function resolveReceiptDirectory(cwd) {
|
|
59
88
|
try {
|
|
60
89
|
const { stdout } = await execFileAsync("git", [
|
|
@@ -69,6 +98,7 @@ async function resolveReceiptDirectory(cwd) {
|
|
|
69
98
|
return null;
|
|
70
99
|
}
|
|
71
100
|
}
|
|
101
|
+
/** Derive the tracker root only when the merged item belongs to this workspace. */
|
|
72
102
|
async function resolveTrackerRootFromItemPath(cwd, itemPath) {
|
|
73
103
|
let directory = path.dirname(path.resolve(cwd, itemPath));
|
|
74
104
|
for (let depth = 0; depth < 12; depth += 1) {
|
|
@@ -81,23 +111,29 @@ async function resolveTrackerRootFromItemPath(cwd, itemPath) {
|
|
|
81
111
|
}
|
|
82
112
|
return null;
|
|
83
113
|
}
|
|
114
|
+
/** Locate the tracked durable receipt sidecar directory. */
|
|
84
115
|
function durableReceiptDirectory(pmRoot) {
|
|
85
116
|
return path.join(pmRoot, "merge-receipts");
|
|
86
117
|
}
|
|
118
|
+
/** Map a validated opaque receipt identifier to its canonical filename. */
|
|
87
119
|
function receiptFileName(id) {
|
|
88
120
|
return `${id}.json`;
|
|
89
121
|
}
|
|
122
|
+
/** Narrow untrusted JSON to a non-array object before schema inspection. */
|
|
90
123
|
function isRecord(value) {
|
|
91
124
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
125
|
}
|
|
126
|
+
/** Refuse receipt extensions outside the versioned schema key set. */
|
|
93
127
|
function hasOnlyKeys(value, allowed) {
|
|
94
128
|
return Object.keys(value).every((key) => allowed.has(key));
|
|
95
129
|
}
|
|
130
|
+
/** Validate bounded dense string collections used in receipt coordinates. */
|
|
96
131
|
function isStringArray(value) {
|
|
97
132
|
return (Array.isArray(value) &&
|
|
98
133
|
value.length <= RECEIPT_FIELD_MAX &&
|
|
99
134
|
value.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= 256));
|
|
100
135
|
}
|
|
136
|
+
/** Bound JSON depth and node count before retaining untrusted decision values. */
|
|
101
137
|
function hasBoundedJsonStructure(value) {
|
|
102
138
|
const pending = [
|
|
103
139
|
{ value, depth: 0 },
|
|
@@ -123,23 +159,37 @@ function hasBoundedJsonStructure(value) {
|
|
|
123
159
|
}
|
|
124
160
|
return true;
|
|
125
161
|
}
|
|
162
|
+
/** Validate one clone-local scalar decision and its optional resolution evidence. */
|
|
126
163
|
function isMergeDecision(value) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
if (!isRecord(value))
|
|
165
|
+
return false;
|
|
166
|
+
const retainedSides = [undefined, "ours", "theirs"];
|
|
167
|
+
const resolutionBases = [
|
|
168
|
+
undefined,
|
|
169
|
+
"requested_preference",
|
|
170
|
+
"document_updated_at",
|
|
171
|
+
"stable_value_tiebreak",
|
|
172
|
+
];
|
|
173
|
+
return [
|
|
174
|
+
hasOnlyKeys(value, RECEIPT_DECISION_KEYS),
|
|
175
|
+
typeof value.field === "string",
|
|
176
|
+
typeof value.field === "string" && value.field.length > 0,
|
|
177
|
+
typeof value.field === "string" && value.field.length <= 256,
|
|
178
|
+
Object.hasOwn(value, "base"),
|
|
179
|
+
Object.hasOwn(value, "ours"),
|
|
180
|
+
Object.hasOwn(value, "theirs"),
|
|
181
|
+
Object.hasOwn(value, "retained"),
|
|
182
|
+
Object.hasOwn(value, "discarded"),
|
|
183
|
+
hasBoundedJsonStructure(value.base),
|
|
184
|
+
hasBoundedJsonStructure(value.ours),
|
|
185
|
+
hasBoundedJsonStructure(value.theirs),
|
|
186
|
+
hasBoundedJsonStructure(value.retained),
|
|
187
|
+
hasBoundedJsonStructure(value.discarded),
|
|
188
|
+
retainedSides.includes(value.retained_side),
|
|
189
|
+
resolutionBases.includes(value.resolution_basis),
|
|
190
|
+
].every(Boolean);
|
|
191
|
+
}
|
|
192
|
+
/** Validate the complete field-to-SHA-256 proof map used during repair. */
|
|
143
193
|
function isMergedFieldHashes(value) {
|
|
144
194
|
if (!isRecord(value))
|
|
145
195
|
return false;
|
|
@@ -150,6 +200,7 @@ function isMergedFieldHashes(value) {
|
|
|
150
200
|
typeof hash === "string" &&
|
|
151
201
|
/^[a-f0-9]{64}$/u.test(hash)));
|
|
152
202
|
}
|
|
203
|
+
/** Require a normalized tracker item path whose basename matches the receipt item. */
|
|
153
204
|
function isSafeReceiptItemPath(value, itemId) {
|
|
154
205
|
if (typeof value !== "string" ||
|
|
155
206
|
value.length === 0 ||
|
|
@@ -164,15 +215,7 @@ function isSafeReceiptItemPath(value, itemId) {
|
|
|
164
215
|
!normalized.startsWith("../") &&
|
|
165
216
|
path.posix.basename(normalized, path.posix.extname(normalized)) === itemId);
|
|
166
217
|
}
|
|
167
|
-
|
|
168
|
-
return !hasOnlyKeys(value, RECEIPT_KEYS) || value.version !== 1
|
|
169
|
-
? false
|
|
170
|
-
: typeof value.id === "string" &&
|
|
171
|
-
isSafeReceiptId(value.id) &&
|
|
172
|
-
typeof value.item_id === "string" &&
|
|
173
|
-
RECEIPT_ITEM_ID_PATTERN.test(value.item_id) &&
|
|
174
|
-
isSafeReceiptItemPath(value.item_path, value.item_id);
|
|
175
|
-
}
|
|
218
|
+
/** Validate receipt arrays and optional merged-field proof coverage. */
|
|
176
219
|
function hasValidReceiptCollections(value) {
|
|
177
220
|
return !isStringArray(value.fields_from_theirs) ||
|
|
178
221
|
!isStringArray(value.union_fields) ||
|
|
@@ -183,18 +226,35 @@ function hasValidReceiptCollections(value) {
|
|
|
183
226
|
: value.merged_field_hashes === undefined ||
|
|
184
227
|
isMergedFieldHashes(value.merged_field_hashes);
|
|
185
228
|
}
|
|
229
|
+
/** Validate every closed-domain lifecycle and merge-policy field. */
|
|
186
230
|
function hasValidReceiptEnums(value) {
|
|
187
231
|
const preferences = [undefined, "ours", "theirs"];
|
|
188
|
-
const resolutions = [
|
|
189
|
-
|
|
232
|
+
const resolutions = [
|
|
233
|
+
undefined,
|
|
234
|
+
"preferred_side",
|
|
235
|
+
"stable_value_order",
|
|
236
|
+
"latest_document_update",
|
|
237
|
+
];
|
|
238
|
+
const availabilities = [
|
|
239
|
+
undefined,
|
|
240
|
+
"clone_local",
|
|
241
|
+
"hash_only",
|
|
242
|
+
"bounded_inline",
|
|
243
|
+
"mixed",
|
|
244
|
+
];
|
|
190
245
|
const sources = [undefined, "clone_local", "durable"];
|
|
191
246
|
return (preferences.includes(value.requested_preference) &&
|
|
192
247
|
preferences.includes(value.preferred) &&
|
|
248
|
+
(value.requested_preference_applied === undefined ||
|
|
249
|
+
typeof value.requested_preference_applied === "boolean") &&
|
|
193
250
|
resolutions.includes(value.conflict_resolution) &&
|
|
194
251
|
(value.state === "pending" || value.state === "reconciled") &&
|
|
195
252
|
availabilities.includes(value.value_availability) &&
|
|
253
|
+
(value.value_policy === undefined ||
|
|
254
|
+
value.value_policy === DURABLE_VALUE_POLICY) &&
|
|
196
255
|
sources.includes(value.evidence_source));
|
|
197
256
|
}
|
|
257
|
+
/** Validate creation and optional settlement timestamps as RFC 3339 values. */
|
|
198
258
|
function hasValidReceiptTimestamps(value) {
|
|
199
259
|
return (typeof value.created_at === "string" &&
|
|
200
260
|
isRfc3339DateTime(value.created_at) &&
|
|
@@ -202,28 +262,99 @@ function hasValidReceiptTimestamps(value) {
|
|
|
202
262
|
(typeof value.reconciled_at === "string" &&
|
|
203
263
|
isRfc3339DateTime(value.reconciled_at))));
|
|
204
264
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
265
|
+
/** Validate one hash-only or policy-bounded durable decision value. */
|
|
266
|
+
function hasDurableDecisionValue(field, value) {
|
|
267
|
+
if (!isRecord(value) || isPrehashedValue(value) === undefined)
|
|
268
|
+
return false;
|
|
269
|
+
const keys = Object.keys(value);
|
|
270
|
+
if (!keys.every((key) => key === "pm_value_hash" || key === "pm_value")) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
if (!Object.hasOwn(value, "pm_value"))
|
|
274
|
+
return keys.length === 1;
|
|
275
|
+
const preview = value.pm_value;
|
|
276
|
+
const allowed = preview === null ||
|
|
277
|
+
REVIEWABLE_DURABLE_VALUES_BY_FIELD.get(field)?.has(stableStringify(preview)) === true;
|
|
278
|
+
return (allowed && hashItemScalarDecisionValue(preview) === value.pm_value_hash);
|
|
279
|
+
}
|
|
280
|
+
/** Validate durable decision redaction and the declared availability summary. */
|
|
281
|
+
function hasValidDurableDecisions(receipt) {
|
|
282
|
+
if (!receipt.decisions.every((decision) => decision.base === null &&
|
|
283
|
+
decision.ours === null &&
|
|
284
|
+
decision.theirs === null &&
|
|
285
|
+
hasDurableDecisionValue(decision.field, decision.retained) &&
|
|
286
|
+
hasDurableDecisionValue(decision.field, decision.discarded))) {
|
|
219
287
|
return false;
|
|
220
288
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
289
|
+
const inlineValueCount = receipt.decisions.reduce((count, decision) => count +
|
|
290
|
+
(isRecord(decision.retained) &&
|
|
291
|
+
Object.hasOwn(decision.retained, "pm_value")
|
|
292
|
+
? 1
|
|
293
|
+
: 0) +
|
|
294
|
+
(isRecord(decision.discarded) &&
|
|
295
|
+
Object.hasOwn(decision.discarded, "pm_value")
|
|
296
|
+
? 1
|
|
297
|
+
: 0), 0);
|
|
298
|
+
const expectedAvailability = inlineValueCount === 0
|
|
299
|
+
? "hash_only"
|
|
300
|
+
: inlineValueCount === receipt.decisions.length * 2
|
|
301
|
+
? "bounded_inline"
|
|
302
|
+
: "mixed";
|
|
303
|
+
return (receipt.value_availability === expectedAvailability &&
|
|
304
|
+
(inlineValueCount === 0 || receipt.value_policy === DURABLE_VALUE_POLICY));
|
|
305
|
+
}
|
|
306
|
+
/** Return the narrowest validation class for untrusted receipt evidence. */
|
|
307
|
+
function receiptValidationError(value, evidenceSource) {
|
|
308
|
+
if (!isRecord(value))
|
|
309
|
+
return "required_fields";
|
|
310
|
+
for (const key of [
|
|
311
|
+
"version",
|
|
312
|
+
"id",
|
|
313
|
+
"item_path",
|
|
314
|
+
"item_id",
|
|
315
|
+
"fields_from_theirs",
|
|
316
|
+
"union_fields",
|
|
317
|
+
"decisions",
|
|
318
|
+
"state",
|
|
319
|
+
"created_at",
|
|
320
|
+
]) {
|
|
321
|
+
if (!Object.hasOwn(value, key))
|
|
322
|
+
return "required_fields";
|
|
323
|
+
}
|
|
324
|
+
if (value.version !== 1 || !hasOnlyKeys(value, RECEIPT_KEYS)) {
|
|
325
|
+
return "required_fields";
|
|
326
|
+
}
|
|
327
|
+
if (typeof value.id !== "string" || !isSafeReceiptId(value.id)) {
|
|
328
|
+
return "receipt_id";
|
|
329
|
+
}
|
|
330
|
+
if (typeof value.item_id !== "string" ||
|
|
331
|
+
!RECEIPT_ITEM_ID_PATTERN.test(value.item_id)) {
|
|
332
|
+
return "item_id";
|
|
333
|
+
}
|
|
334
|
+
if (!isSafeReceiptItemPath(value.item_path, value.item_id)) {
|
|
335
|
+
return "item_path";
|
|
336
|
+
}
|
|
337
|
+
if (!hasValidReceiptCollections(value))
|
|
338
|
+
return "collections";
|
|
339
|
+
if (!hasValidReceiptEnums(value))
|
|
340
|
+
return "enums";
|
|
341
|
+
if (!hasValidReceiptTimestamps(value))
|
|
342
|
+
return "timestamps";
|
|
343
|
+
if (evidenceSource === "durable" &&
|
|
344
|
+
!hasValidDurableDecisions(value)) {
|
|
345
|
+
return "durable_decisions";
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
223
348
|
}
|
|
349
|
+
/** Validate a complete receipt against its clone-local or durable evidence policy. */
|
|
350
|
+
function isMergeDecisionReceipt(value, evidenceSource) {
|
|
351
|
+
return receiptValidationError(value, evidenceSource) === null;
|
|
352
|
+
}
|
|
353
|
+
/** Read one receipt through the no-follow bounded regular-file guard. */
|
|
224
354
|
async function readBoundedRegularReceiptFile(receiptPath) {
|
|
225
355
|
return readBoundedRegularFile(receiptPath, RECEIPT_FILE_MAX_BYTES);
|
|
226
356
|
}
|
|
357
|
+
/** Revalidate one receipt and stage its atomic reconciled-state replacement. */
|
|
227
358
|
async function prepareReceiptSettlement(params) {
|
|
228
359
|
let raw;
|
|
229
360
|
try {
|
|
@@ -260,6 +391,7 @@ async function prepareReceiptSettlement(params) {
|
|
|
260
391
|
}, null, 2)}\n`,
|
|
261
392
|
};
|
|
262
393
|
}
|
|
394
|
+
/** Fingerprint immutable receipt provenance while excluding lifecycle state. */
|
|
263
395
|
function receiptProvenanceFingerprint(receipt) {
|
|
264
396
|
return sha256Hex(stableStringify({
|
|
265
397
|
id: receipt.id,
|
|
@@ -274,6 +406,7 @@ function receiptProvenanceFingerprint(receipt) {
|
|
|
274
406
|
decisions: summarizeMergeReceipt(receipt).decisions,
|
|
275
407
|
}));
|
|
276
408
|
}
|
|
409
|
+
/** Merge clone-local and durable lifecycle state without losing pending work. */
|
|
277
410
|
function mergeReceiptCopyLifecycle(local, durable) {
|
|
278
411
|
if (local.state === "reconciled" && durable.state === "reconciled") {
|
|
279
412
|
return local;
|
|
@@ -291,16 +424,23 @@ export function summarizeMergeReceipt(receipt) {
|
|
|
291
424
|
fields_from_theirs: receipt.fields_from_theirs,
|
|
292
425
|
union_fields: receipt.union_fields,
|
|
293
426
|
requested_preference: receipt.requested_preference ?? receipt.preferred ?? "ours",
|
|
427
|
+
requested_preference_applied: receipt.requested_preference_applied ??
|
|
428
|
+
(receipt.conflict_resolution ?? "preferred_side") === "preferred_side",
|
|
294
429
|
conflict_resolution: receipt.conflict_resolution,
|
|
295
430
|
decisions: receipt.decisions.map((decision) => ({
|
|
296
431
|
field: decision.field,
|
|
297
432
|
retained_hash: isPrehashedValue(decision.retained) ??
|
|
298
|
-
|
|
433
|
+
hashItemScalarDecisionValue(isItemScalarMissingValue(decision.retained)
|
|
434
|
+
? undefined
|
|
435
|
+
: decision.retained),
|
|
299
436
|
discarded_hash: isPrehashedValue(decision.discarded) ??
|
|
300
|
-
|
|
437
|
+
hashItemScalarDecisionValue(isItemScalarMissingValue(decision.discarded)
|
|
438
|
+
? undefined
|
|
439
|
+
: decision.discarded),
|
|
301
440
|
})),
|
|
302
441
|
};
|
|
303
442
|
}
|
|
443
|
+
/** Extract a structurally valid precomputed SHA-256 decision hash. */
|
|
304
444
|
function isPrehashedValue(value) {
|
|
305
445
|
if (typeof value !== "object" ||
|
|
306
446
|
value === null ||
|
|
@@ -313,10 +453,16 @@ function isPrehashedValue(value) {
|
|
|
313
453
|
? hash
|
|
314
454
|
: undefined;
|
|
315
455
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
456
|
+
/** Convert a raw scalar into hashed evidence with an allowlisted preview. */
|
|
457
|
+
function durableValueEvidence(field, value) {
|
|
458
|
+
const missing = isItemScalarMissingValue(value);
|
|
459
|
+
const pmValueHash = hashItemScalarDecisionValue(missing ? undefined : value);
|
|
460
|
+
const reviewable = !missing &&
|
|
461
|
+
(value === null ||
|
|
462
|
+
REVIEWABLE_DURABLE_VALUES_BY_FIELD.get(field)?.has(stableStringify(value)) === true);
|
|
463
|
+
return reviewable
|
|
464
|
+
? { pm_value_hash: pmValueHash, pm_value: value }
|
|
465
|
+
: { pm_value_hash: pmValueHash };
|
|
320
466
|
}
|
|
321
467
|
/** Persist one item-driver outcome in the clone-local Git directory. */
|
|
322
468
|
export async function writeMergeReceipt(params) {
|
|
@@ -325,11 +471,7 @@ export async function writeMergeReceipt(params) {
|
|
|
325
471
|
return null;
|
|
326
472
|
}
|
|
327
473
|
const trimmedItemPath = params.itemPath.trim();
|
|
328
|
-
const itemPath = trimmedItemPath
|
|
329
|
-
((trimmedItemPath.startsWith("'") && trimmedItemPath.endsWith("'")) ||
|
|
330
|
-
(trimmedItemPath.startsWith('"') && trimmedItemPath.endsWith('"')))
|
|
331
|
-
? trimmedItemPath.slice(1, -1)
|
|
332
|
-
: trimmedItemPath;
|
|
474
|
+
const itemPath = /^(['"])(.*)\1$/su.exec(trimmedItemPath)?.[2] ?? trimmedItemPath;
|
|
333
475
|
const itemId = path.basename(itemPath, path.extname(itemPath));
|
|
334
476
|
const receipt = {
|
|
335
477
|
version: 1,
|
|
@@ -337,6 +479,7 @@ export async function writeMergeReceipt(params) {
|
|
|
337
479
|
item_path: itemPath.replaceAll("\\", "/"),
|
|
338
480
|
item_id: itemId,
|
|
339
481
|
requested_preference: params.preferred,
|
|
482
|
+
requested_preference_applied: (params.conflictResolution ?? "preferred_side") === "preferred_side",
|
|
340
483
|
conflict_resolution: params.conflictResolution ?? "preferred_side",
|
|
341
484
|
fields_from_theirs: [...params.fieldsFromTheirs],
|
|
342
485
|
union_fields: [...params.unionFields],
|
|
@@ -353,24 +496,35 @@ export async function writeMergeReceipt(params) {
|
|
|
353
496
|
const trackerRoot = await resolveTrackerRootFromItemPath(params.cwd, receipt.item_path);
|
|
354
497
|
if (trackerRoot !== null) {
|
|
355
498
|
const durableDirectory = durableReceiptDirectory(trackerRoot);
|
|
356
|
-
const
|
|
499
|
+
const durableDecisions = receipt.decisions.map((decision) => ({
|
|
500
|
+
field: decision.field,
|
|
501
|
+
base: null,
|
|
502
|
+
ours: null,
|
|
503
|
+
theirs: null,
|
|
504
|
+
retained: durableValueEvidence(decision.field, decision.retained),
|
|
505
|
+
discarded: durableValueEvidence(decision.field, decision.discarded),
|
|
506
|
+
retained_side: decision.retained_side,
|
|
507
|
+
resolution_basis: decision.resolution_basis,
|
|
508
|
+
}));
|
|
509
|
+
const inlineValueCount = durableDecisions.reduce((count, decision) => count +
|
|
510
|
+
(Object.hasOwn(decision.retained, "pm_value") ? 1 : 0) +
|
|
511
|
+
(Object.hasOwn(decision.discarded, "pm_value") ? 1 : 0), 0);
|
|
357
512
|
const durableReceipt = {
|
|
358
513
|
...receipt,
|
|
359
|
-
decisions:
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
})),
|
|
367
|
-
value_availability: "hash_only",
|
|
514
|
+
decisions: durableDecisions,
|
|
515
|
+
value_availability: inlineValueCount === 0
|
|
516
|
+
? "hash_only"
|
|
517
|
+
: inlineValueCount === durableDecisions.length * 2
|
|
518
|
+
? "bounded_inline"
|
|
519
|
+
: "mixed",
|
|
520
|
+
value_policy: DURABLE_VALUE_POLICY,
|
|
368
521
|
};
|
|
369
522
|
await ensureDir(durableDirectory);
|
|
370
523
|
await writeFileAtomic(path.join(durableDirectory, receiptFileName(receipt.id)), `${JSON.stringify(durableReceipt, null, 2)}\n`);
|
|
371
524
|
}
|
|
372
525
|
return receipt;
|
|
373
526
|
}
|
|
527
|
+
/** Scan one receipt directory with bounded per-candidate failure evidence. */
|
|
374
528
|
async function readReceiptsFromDirectory(directory, evidenceSource) {
|
|
375
529
|
let names;
|
|
376
530
|
try {
|
|
@@ -390,14 +544,17 @@ async function readReceiptsFromDirectory(directory, evidenceSource) {
|
|
|
390
544
|
const receipts = [];
|
|
391
545
|
let invalidEvidenceCount = 0;
|
|
392
546
|
const invalidEvidence = [];
|
|
393
|
-
const recordInvalidEvidence = (name,
|
|
547
|
+
const recordInvalidEvidence = (name, candidate) => {
|
|
394
548
|
invalidEvidenceCount += 1;
|
|
395
549
|
if (invalidEvidence.length >= RECEIPT_INVALID_EVIDENCE_DETAIL_LIMIT)
|
|
396
550
|
return;
|
|
397
551
|
const receiptId = name.slice(0, -".json".length);
|
|
398
552
|
invalidEvidence.push({
|
|
399
553
|
evidence_source: evidenceSource,
|
|
400
|
-
reason,
|
|
554
|
+
reason: candidate.reason,
|
|
555
|
+
...(candidate.validation_error === undefined
|
|
556
|
+
? {}
|
|
557
|
+
: { validation_error: candidate.validation_error }),
|
|
401
558
|
...(isSafeReceiptId(receiptId)
|
|
402
559
|
? { receipt_id: receiptId }
|
|
403
560
|
: { candidate_name_hash: sha256Hex(name) }),
|
|
@@ -411,7 +568,7 @@ async function readReceiptsFromDirectory(directory, evidenceSource) {
|
|
|
411
568
|
receipts.push(candidate.receipt);
|
|
412
569
|
}
|
|
413
570
|
else {
|
|
414
|
-
recordInvalidEvidence(name, candidate
|
|
571
|
+
recordInvalidEvidence(name, candidate);
|
|
415
572
|
}
|
|
416
573
|
}
|
|
417
574
|
return {
|
|
@@ -421,6 +578,7 @@ async function readReceiptsFromDirectory(directory, evidenceSource) {
|
|
|
421
578
|
invalid_evidence_truncated: invalidEvidenceCount > invalidEvidence.length,
|
|
422
579
|
};
|
|
423
580
|
}
|
|
581
|
+
/** Distinguish a missing optional receipt store from an unreadable one. */
|
|
424
582
|
async function receiptDirectoryFailureMeansAbsent(directory, error, inspectAncestor = stat) {
|
|
425
583
|
if (!isFileMissingError(error))
|
|
426
584
|
return false;
|
|
@@ -439,6 +597,7 @@ async function receiptDirectoryFailureMeansAbsent(directory, error, inspectAnces
|
|
|
439
597
|
}
|
|
440
598
|
}
|
|
441
599
|
}
|
|
600
|
+
/** Inspect one candidate without exposing rejected file contents. */
|
|
442
601
|
async function inspectReceiptCandidate(directory, name, evidenceSource) {
|
|
443
602
|
let raw;
|
|
444
603
|
try {
|
|
@@ -456,16 +615,28 @@ async function inspectReceiptCandidate(directory, name, evidenceSource) {
|
|
|
456
615
|
catch {
|
|
457
616
|
return { reason: "candidate_invalid_json" };
|
|
458
617
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
618
|
+
const validationError = receiptValidationError(parsed, evidenceSource);
|
|
619
|
+
if (validationError !== null) {
|
|
620
|
+
const identityErrors = new Set(["receipt_id", "item_id", "item_path"]);
|
|
621
|
+
return {
|
|
622
|
+
reason: identityErrors.has(validationError)
|
|
623
|
+
? "identity_invalid"
|
|
624
|
+
: "schema_invalid",
|
|
625
|
+
validation_error: validationError,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const receipt = parsed;
|
|
629
|
+
if (name !== receiptFileName(receipt.id)) {
|
|
630
|
+
return { reason: "identity_invalid", validation_error: "filename" };
|
|
462
631
|
}
|
|
463
|
-
const { preferred: legacyPreference, evidence_source: _serializedEvidenceSource, ...receiptWithoutRuntimeKeys } =
|
|
632
|
+
const { preferred: legacyPreference, evidence_source: _serializedEvidenceSource, ...receiptWithoutRuntimeKeys } = receipt;
|
|
464
633
|
return {
|
|
465
634
|
receipt: {
|
|
466
635
|
...receiptWithoutRuntimeKeys,
|
|
467
|
-
requested_preference:
|
|
468
|
-
|
|
636
|
+
requested_preference: receipt.requested_preference ?? legacyPreference ?? "ours",
|
|
637
|
+
requested_preference_applied: receipt.requested_preference_applied ??
|
|
638
|
+
(receipt.conflict_resolution ?? "preferred_side") === "preferred_side",
|
|
639
|
+
conflict_resolution: receipt.conflict_resolution ?? "preferred_side",
|
|
469
640
|
evidence_source: evidenceSource,
|
|
470
641
|
},
|
|
471
642
|
};
|
|
@@ -638,4 +809,4 @@ export const _testOnlyMergeReceipts = {
|
|
|
638
809
|
receiptDirectoryFailureMeansAbsent,
|
|
639
810
|
};
|
|
640
811
|
//# sourceMappingURL=receipts.js.map
|
|
641
|
-
//# debugId=
|
|
812
|
+
//# debugId=dfdd6f10-e11f-59e8-a16f-507dc40d5b31
|