@unbrained/pm-cli 2026.9.2 → 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 +44 -0
- package/dist/cli/after-command-context-usage.js +16 -3
- package/dist/cli-bundle/bundle-manifest.json +151 -151
- package/dist/cli-bundle/chunks/chunk-6XSKPNCF.js +230 -0
- package/dist/cli-bundle/chunks/chunk-A24WAVE6.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-TAG4VC22.js → chunk-HI6BHPCU.js} +27 -27
- package/dist/cli-bundle/chunks/{chunk-Y7VOMSZT.js → chunk-I2AS367G.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-X4RCGLTX.js → chunk-OGBVU67G.js} +5 -5
- package/dist/cli-bundle/chunks/{chunk-DTGWEL23.js → chunk-RI4XNOYS.js} +2 -2
- package/dist/cli-bundle/chunks/chunk-TXW6VL33.js +38 -0
- package/dist/cli-bundle/chunks/{chunk-FZ4Z2J4F.js → chunk-V72UV7QS.js} +76 -79
- package/dist/cli-bundle/chunks/chunk-VGLHECAP.js +9 -0
- package/dist/cli-bundle/chunks/{chunk-T6H5BZCP.js → chunk-X6CMGV6L.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-3WICXLVT.js → chunk-YW4QK2G7.js} +2 -2
- package/dist/cli-bundle/chunks/{register-list-query-XYNX4ASQ.js → register-list-query-Z5YYSJJG.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-WFQ7MJTX.js → register-mutation-ELSP6I4I.js} +2 -2
- package/dist/cli-bundle/chunks/{register-operations-JVTI7JPP.js → register-operations-OH7R3ZT4.js} +2 -2
- package/dist/cli-bundle/chunks/{register-setup-KVNC5KPL.js → register-setup-E2AE72QG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-MEASX544.js → chunk-25UJBH4M.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-SVXJYD6G.js → chunk-3646BZ4W.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-4YRAVI43.js +12 -0
- package/dist/cli-bundle/focused-chunks/chunk-53IHPICX.js +3 -0
- 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-4O4CJAR6.js → chunk-6PWPQKDN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-7N2TV7WZ.js +8 -0
- package/dist/cli-bundle/focused-chunks/chunk-AJR3OP2E.js +3 -0
- package/dist/cli-bundle/focused-chunks/chunk-B3A6MJYE.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-BFDDJ2T2.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-BHUIRZWQ.js +9 -0
- package/dist/cli-bundle/focused-chunks/chunk-JADHHPY6.js +6 -0
- package/dist/cli-bundle/focused-chunks/{chunk-KB5OFSFD.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-BEVHCEWG.js → chunk-P2GOAOQN.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-YARWJM2B.js +2 -0
- package/dist/cli-bundle/main.js +15 -15
- 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 +43 -36
- 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/event-classification.d.ts +21 -0
- package/dist/core/history/event-classification.js +109 -0
- package/dist/core/history/event-index.d.ts +14 -3
- package/dist/core/history/event-index.js +543 -88
- package/dist/core/history/history.d.ts +37 -1
- package/dist/core/history/history.js +320 -71
- package/dist/core/history/replay.js +68 -34
- package/dist/core/history/workspace-history.js +7 -6
- package/dist/core/lock/lock.d.ts +32 -2
- package/dist/core/lock/lock.js +161 -20
- package/dist/core/output/output.js +31 -7
- package/dist/core/shared/author.d.ts +3 -1
- package/dist/core/shared/author.js +111 -23
- package/dist/core/shared/time.d.ts +2 -0
- package/dist/core/shared/time.js +8 -2
- package/dist/core/store/item-metadata-query-index.js +5 -5
- package/dist/sdk/context/recency.d.ts +9 -0
- package/dist/sdk/context/recency.js +34 -0
- package/dist/sdk/context/usage-egress.d.ts +9 -0
- package/dist/sdk/context/usage-egress.js +30 -0
- package/dist/sdk/context-intent-contracts.js +7 -3
- package/dist/sdk/context-relevance.d.ts +23 -0
- package/dist/sdk/context-relevance.js +94 -23
- package/dist/sdk/context-signal-store.d.ts +7 -3
- package/dist/sdk/context-signal-store.js +155 -42
- package/dist/sdk/context-usage.d.ts +54 -0
- package/dist/sdk/context-usage.js +245 -16
- 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 -2
- 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 +4 -0
- package/dist/sdk/index.js +6 -2
- package/dist/sdk/init.d.ts +2 -0
- package/dist/sdk/init.js +18 -9
- 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/query/context.d.ts +3 -1
- package/dist/sdk/query/context.js +21 -9
- package/dist/sdk/query/next.js +18 -9
- package/dist/sdk/runtime-input.d.ts +2 -0
- package/dist/sdk/runtime-input.js +25 -2
- package/dist/sdk/runtime-primitives.d.ts +2 -2
- package/dist/sdk/runtime-primitives.js +4 -4
- package/dist/sdk/runtime.d.ts +1 -0
- package/dist/sdk/runtime.js +17 -26
- package/dist/types.d.ts +27 -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/CONTEXT_RELEVANCE.md +44 -1
- package/docs/MERGE_SAFETY.md +119 -23
- package/docs/PERFORMANCE.md +6 -3
- package/docs/README.md +1 -0
- package/docs/READ_OUTPUT_CONTRACTS.md +9 -1
- package/docs/SDK.md +11 -8
- package/docs/SDK_CONTEXT.md +7 -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 +5 -3
- package/marketplace.json +2 -2
- package/package.json +5 -5
- 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 +773 -46
- package/dist/cli-bundle/chunks/chunk-GOACULJW.js +0 -13
- package/dist/cli-bundle/chunks/chunk-NOPL37X4.js +0 -205
- package/dist/cli-bundle/chunks/chunk-UDW64J2W.js +0 -8
- package/dist/cli-bundle/chunks/chunk-ZLJIFSOE.js +0 -35
- package/dist/cli-bundle/focused-chunks/chunk-242AXYB2.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-2FEZ5SKM.js +0 -3
- package/dist/cli-bundle/focused-chunks/chunk-6GCRSLPG.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-7VATS5QY.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-JEFKLWMM.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-MKHG3T76.js +0 -20
- package/dist/cli-bundle/focused-chunks/chunk-O67VDLOT.js +0 -51
- package/dist/cli-bundle/focused-chunks/chunk-PF3E6Q36.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-QDFJIRKN.js +0 -4
- package/dist/cli-bundle/focused-chunks/chunk-RMRCQPWU.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-S4U5CN5Q.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-T2LXXSOD.js +0 -158
- package/dist/cli-bundle/focused-chunks/chunk-Z2USIBR2.js +0 -5
|
@@ -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
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module sdk/merge/reconcile
|
|
3
|
-
*
|
|
4
|
-
* Provides the post-merge SDK workflow that previews or repairs every drifted
|
|
5
|
-
* item-history stream, then verifies history and storage integrity in one call.
|
|
6
|
-
*/
|
|
7
1
|
import type { GlobalOptions } from "../../core/shared/command-types.js";
|
|
8
|
-
import { type HistoryRepairAllResult } from "../history-repair.js";
|
|
9
2
|
import { type ValidateResult } from "../governance/validate.js";
|
|
3
|
+
import { type HistoryRepairAllResult } from "../history-repair.js";
|
|
10
4
|
import { type MergeDecisionReceiptSummary } from "./receipts.js";
|
|
11
5
|
/** Options for the audited post-merge reconciliation workflow. */
|
|
12
6
|
export interface MergeReconcileOptions {
|
|
@@ -36,6 +30,14 @@ export interface MergeReconcileResult {
|
|
|
36
30
|
pending_before: number;
|
|
37
31
|
reconciled: number;
|
|
38
32
|
summaries: MergeDecisionReceiptSummary[];
|
|
33
|
+
/** Missing durable references discovered before this reconciliation. */
|
|
34
|
+
missing_history_references_before: number;
|
|
35
|
+
/** Pre-durable references eligible for explicit audited disposition. */
|
|
36
|
+
legacy_disposition_eligible: number;
|
|
37
|
+
/** Eligible references dispositioned by this apply pass. */
|
|
38
|
+
legacy_disposition_recorded: number;
|
|
39
|
+
/** Missing durable references still blocking health after this pass. */
|
|
40
|
+
missing_history_references_after: number;
|
|
39
41
|
};
|
|
40
42
|
/** Post-operation history-drift and storage-integrity validation. */
|
|
41
43
|
validation: ValidateResult;
|