@unbrained/pm-cli 2026.8.12 → 2026.8.14
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 +162 -111
- package/dist/cli/error-guidance.js +4 -8
- package/dist/cli/main.js +40 -3
- package/dist/cli/register-assurance.js +28 -17
- package/dist/cli-bundle/bundle-manifest.json +141 -173
- package/dist/cli-bundle/chunks/chunk-BBOFSVAK.js +197 -0
- package/dist/cli-bundle/chunks/chunk-BFALV3YP.js +5 -0
- package/dist/cli-bundle/chunks/{chunk-WLAHPSOX.js → chunk-FIW4P64F.js} +2 -2
- package/dist/cli-bundle/chunks/{chunk-BM5YJ2UW.js → chunk-G5JUB66K.js} +52 -52
- package/dist/cli-bundle/chunks/chunk-SBRKH6OR.js +2 -0
- package/dist/cli-bundle/chunks/chunk-TMJ7URWS.js +13 -0
- package/dist/cli-bundle/chunks/chunk-TXMEUUUY.js +8 -0
- package/dist/cli-bundle/chunks/{chunk-KOQMGCTA.js → chunk-XAVMOK6B.js} +50 -50
- package/dist/cli-bundle/chunks/chunk-YAMTJL6Y.js +34 -0
- package/dist/cli-bundle/chunks/chunk-YSVSMJ2B.js +2 -0
- package/dist/cli-bundle/chunks/{register-list-query-XPCJRUUD.js → register-list-query-T3EHHNNV.js} +2 -2
- package/dist/cli-bundle/chunks/{register-mutation-4RJCBKRS.js → register-mutation-E27WCLSZ.js} +2 -2
- package/dist/cli-bundle/chunks/register-operations-4R6I7CPY.js +2 -0
- package/dist/cli-bundle/chunks/{register-setup-LLHAKP2V.js → register-setup-3YXZ444E.js} +2 -2
- package/dist/cli-bundle/focused-chunks/{chunk-JAFRIWVF.js → chunk-4DQ2YERC.js} +4 -4
- package/dist/cli-bundle/focused-chunks/chunk-F43TT2QQ.js +8 -0
- package/dist/cli-bundle/focused-chunks/chunk-K7RWWOAG.js +4 -0
- package/dist/cli-bundle/focused-chunks/chunk-KFSZ3BP6.js +16 -0
- package/dist/cli-bundle/focused-chunks/{chunk-KHM2CS2C.js → chunk-KSEDUWVZ.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-KYUQGTSB.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-ZMGWAYH7.js → chunk-LZ4MV3RH.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-PGXJQLCT.js +155 -0
- package/dist/cli-bundle/focused-chunks/{chunk-RWPAWUOT.js → chunk-PPFGQDSI.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-Q4YY3XNF.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-QK753U3U.js +46 -0
- package/dist/cli-bundle/focused-chunks/chunk-QUMB2J27.js +2 -0
- package/dist/cli-bundle/focused-chunks/chunk-SM7MTG27.js +2 -0
- package/dist/cli-bundle/focused-chunks/{chunk-GQW4BPUT.js → chunk-WCVE2NIG.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-XDNAW5OI.js +18 -0
- package/dist/cli-bundle/focused-chunks/{chunk-YIAXMC3T.js → chunk-YOOI3UZX.js} +2 -2
- package/dist/cli-bundle/focused-chunks/chunk-ZM6TZVHC.js +2 -0
- package/dist/cli-bundle/main.js +13 -13
- 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 +45 -41
- package/dist/cli-bundle/sdk-governance.js +1 -1
- package/dist/cli-bundle/sdk-graph.js +1 -1
- package/dist/cli-bundle/sdk-merge.js +109 -1
- 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 +2 -2
- package/dist/core/checkpoint/checkpoint-gc.js +7 -12
- package/dist/core/extensions/activation-summary-markdown.js +5 -4
- package/dist/core/extensions/activation-summary.d.ts +2 -0
- package/dist/core/extensions/activation-summary.js +6 -2
- package/dist/core/extensions/capability-usage.d.ts +1 -1
- package/dist/core/extensions/capability-usage.js +4 -3
- package/dist/core/extensions/contribution-inventory.d.ts +1 -0
- package/dist/core/extensions/contribution-inventory.js +6 -2
- package/dist/core/extensions/extension-registries.js +3 -2
- package/dist/core/extensions/extension-types.d.ts +83 -1
- package/dist/core/extensions/extension-types.js +3 -2
- package/dist/core/extensions/loader.d.ts +2 -8
- package/dist/core/extensions/loader.js +53 -144
- package/dist/core/extensions/registration-values.d.ts +43 -0
- package/dist/core/extensions/registration-values.js +210 -0
- package/dist/core/extensions/runtime-registrations.d.ts +3 -1
- package/dist/core/extensions/runtime-registrations.js +12 -2
- package/dist/core/fs/fs-utils.d.ts +2 -0
- package/dist/core/fs/fs-utils.js +7 -3
- package/dist/core/history/drift-cache.js +4 -3
- package/dist/core/history/drift-scan.d.ts +6 -0
- package/dist/core/history/drift-scan.js +76 -58
- package/dist/core/history/workspace-history.d.ts +59 -0
- package/dist/core/history/workspace-history.js +197 -10
- package/dist/core/lock/lock-gc.js +8 -13
- package/dist/core/lock/lock.js +5 -4
- package/dist/core/output/output.js +12 -4
- package/dist/core/search/vector-stores.js +6 -11
- package/dist/core/store/item-metadata-cache.js +4 -4
- package/dist/core/telemetry/runtime.d.ts +6 -0
- package/dist/core/telemetry/runtime.js +101 -30
- package/dist/sdk/agent/command-recovery.d.ts +2 -0
- package/dist/sdk/agent/command-recovery.js +10 -2
- package/dist/sdk/annotations.js +4 -4
- package/dist/sdk/cli-contracts/completeness.js +9 -3
- package/dist/sdk/cli-contracts/enum-contracts.d.ts +1 -1
- package/dist/sdk/cli-contracts/flag-contracts.js +6 -3
- package/dist/sdk/cli-contracts/runtime-contracts.d.ts +6 -1
- package/dist/sdk/cli-contracts/runtime-contracts.js +155 -60
- package/dist/sdk/cli-contracts/schemas/assurance-definition.d.ts +32 -0
- package/dist/sdk/cli-contracts/schemas/assurance-definition.js +25 -2
- package/dist/sdk/cli-contracts/tool-parameter-tables.js +35 -5
- package/dist/sdk/cli-contracts/tool-schema.js +17 -2
- package/dist/sdk/cli-program.js +3 -3
- package/dist/sdk/comments.js +4 -3
- package/dist/sdk/compile-cache.js +4 -3
- package/dist/sdk/compose.d.ts +3 -1
- package/dist/sdk/compose.js +16 -2
- package/dist/sdk/context-relevance.js +12 -4
- package/dist/sdk/context-signal-store.js +4 -3
- package/dist/sdk/extension/install-runtime.js +4 -4
- package/dist/sdk/extension/managed-state.js +4 -3
- package/dist/sdk/extension-contracts.d.ts +1 -1
- package/dist/sdk/flag-invocation-contracts.js +5 -6
- package/dist/sdk/generated/generated-error-code-catalog-part-1.js +14 -2
- package/dist/sdk/governance/assurance-action.d.ts +27 -3
- package/dist/sdk/governance/assurance-action.js +112 -28
- package/dist/sdk/governance/assurance-mutation-error.d.ts +7 -0
- package/dist/sdk/governance/assurance-mutation-error.js +25 -2
- package/dist/sdk/governance/assurance-presets.d.ts +54 -0
- package/dist/sdk/governance/assurance-presets.js +136 -0
- package/dist/sdk/governance/assurance-runtime.d.ts +4 -0
- package/dist/sdk/governance/assurance-runtime.js +109 -8
- package/dist/sdk/governance/assurance.d.ts +39 -0
- package/dist/sdk/governance/assurance.js +145 -6
- package/dist/sdk/governance/gc.js +5 -11
- package/dist/sdk/governance/health.js +18 -6
- package/dist/sdk/governance/validate-history-drift.d.ts +7 -0
- package/dist/sdk/governance/validate-history-drift.js +68 -0
- package/dist/sdk/governance/validate.js +6 -48
- package/dist/sdk/graph/assembly.d.ts +23 -0
- package/dist/sdk/graph/assembly.js +54 -2
- package/dist/sdk/graph/durable-cache.js +91 -3
- package/dist/sdk/graph/governance-contracts.d.ts +12 -0
- package/dist/sdk/graph/governance-contracts.js +29 -0
- package/dist/sdk/graph/governance.d.ts +51 -3
- package/dist/sdk/graph/governance.js +216 -35
- package/dist/sdk/graph/index.d.ts +2 -2
- package/dist/sdk/graph/index.js +4 -4
- package/dist/sdk/graph/mutation-advisory.js +15 -7
- package/dist/sdk/graph/remediation.js +12 -2
- package/dist/sdk/guide.js +4 -4
- package/dist/sdk/history-read.d.ts +8 -0
- package/dist/sdk/history-read.js +2 -2
- package/dist/sdk/index.d.ts +3 -2
- package/dist/sdk/index.js +5 -4
- package/dist/sdk/linked-artifacts.d.ts +2 -0
- package/dist/sdk/linked-artifacts.js +29 -4
- package/dist/sdk/merge/install.js +5 -4
- package/dist/sdk/query/activity.js +4 -3
- package/dist/sdk/query/history.js +20 -3
- package/dist/sdk/read-output-budget.d.ts +2 -0
- package/dist/sdk/read-output-budget.js +12 -5
- package/dist/sdk/read-output-contracts.d.ts +61 -5
- package/dist/sdk/read-output-contracts.js +210 -28
- package/dist/sdk/read-output-rows.d.ts +11 -0
- package/dist/sdk/read-output-rows.js +47 -2
- package/dist/sdk/relationship-history.js +5 -5
- package/dist/sdk/runtime-primitives.d.ts +1 -1
- package/dist/sdk/runtime-primitives.js +3 -3
- package/dist/sdk/runtime.js +5 -3
- package/dist/sdk/schema-migration.js +4 -4
- package/dist/sdk/test/execution.js +4 -4
- package/dist/sdk/workspace-memory.js +4 -4
- package/dist/sdk/workspace-snapshot.js +8 -14
- package/dist/sdk/workspace-transaction-gc.js +4 -9
- package/dist/sdk/workspace-transaction.js +4 -4
- package/dist/sdk/workspace.js +4 -3
- package/docs/ASSURANCE.md +163 -3
- package/docs/COMMANDS.md +31 -2
- package/docs/CONFIGURATION.md +2 -0
- package/docs/EXTENSIONS.md +11 -12
- package/docs/READ_OUTPUT_CONTRACTS.md +44 -8
- package/docs/RELATIONSHIP_GRAPH.md +34 -4
- package/docs/SDK.md +29 -8
- package/docs/SDK_CONTEXT_INTEGRITY.md +32 -1
- package/docs/TESTING.md +47 -16
- package/docs/performance/sdk-entrypoint-import-costs.md +20 -13
- package/marketplace.json +2 -2
- package/package.json +6 -6
- package/packages/pm-beads/extensions/beads/index.ts +1 -0
- package/packages/pm-beads/package.json +1 -1
- package/packages/pm-calendar/extensions/calendar/index.ts +1 -0
- package/packages/pm-calendar/package.json +1 -1
- package/packages/pm-command-kit/package.json +1 -1
- package/packages/pm-digital-twin/extensions/digital-twin/index.ts +7 -0
- package/packages/pm-digital-twin/package.json +1 -1
- package/packages/pm-governance-audit/extensions/governance-audit/index.ts +4 -0
- package/packages/pm-governance-audit/package.json +1 -1
- package/packages/pm-guide-shell/extensions/guide-shell/index.ts +5 -0
- 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/extensions/linked-test-adapters/index.ts +6 -0
- package/packages/pm-linked-test-adapters/package.json +1 -1
- package/packages/pm-search-advanced/extensions/search-advanced/index.ts +2 -0
- package/packages/pm-search-advanced/package.json +1 -1
- package/packages/pm-templates/extensions/templates/index.ts +4 -0
- package/packages/pm-templates/package.json +1 -1
- package/packages/pm-todos/extensions/todos/index.ts +2 -0
- package/packages/pm-todos/package.json +1 -1
- package/packages/pm-vcs/extensions/vcs/index.ts +7 -0
- 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/scripts/bundle-cli.mjs +17 -4
- package/sdk/public-surface.json +390 -60
- package/dist/cli-bundle/chunks/chunk-3PS7X6GS.js +0 -8
- package/dist/cli-bundle/chunks/chunk-7KL6KMWF.js +0 -13
- package/dist/cli-bundle/chunks/chunk-D55A5AEN.js +0 -33
- package/dist/cli-bundle/chunks/chunk-KIKDWECH.js +0 -196
- package/dist/cli-bundle/chunks/chunk-R76342NV.js +0 -2
- package/dist/cli-bundle/chunks/chunk-RBTK5EZV.js +0 -5
- package/dist/cli-bundle/chunks/chunk-VLUQOIDT.js +0 -2
- package/dist/cli-bundle/chunks/register-operations-2OGISOVR.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-2DALMVF7.js +0 -5
- package/dist/cli-bundle/focused-chunks/chunk-5OQIMELN.js +0 -155
- package/dist/cli-bundle/focused-chunks/chunk-5TAGTDRE.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-ECADZMVC.js +0 -18
- package/dist/cli-bundle/focused-chunks/chunk-EJGXRA6B.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-FFGSZCPK.js +0 -16
- package/dist/cli-bundle/focused-chunks/chunk-ISKOBJCM.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-KPSXVBDV.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-KTLOKKCQ.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-LGYK3NUF.js +0 -8
- package/dist/cli-bundle/focused-chunks/chunk-NBLUKITQ.js +0 -4
- package/dist/cli-bundle/focused-chunks/chunk-NYLOUPOB.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-Q4T463SD.js +0 -31
- package/dist/cli-bundle/focused-chunks/chunk-QVZVY53N.js +0 -2
- package/dist/cli-bundle/focused-chunks/chunk-UTEWXDEQ.js +0 -17
package/dist/core/fs/fs-utils.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides filesystem helpers for Fs Utils.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="23fc5fcb-5c80-57ab-8d91-8d15ee35ab53")}catch(e){}}();
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import crypto from "node:crypto";
|
|
@@ -33,7 +33,11 @@ export async function pathExists(targetPath) {
|
|
|
33
33
|
* runtime fault instead of reaching the caller's own root validation.
|
|
34
34
|
*/
|
|
35
35
|
export function isFileAbsentError(error) {
|
|
36
|
-
return
|
|
36
|
+
return isFileMissingError(error) || isErrno(error, "ENOTDIR");
|
|
37
|
+
}
|
|
38
|
+
/** Report an exact missing-entry race while preserving ENOTDIR as structural corruption. */
|
|
39
|
+
export function isFileMissingError(error) {
|
|
40
|
+
return isErrno(error, "ENOENT");
|
|
37
41
|
}
|
|
38
42
|
/** Implements read file if exists for the public runtime surface of this module. */
|
|
39
43
|
export async function readFileIfExists(targetPath) {
|
|
@@ -152,4 +156,4 @@ function isErrno(error, code) {
|
|
|
152
156
|
error.code === code);
|
|
153
157
|
}
|
|
154
158
|
//# sourceMappingURL=fs-utils.js.map
|
|
155
|
-
//# debugId=
|
|
159
|
+
//# debugId=23fc5fcb-5c80-57ab-8d91-8d15ee35ab53
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Invalidates derived history-chain verification state after committed writes.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
7
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5121b339-6594-58ec-b479-11cbf7dafd27")}catch(e){}}();
|
|
8
|
+
import { isFileMissingError } from "../fs/fs-utils.js";
|
|
8
9
|
import fs from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { writeStderr } from "../output/output.js";
|
|
@@ -49,7 +50,7 @@ export async function invalidateHistoryDriftCache(pmRoot) {
|
|
|
49
50
|
}
|
|
50
51
|
catch (error) {
|
|
51
52
|
const errorCode = getFilesystemErrorCode(error);
|
|
52
|
-
if (
|
|
53
|
+
if (isFileMissingError(error)) {
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
const code = errorCode === undefined
|
|
@@ -69,4 +70,4 @@ export async function invalidateHistoryDriftCacheForPath(historyPath) {
|
|
|
69
70
|
await invalidateHistoryDriftCache(path.dirname(historyDirectory));
|
|
70
71
|
}
|
|
71
72
|
//# sourceMappingURL=drift-cache.js.map
|
|
72
|
-
//# debugId=
|
|
73
|
+
//# debugId=5121b339-6594-58ec-b479-11cbf7dafd27
|
|
@@ -11,6 +11,12 @@ export interface DriftScanResult {
|
|
|
11
11
|
chainMismatches: string[];
|
|
12
12
|
/** Value that configures or reports drifted items for this contract. */
|
|
13
13
|
driftedItems: string[];
|
|
14
|
+
/** Governed singleton files whose JSON differs from workspace-history replay. */
|
|
15
|
+
workspaceStateMismatches: string[];
|
|
16
|
+
/** Governed singleton files absent from disk. */
|
|
17
|
+
workspaceStateMissing: string[];
|
|
18
|
+
/** Governed singleton files unreadable as safe workspace-local JSON. */
|
|
19
|
+
workspaceStateUnreadable: string[];
|
|
14
20
|
}
|
|
15
21
|
/** Controls how cached history stream verification is trusted when the file stat tuple still matches a previous scan. */
|
|
16
22
|
export type DriftCacheHitVerification = "content_hash" | "metadata";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
|
|
2
|
-
!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]="
|
|
2
|
+
!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]="32538934-2cbb-50fe-b721-ad9197cef7d8")}catch(e){}}();
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { getHistoryPath } from "../store/paths.js";
|
|
7
|
-
import { writeFileAtomic } from "../fs/fs-utils.js";
|
|
7
|
+
import { isFileMissingError, writeFileAtomic } from "../fs/fs-utils.js";
|
|
8
8
|
import { CURRENT_HISTORY_ITEM_HASH_VERSION, hashDocumentForVersion, } from "./history.js";
|
|
9
9
|
import { verifyHistoryChainWithVersion } from "./replay.js";
|
|
10
|
-
import { getWorkspaceHistoryPath, WORKSPACE_HISTORY_ID, } from "./workspace-history.js";
|
|
10
|
+
import { getWorkspaceHistoryPath, inspectWorkspaceHistoryState, WORKSPACE_HISTORY_ID, } from "./workspace-history.js";
|
|
11
11
|
const DRIFT_CACHE_VERSION = 4;
|
|
12
12
|
const DRIFT_CACHE_FILENAME = "history-drift-cache.json";
|
|
13
13
|
function getDriftCachePath(pmRoot) {
|
|
@@ -28,12 +28,6 @@ async function loadDriftCache(pmRoot) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
function isErrno(error, code) {
|
|
32
|
-
return (typeof error === "object" &&
|
|
33
|
-
error !== null &&
|
|
34
|
-
"code" in error &&
|
|
35
|
-
error.code === code);
|
|
36
|
-
}
|
|
37
31
|
async function scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verifyCacheHitByContent, accumulator) {
|
|
38
32
|
const historyPath = getWorkspaceHistoryPath(pmRoot);
|
|
39
33
|
let stat;
|
|
@@ -41,7 +35,7 @@ async function scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verify
|
|
|
41
35
|
stat = await fs.stat(historyPath);
|
|
42
36
|
}
|
|
43
37
|
catch (error) {
|
|
44
|
-
if (
|
|
38
|
+
if (isFileMissingError(error))
|
|
45
39
|
return false;
|
|
46
40
|
accumulator.unreadableStreams.push(WORKSPACE_HISTORY_ID);
|
|
47
41
|
return false;
|
|
@@ -179,6 +173,61 @@ async function resolveStreamVerification(params) {
|
|
|
179
173
|
cacheDirty: true,
|
|
180
174
|
};
|
|
181
175
|
}
|
|
176
|
+
/** Scan one item stream and return whether its cache entry needs persistence. */
|
|
177
|
+
async function scanItemHistory(pmRoot, item, cache, accumulator) {
|
|
178
|
+
const historyPath = getHistoryPath(pmRoot, item.id);
|
|
179
|
+
let stat;
|
|
180
|
+
try {
|
|
181
|
+
stat = await fs.stat(historyPath);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
(isFileMissingError(error)
|
|
185
|
+
? accumulator.missingStreams
|
|
186
|
+
: accumulator.unreadableStreams).push(item.id);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
const resolved = await resolveStreamVerification({
|
|
190
|
+
itemId: item.id,
|
|
191
|
+
historyPath,
|
|
192
|
+
stat,
|
|
193
|
+
cached: cache.previousEntries[item.id],
|
|
194
|
+
verifyCacheHitByContent: cache.verifyCacheHitByContent,
|
|
195
|
+
accumulator,
|
|
196
|
+
});
|
|
197
|
+
if (!resolved.verification)
|
|
198
|
+
return resolved.cacheDirty;
|
|
199
|
+
if (!resolved.verification.chainOk)
|
|
200
|
+
accumulator.chainMismatches.push(item.id);
|
|
201
|
+
cache.nextEntries[item.id] = {
|
|
202
|
+
mtime_ms: stat.mtimeMs,
|
|
203
|
+
ctime_ms: stat.ctimeMs,
|
|
204
|
+
size: stat.size,
|
|
205
|
+
content_hash: resolved.verification.contentHash,
|
|
206
|
+
latest_after_hash: resolved.verification.latestAfterHash,
|
|
207
|
+
chain_ok: resolved.verification.chainOk,
|
|
208
|
+
item_hash_version: resolved.verification.itemHashVersion,
|
|
209
|
+
};
|
|
210
|
+
const { body, ...itemMetadata } = item;
|
|
211
|
+
const currentHash = hashDocumentForVersion({ metadata: itemMetadata, body }, resolved.verification.itemHashVersion);
|
|
212
|
+
if (currentHash !== resolved.verification.latestAfterHash)
|
|
213
|
+
accumulator.hashMismatches.push(item.id);
|
|
214
|
+
return resolved.cacheDirty;
|
|
215
|
+
}
|
|
216
|
+
/** Compare the verified workspace stream with every governed singleton file. */
|
|
217
|
+
async function scanWorkspaceStateAgreement(pmRoot, accumulator) {
|
|
218
|
+
if (accumulator.unreadableStreams.includes(WORKSPACE_HISTORY_ID) ||
|
|
219
|
+
accumulator.chainMismatches.includes(WORKSPACE_HISTORY_ID))
|
|
220
|
+
return;
|
|
221
|
+
try {
|
|
222
|
+
const agreement = await inspectWorkspaceHistoryState(pmRoot);
|
|
223
|
+
accumulator.workspaceStateMismatches.push(...agreement.mismatched_documents);
|
|
224
|
+
accumulator.workspaceStateMissing.push(...agreement.missing_documents);
|
|
225
|
+
accumulator.workspaceStateUnreadable.push(...agreement.unreadable_documents);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
accumulator.unreadableStreams.push(WORKSPACE_HISTORY_ID);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
182
231
|
/**
|
|
183
232
|
* Scan every item's history stream for drift (missing/unreadable streams, broken
|
|
184
233
|
* hash chains, and item/history hash mismatches).
|
|
@@ -195,6 +244,9 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
|
|
|
195
244
|
unreadableStreams: [],
|
|
196
245
|
hashMismatches: [],
|
|
197
246
|
chainMismatches: [],
|
|
247
|
+
workspaceStateMismatches: [],
|
|
248
|
+
workspaceStateMissing: [],
|
|
249
|
+
workspaceStateUnreadable: [],
|
|
198
250
|
};
|
|
199
251
|
const cache = await loadDriftCache(pmRoot);
|
|
200
252
|
const previousEntries = cache?.entries ?? {};
|
|
@@ -203,57 +255,18 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
|
|
|
203
255
|
// Metadata mode does not open a stat-matched stream, so unreadable-after-stat
|
|
204
256
|
// failures are intentionally deferred to strict validate/history-repair scans.
|
|
205
257
|
const verifyCacheHitByContent = options.cacheHitVerification !== "metadata";
|
|
258
|
+
const cacheState = {
|
|
259
|
+
previousEntries,
|
|
260
|
+
nextEntries,
|
|
261
|
+
verifyCacheHitByContent,
|
|
262
|
+
};
|
|
206
263
|
for (const item of items) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
stat = await fs.stat(historyPath);
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
if (isErrno(error, "ENOENT")) {
|
|
214
|
-
accumulator.missingStreams.push(item.id);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
accumulator.unreadableStreams.push(item.id);
|
|
218
|
-
}
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
const cached = previousEntries[item.id];
|
|
222
|
-
const resolved = await resolveStreamVerification({
|
|
223
|
-
itemId: item.id,
|
|
224
|
-
historyPath,
|
|
225
|
-
stat,
|
|
226
|
-
cached,
|
|
227
|
-
verifyCacheHitByContent,
|
|
228
|
-
accumulator,
|
|
229
|
-
});
|
|
230
|
-
cacheDirty ||= resolved.cacheDirty;
|
|
231
|
-
if (!resolved.verification) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (!resolved.verification.chainOk) {
|
|
235
|
-
accumulator.chainMismatches.push(item.id);
|
|
236
|
-
}
|
|
237
|
-
nextEntries[item.id] = {
|
|
238
|
-
mtime_ms: stat.mtimeMs,
|
|
239
|
-
ctime_ms: stat.ctimeMs,
|
|
240
|
-
size: stat.size,
|
|
241
|
-
content_hash: resolved.verification.contentHash,
|
|
242
|
-
latest_after_hash: resolved.verification.latestAfterHash,
|
|
243
|
-
chain_ok: resolved.verification.chainOk,
|
|
244
|
-
item_hash_version: resolved.verification.itemHashVersion,
|
|
245
|
-
};
|
|
246
|
-
const { body, ...itemMetadata } = item;
|
|
247
|
-
const currentHash = hashDocumentForVersion({
|
|
248
|
-
metadata: itemMetadata,
|
|
249
|
-
body,
|
|
250
|
-
}, resolved.verification.itemHashVersion);
|
|
251
|
-
if (currentHash !== resolved.verification.latestAfterHash) {
|
|
252
|
-
accumulator.hashMismatches.push(item.id);
|
|
253
|
-
}
|
|
264
|
+
if (await scanItemHistory(pmRoot, item, cacheState, accumulator))
|
|
265
|
+
cacheDirty = true;
|
|
254
266
|
}
|
|
255
267
|
const workspaceCacheDirty = await scanWorkspaceHistory(pmRoot, previousEntries, nextEntries, verifyCacheHitByContent, accumulator);
|
|
256
268
|
cacheDirty ||= workspaceCacheDirty;
|
|
269
|
+
await scanWorkspaceStateAgreement(pmRoot, accumulator);
|
|
257
270
|
if (cacheDirty ||
|
|
258
271
|
Object.keys(previousEntries).length !== Object.keys(nextEntries).length) {
|
|
259
272
|
const cachePath = getDriftCachePath(pmRoot);
|
|
@@ -271,9 +284,14 @@ export async function scanHistoryDrift(pmRoot, items, options = {}) {
|
|
|
271
284
|
...accumulator.unreadableStreams,
|
|
272
285
|
...accumulator.hashMismatches,
|
|
273
286
|
...accumulator.chainMismatches,
|
|
287
|
+
...(accumulator.workspaceStateMismatches.length > 0 ||
|
|
288
|
+
accumulator.workspaceStateMissing.length > 0 ||
|
|
289
|
+
accumulator.workspaceStateUnreadable.length > 0
|
|
290
|
+
? [WORKSPACE_HISTORY_ID]
|
|
291
|
+
: []),
|
|
274
292
|
]),
|
|
275
293
|
].sort((a, b) => a.localeCompare(b));
|
|
276
294
|
return { ...accumulator, driftedItems };
|
|
277
295
|
}
|
|
278
296
|
//# sourceMappingURL=drift-scan.js.map
|
|
279
|
-
//# debugId=
|
|
297
|
+
//# debugId=32538934-2cbb-50fe-b721-ad9197cef7d8
|
|
@@ -76,6 +76,47 @@ export interface WorkspaceAuditEventOptions {
|
|
|
76
76
|
/** Maximum lock wait in milliseconds. */
|
|
77
77
|
lockWaitMs: number;
|
|
78
78
|
}
|
|
79
|
+
/** Agreement between the replayed workspace stream and governed singleton files. */
|
|
80
|
+
export interface WorkspaceHistoryStateAgreement {
|
|
81
|
+
/** Whether every governed singleton is readable and equals its replayed value. */
|
|
82
|
+
ok: boolean;
|
|
83
|
+
/** Number of singleton documents represented by the latest replayed state. */
|
|
84
|
+
document_count: number;
|
|
85
|
+
/** Governed documents whose on-disk JSON equals the replayed value. */
|
|
86
|
+
matching_documents: string[];
|
|
87
|
+
/** Governed documents whose on-disk JSON differs from the replayed value. */
|
|
88
|
+
mismatched_documents: string[];
|
|
89
|
+
/** Governed documents absent from disk. */
|
|
90
|
+
missing_documents: string[];
|
|
91
|
+
/** Governed documents that cannot be read as safe workspace-local JSON. */
|
|
92
|
+
unreadable_documents: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Authorized append-only reconciliation of one out-of-band singleton state. */
|
|
95
|
+
export interface WorkspaceJsonReconciliationOptions extends Omit<WorkspaceJsonWriteOptions, "raw" | "recordCreation"> {
|
|
96
|
+
/** Closed Decision item authorizing acceptance of the on-disk state. */
|
|
97
|
+
authorizationDecision: string;
|
|
98
|
+
}
|
|
99
|
+
/** Restore one governed singleton from a recorded workspace history version. */
|
|
100
|
+
export interface WorkspaceJsonRestoreOptions extends Omit<WorkspaceJsonWriteOptions, "raw" | "recordCreation"> {
|
|
101
|
+
/** One-based workspace history version whose document value is restored. */
|
|
102
|
+
targetVersion: number;
|
|
103
|
+
}
|
|
104
|
+
/** Receipt returned after a singleton is restored from workspace history. */
|
|
105
|
+
export interface WorkspaceJsonRestoreResult {
|
|
106
|
+
/** Tracker-relative singleton identity. */
|
|
107
|
+
document_path: string;
|
|
108
|
+
/** One-based workspace history version used as the restore source. */
|
|
109
|
+
restored_from_version: number;
|
|
110
|
+
/** Whether the on-disk singleton and latest history state changed. */
|
|
111
|
+
changed: boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Compare every singleton represented by the verified workspace stream with
|
|
115
|
+
* its current on-disk JSON. Hash-chain verification proves recorded ordering;
|
|
116
|
+
* this state-agreement check separately proves that every current singleton
|
|
117
|
+
* mutation is represented by that chain.
|
|
118
|
+
*/
|
|
119
|
+
export declare function inspectWorkspaceHistoryState(pmRoot: string): Promise<WorkspaceHistoryStateAgreement>;
|
|
79
120
|
/**
|
|
80
121
|
* Append one workspace document mutation under the dedicated workspace-history
|
|
81
122
|
* lock. Existing history is verified and replayed before the new entry is
|
|
@@ -103,3 +144,21 @@ export declare function mutateWorkspaceJsonWithHistory<Result>(params: Workspace
|
|
|
103
144
|
* under the workspace-history lock, with compensation if history append fails.
|
|
104
145
|
*/
|
|
105
146
|
export declare function writeWorkspaceJsonWithHistory(params: WorkspaceJsonWriteOptions): Promise<boolean>;
|
|
147
|
+
/**
|
|
148
|
+
* Append an authorized reconciliation event that adopts a reviewed on-disk
|
|
149
|
+
* singleton after an out-of-band change. The existing stream is never edited:
|
|
150
|
+
* its latest replayed state becomes the event before-state and the reviewed
|
|
151
|
+
* singleton becomes the after-state, with the authorizing Decision recorded in
|
|
152
|
+
* structured context.
|
|
153
|
+
*/
|
|
154
|
+
export declare function reconcileWorkspaceJsonHistory(options: WorkspaceJsonReconciliationOptions): Promise<{
|
|
155
|
+
changed: boolean;
|
|
156
|
+
entry?: HistoryEntry;
|
|
157
|
+
historyPath: string;
|
|
158
|
+
}>;
|
|
159
|
+
/**
|
|
160
|
+
* Restore one governed JSON singleton to the value recorded at a one-based
|
|
161
|
+
* workspace history version, then append the restore as a new history state so
|
|
162
|
+
* replay remains forward-only and independently verifiable.
|
|
163
|
+
*/
|
|
164
|
+
export declare function restoreWorkspaceJsonFromHistory(options: WorkspaceJsonRestoreOptions): Promise<WorkspaceJsonRestoreResult>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* HistoryEntry format used by item mutations.
|
|
6
6
|
*/
|
|
7
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]="
|
|
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]="0f666995-78f8-5741-ae0c-0d792feea599")}catch(e){}}();
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { readFileIfExists, writeFileAtomic } from "../fs/fs-utils.js";
|
|
@@ -37,14 +37,83 @@ function workspaceDocument(documents, timestamp) {
|
|
|
37
37
|
};
|
|
38
38
|
return { metadata, body: "" };
|
|
39
39
|
}
|
|
40
|
-
function replayWorkspaceEntries(entries) {
|
|
40
|
+
function replayWorkspaceEntries(entries, count = entries.length) {
|
|
41
41
|
let replay = cloneEmptyReplayDocument();
|
|
42
|
-
for (const entry of entries) {
|
|
42
|
+
for (const entry of entries.slice(0, count)) {
|
|
43
43
|
const applied = tryApplyReplayPatch(replay, entry.patch);
|
|
44
44
|
replay = applied.document;
|
|
45
45
|
}
|
|
46
46
|
return replayToItemDocument(replay);
|
|
47
47
|
}
|
|
48
|
+
/** Read the governed document map from one replayed workspace document. */
|
|
49
|
+
function workspaceDocuments(document) {
|
|
50
|
+
return "documents" in document.metadata &&
|
|
51
|
+
typeof document.metadata.documents === "object" &&
|
|
52
|
+
document.metadata.documents !== null &&
|
|
53
|
+
!Array.isArray(document.metadata.documents)
|
|
54
|
+
? document.metadata.documents
|
|
55
|
+
: {};
|
|
56
|
+
}
|
|
57
|
+
/** Resolve a governed singleton path and reject paths outside the tracker root. */
|
|
58
|
+
function resolveGovernedDocumentPath(pmRoot, filePath) {
|
|
59
|
+
const absolutePath = path.resolve(filePath);
|
|
60
|
+
const documentPath = path.relative(pmRoot, absolutePath).replaceAll("\\", "/");
|
|
61
|
+
if (documentPath.length === 0 ||
|
|
62
|
+
documentPath === ".." ||
|
|
63
|
+
documentPath.startsWith("../") ||
|
|
64
|
+
path.isAbsolute(documentPath)) {
|
|
65
|
+
throw new TypeError("Workspace history document path must stay inside the tracker root");
|
|
66
|
+
}
|
|
67
|
+
return { documentPath, absolutePath };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Compare every singleton represented by the verified workspace stream with
|
|
71
|
+
* its current on-disk JSON. Hash-chain verification proves recorded ordering;
|
|
72
|
+
* this state-agreement check separately proves that every current singleton
|
|
73
|
+
* mutation is represented by that chain.
|
|
74
|
+
*/
|
|
75
|
+
export async function inspectWorkspaceHistoryState(pmRoot) {
|
|
76
|
+
const entries = await readHistoryEntries(getWorkspaceHistoryPath(pmRoot), WORKSPACE_HISTORY_ID);
|
|
77
|
+
const verification = verifyHistoryChain(entries);
|
|
78
|
+
if (!verification.ok) {
|
|
79
|
+
throw new TypeError(`Workspace history verification failed: ${verification.errors.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
const documents = workspaceDocuments(entries.length === 0
|
|
82
|
+
? EMPTY_CANONICAL_DOCUMENT
|
|
83
|
+
: replayWorkspaceEntries(entries));
|
|
84
|
+
const matching = [];
|
|
85
|
+
const mismatched = [];
|
|
86
|
+
const missing = [];
|
|
87
|
+
const unreadable = [];
|
|
88
|
+
for (const documentPath of Object.keys(documents).sort()) {
|
|
89
|
+
let raw;
|
|
90
|
+
try {
|
|
91
|
+
const resolved = resolveGovernedDocumentPath(pmRoot, path.resolve(pmRoot, documentPath));
|
|
92
|
+
raw = await readFileIfExists(resolved.absolutePath);
|
|
93
|
+
if (raw === null) {
|
|
94
|
+
missing.push(documentPath);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const current = JSON.parse(raw);
|
|
98
|
+
(stableStringify(current) === stableStringify(documents[documentPath])
|
|
99
|
+
? matching
|
|
100
|
+
: mismatched).push(documentPath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
unreadable.push(documentPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
ok: mismatched.length === 0 &&
|
|
108
|
+
missing.length === 0 &&
|
|
109
|
+
unreadable.length === 0,
|
|
110
|
+
document_count: Object.keys(documents).length,
|
|
111
|
+
matching_documents: matching,
|
|
112
|
+
mismatched_documents: mismatched,
|
|
113
|
+
missing_documents: missing,
|
|
114
|
+
unreadable_documents: unreadable,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
48
117
|
/**
|
|
49
118
|
* Append one workspace document mutation while the caller holds the dedicated
|
|
50
119
|
* workspace-history lock.
|
|
@@ -66,12 +135,7 @@ async function appendWorkspaceHistoryChangeLocked(change) {
|
|
|
66
135
|
const beforeDocument = entries.length === 0
|
|
67
136
|
? EMPTY_CANONICAL_DOCUMENT
|
|
68
137
|
: replayWorkspaceEntries(entries);
|
|
69
|
-
const priorDocuments =
|
|
70
|
-
typeof beforeDocument.metadata.documents === "object" &&
|
|
71
|
-
beforeDocument.metadata.documents !== null &&
|
|
72
|
-
!Array.isArray(beforeDocument.metadata.documents)
|
|
73
|
-
? beforeDocument.metadata.documents
|
|
74
|
-
: {};
|
|
138
|
+
const priorDocuments = workspaceDocuments(beforeDocument);
|
|
75
139
|
const recordedBefore = priorDocuments[change.documentPath];
|
|
76
140
|
if (recordedBefore !== undefined &&
|
|
77
141
|
stableStringify(recordedBefore) !== stableStringify(change.before)) {
|
|
@@ -220,5 +284,128 @@ export async function writeWorkspaceJsonWithHistory(params) {
|
|
|
220
284
|
await release();
|
|
221
285
|
}
|
|
222
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Append an authorized reconciliation event that adopts a reviewed on-disk
|
|
289
|
+
* singleton after an out-of-band change. The existing stream is never edited:
|
|
290
|
+
* its latest replayed state becomes the event before-state and the reviewed
|
|
291
|
+
* singleton becomes the after-state, with the authorizing Decision recorded in
|
|
292
|
+
* structured context.
|
|
293
|
+
*/
|
|
294
|
+
export async function reconcileWorkspaceJsonHistory(options) {
|
|
295
|
+
const authorizationDecision = options.authorizationDecision.trim();
|
|
296
|
+
if (!authorizationDecision) {
|
|
297
|
+
throw new TypeError("Workspace history reconciliation requires an authorization decision");
|
|
298
|
+
}
|
|
299
|
+
const release = await acquireLock(options.pmRoot, "workspace-history", options.lockTtlSeconds, options.author, false, false, options.lockWaitMs);
|
|
300
|
+
try {
|
|
301
|
+
const { documentPath, absolutePath } = resolveGovernedDocumentPath(options.pmRoot, options.filePath);
|
|
302
|
+
const historyPath = getWorkspaceHistoryPath(options.pmRoot);
|
|
303
|
+
const entries = await readHistoryEntries(historyPath, WORKSPACE_HISTORY_ID);
|
|
304
|
+
const verification = verifyHistoryChain(entries);
|
|
305
|
+
if (!verification.ok) {
|
|
306
|
+
throw new TypeError(`Workspace history verification failed: ${verification.errors.join(", ")}`);
|
|
307
|
+
}
|
|
308
|
+
const beforeDocument = replayWorkspaceEntries(entries);
|
|
309
|
+
const priorDocuments = workspaceDocuments(beforeDocument);
|
|
310
|
+
if (!Object.hasOwn(priorDocuments, documentPath)) {
|
|
311
|
+
throw new TypeError(`Workspace history does not govern document ${documentPath}`);
|
|
312
|
+
}
|
|
313
|
+
const raw = await readFileIfExists(absolutePath);
|
|
314
|
+
if (raw === null) {
|
|
315
|
+
throw new TypeError(`Workspace history document is missing: ${documentPath}`);
|
|
316
|
+
}
|
|
317
|
+
const current = JSON.parse(raw);
|
|
318
|
+
if (stableStringify(priorDocuments[documentPath]) === stableStringify(current)) {
|
|
319
|
+
return { changed: false, historyPath };
|
|
320
|
+
}
|
|
321
|
+
const timestamp = new Date().toISOString();
|
|
322
|
+
const entry = createHistoryEntry({
|
|
323
|
+
nowIso: timestamp,
|
|
324
|
+
author: options.author,
|
|
325
|
+
op: options.op,
|
|
326
|
+
before: beforeDocument,
|
|
327
|
+
after: workspaceDocument({ ...priorDocuments, [documentPath]: current }, beforeDocument.metadata.created_at),
|
|
328
|
+
message: options.message,
|
|
329
|
+
context: {
|
|
330
|
+
reason: "out_of_band_workspace_state_reconciliation",
|
|
331
|
+
document_path: documentPath,
|
|
332
|
+
authorization_decision: authorizationDecision,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
await appendHistoryEntry(historyPath, entry);
|
|
336
|
+
return { changed: true, entry, historyPath };
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
await release();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Restore one governed JSON singleton to the value recorded at a one-based
|
|
344
|
+
* workspace history version, then append the restore as a new history state so
|
|
345
|
+
* replay remains forward-only and independently verifiable.
|
|
346
|
+
*/
|
|
347
|
+
export async function restoreWorkspaceJsonFromHistory(options) {
|
|
348
|
+
const release = await acquireLock(options.pmRoot, "workspace-history", options.lockTtlSeconds, options.author, false, false, options.lockWaitMs);
|
|
349
|
+
try {
|
|
350
|
+
const { documentPath, absolutePath } = resolveGovernedDocumentPath(options.pmRoot, options.filePath);
|
|
351
|
+
const entries = await readHistoryEntries(getWorkspaceHistoryPath(options.pmRoot), WORKSPACE_HISTORY_ID);
|
|
352
|
+
if (!Number.isInteger(options.targetVersion) ||
|
|
353
|
+
options.targetVersion < 1 ||
|
|
354
|
+
options.targetVersion > entries.length) {
|
|
355
|
+
throw new TypeError(`Invalid workspace history target version: ${String(options.targetVersion)}`);
|
|
356
|
+
}
|
|
357
|
+
const verification = verifyHistoryChain(entries);
|
|
358
|
+
if (!verification.ok) {
|
|
359
|
+
throw new TypeError(`Workspace history verification failed: ${verification.errors.join(", ")}`);
|
|
360
|
+
}
|
|
361
|
+
const target = workspaceDocuments(replayWorkspaceEntries(entries, options.targetVersion))[documentPath];
|
|
362
|
+
if (target === undefined) {
|
|
363
|
+
throw new TypeError(`Workspace history version ${options.targetVersion} does not contain ${documentPath}`);
|
|
364
|
+
}
|
|
365
|
+
const beforeRaw = await readFileIfExists(absolutePath);
|
|
366
|
+
const targetRaw = `${JSON.stringify(target, null, 2)}\n`;
|
|
367
|
+
if (beforeRaw === targetRaw)
|
|
368
|
+
return {
|
|
369
|
+
document_path: documentPath,
|
|
370
|
+
restored_from_version: options.targetVersion,
|
|
371
|
+
changed: false,
|
|
372
|
+
};
|
|
373
|
+
const beforeDocument = replayWorkspaceEntries(entries);
|
|
374
|
+
const timestamp = new Date().toISOString();
|
|
375
|
+
const entry = createHistoryEntry({
|
|
376
|
+
nowIso: timestamp,
|
|
377
|
+
author: options.author,
|
|
378
|
+
op: options.op,
|
|
379
|
+
before: beforeDocument,
|
|
380
|
+
after: workspaceDocument({ ...workspaceDocuments(beforeDocument), [documentPath]: target }, beforeDocument.metadata.created_at),
|
|
381
|
+
message: options.message,
|
|
382
|
+
context: {
|
|
383
|
+
reason: "workspace_state_restore",
|
|
384
|
+
document_path: documentPath,
|
|
385
|
+
restored_from_version: options.targetVersion,
|
|
386
|
+
replaced_out_of_band_state: true,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
await writeFileAtomic(absolutePath, targetRaw);
|
|
390
|
+
try {
|
|
391
|
+
await appendHistoryEntry(getWorkspaceHistoryPath(options.pmRoot), entry);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (beforeRaw === null)
|
|
395
|
+
await fs.rm(absolutePath, { force: true });
|
|
396
|
+
else
|
|
397
|
+
await writeFileAtomic(absolutePath, beforeRaw);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
document_path: documentPath,
|
|
402
|
+
restored_from_version: options.targetVersion,
|
|
403
|
+
changed: true,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
await release();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
223
410
|
//# sourceMappingURL=workspace-history.js.map
|
|
224
|
-
//# debugId=
|
|
411
|
+
//# debugId=0f666995-78f8-5741-ae0c-0d792feea599
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Zero ttl means any lock older than 0 ms is stale. Negative ttl means always stale.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
12
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="79f9ccbd-48d1-5eb3-82ae-3dd62427041c")}catch(e){}}();
|
|
13
|
+
import { isFileMissingError } from "../fs/fs-utils.js";
|
|
13
14
|
import fs from "node:fs/promises";
|
|
14
15
|
import path from "node:path";
|
|
15
16
|
function parseLock(raw) {
|
|
@@ -34,12 +35,6 @@ function parseLock(raw) {
|
|
|
34
35
|
}
|
|
35
36
|
return { id, owner, created_at, ttl_seconds };
|
|
36
37
|
}
|
|
37
|
-
function isErrno(error, code) {
|
|
38
|
-
return (typeof error === "object" &&
|
|
39
|
-
error !== null &&
|
|
40
|
-
"code" in error &&
|
|
41
|
-
error.code === code);
|
|
42
|
-
}
|
|
43
38
|
/** Pure classification of one lock file's raw content against `nowMs`. This is the single staleness/parse policy shared by `pm gc --scope locks` (which acts on it) and the `pm health` locks check (which only reports it). */
|
|
44
39
|
export function classifyLockContent(file, raw, nowMs) {
|
|
45
40
|
const lock = parseLock(raw);
|
|
@@ -108,7 +103,7 @@ export async function scanLockHealth(pmRoot, now) {
|
|
|
108
103
|
dirEntries = rawDir.filter((f) => f.endsWith(".lock")).sort();
|
|
109
104
|
}
|
|
110
105
|
catch (error) {
|
|
111
|
-
if (
|
|
106
|
+
if (isFileMissingError(error)) {
|
|
112
107
|
return scan;
|
|
113
108
|
}
|
|
114
109
|
throw error;
|
|
@@ -119,7 +114,7 @@ export async function scanLockHealth(pmRoot, now) {
|
|
|
119
114
|
raw = await fs.readFile(path.join(locksDir, file), "utf8");
|
|
120
115
|
}
|
|
121
116
|
catch (readError) {
|
|
122
|
-
if (
|
|
117
|
+
if (isFileMissingError(readError)) {
|
|
123
118
|
// Disappeared between readdir and readFile — treat as a ghost; skip quietly.
|
|
124
119
|
continue;
|
|
125
120
|
}
|
|
@@ -159,7 +154,7 @@ export async function runLockGc(pmRoot, options) {
|
|
|
159
154
|
dirEntries = raw.filter((f) => f.endsWith(".lock")).sort();
|
|
160
155
|
}
|
|
161
156
|
catch (error) {
|
|
162
|
-
if (
|
|
157
|
+
if (isFileMissingError(error)) {
|
|
163
158
|
return result;
|
|
164
159
|
}
|
|
165
160
|
throw error;
|
|
@@ -179,7 +174,7 @@ export async function runLockGc(pmRoot, options) {
|
|
|
179
174
|
raw = await fs.readFile(absPath, "utf8");
|
|
180
175
|
}
|
|
181
176
|
catch (readError) {
|
|
182
|
-
if (
|
|
177
|
+
if (isFileMissingError(readError)) {
|
|
183
178
|
// Disappeared between readdir and readFile — treat as a ghost; skip quietly.
|
|
184
179
|
result.scanned -= 1;
|
|
185
180
|
continue;
|
|
@@ -223,7 +218,7 @@ export async function runLockGc(pmRoot, options) {
|
|
|
223
218
|
await fs.unlink(absPath);
|
|
224
219
|
}
|
|
225
220
|
catch (unlinkError) {
|
|
226
|
-
if (!
|
|
221
|
+
if (!isFileMissingError(unlinkError)) {
|
|
227
222
|
result.warnings.push(`lock_remove_failed:${file}`);
|
|
228
223
|
result.retained.push(relPath);
|
|
229
224
|
result.entries.push(classified.entry);
|
|
@@ -243,4 +238,4 @@ export async function runLockGc(pmRoot, options) {
|
|
|
243
238
|
return result;
|
|
244
239
|
}
|
|
245
240
|
//# sourceMappingURL=lock-gc.js.map
|
|
246
|
-
//# debugId=
|
|
241
|
+
//# debugId=79f9ccbd-48d1-5eb3-82ae-3dd62427041c
|