weir-harness 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/lib/client.blackboard-model.js +172 -56
  2. package/lib/client.capability-manager.js +0 -1
  3. package/lib/client.capability-model.js +378 -31
  4. package/lib/client.chain-model.js +80 -0
  5. package/lib/client.disabled-categories-editor.js +22 -0
  6. package/lib/client.edit-lock-maintenance.js +239 -246
  7. package/lib/client.edit-lock-panel.js +110 -0
  8. package/lib/client.hash-edit-model.js +48 -0
  9. package/lib/client.js +267 -1549
  10. package/lib/client.lsp-model.js +92 -0
  11. package/lib/client.lsp-panel.js +11 -16
  12. package/lib/client.lsp-toggle.js +18 -0
  13. package/lib/client.notify-permissions.js +112 -20
  14. package/lib/client.notify-web.js +14 -11
  15. package/lib/client.robash-model.js +26 -0
  16. package/lib/client.settings-page.js +287 -17
  17. package/lib/client.worktree-model.js +225 -0
  18. package/package.json +1 -1
  19. package/src/blackboard/durable.js +12 -0
  20. package/src/blackboard/index.js +2 -2
  21. package/src/blackboard/kernel.js +23 -6
  22. package/src/blackboard/notify.js +10 -3
  23. package/src/blackboard/remote.js +31 -13
  24. package/src/blackboard/storage.js +13 -5
  25. package/src/capabilities/apply-engine.js +189 -101
  26. package/src/capabilities/defaults-transaction.js +136 -1
  27. package/src/capabilities/lifecycle-snapshot.js +83 -204
  28. package/src/capabilities/mcp-admission.js +1 -23
  29. package/src/capabilities/mcp-drain.js +30 -19
  30. package/src/capabilities/mcp-facade.js +23 -3
  31. package/src/capabilities/preset-library.js +247 -0
  32. package/src/capabilities/preset-package.js +212 -1
  33. package/src/capabilities/same-name-policy.js +3 -41
  34. package/src/capabilities/selection-notify.js +324 -21
  35. package/src/capabilities/skill-selection-plugin.js +315 -729
  36. package/src/capabilities/store/checked-contract.d.ts +5 -0
  37. package/src/capabilities/store/fs-adapter.js +52 -0
  38. package/src/capabilities/store/lock.js +86 -0
  39. package/src/capabilities/store/paths.js +5 -0
  40. package/src/capabilities/store/store.js +105 -5
  41. package/src/delegate/group-coordinator.js +1 -17
  42. package/src/delegate/index.js +4 -2
  43. package/src/delegate/rehydrate.js +2 -1
  44. package/src/delegate/robash-guard-core.js +2 -2
  45. package/src/delegate/supervision-mount.js +5 -4
  46. package/src/delegate/tool.js +26 -3
  47. package/src/{shared → delegate}/whitelist-defaults.js +2 -2
  48. package/src/edit-lock/admin-ledger.js +8 -11
  49. package/src/edit-lock/admin-recovery.js +2 -1
  50. package/src/edit-lock/admission.js +12 -2
  51. package/src/edit-lock/binding-guidance.js +28 -10
  52. package/src/edit-lock/binding-machine.js +312 -0
  53. package/src/edit-lock/derived-facts.js +67 -0
  54. package/src/edit-lock/index.js +44 -244
  55. package/src/edit-lock/inspect.js +113 -1
  56. package/src/edit-lock/lifecycle.js +8 -5
  57. package/src/edit-lock/limits.js +61 -0
  58. package/src/edit-lock/maintenance.js +5 -2
  59. package/src/edit-lock/manager.js +6 -2
  60. package/src/edit-lock/operation-history.js +1 -1
  61. package/src/edit-lock/peer-client.js +9 -2
  62. package/src/edit-lock/peer-transport.js +7 -1
  63. package/src/edit-lock/remote.js +11 -4
  64. package/src/edit-lock/state.js +2 -4
  65. package/src/edit-lock/view.js +10 -11
  66. package/src/lsp/admin.js +2 -24
  67. package/src/lsp/index.js +1 -1
  68. package/src/lsp/manager.js +1 -1
  69. package/src/notify/permissions-admin.js +1 -1
  70. package/src/{lsp/child-process.js → process/index.js} +12 -4
  71. package/src/settings/env-admin.js +1 -1
  72. package/src/settings/index.js +1 -1
  73. package/src/settings/sections.js +0 -54
  74. package/src/shared/audit.js +5 -0
  75. package/src/shared/http-json.js +28 -0
  76. package/src/{delegate/audit-readers.js → shared/terminal-evidence.js} +34 -4
  77. package/src/worktree/errors.js +3 -0
  78. package/src/worktree/index.js +1 -2
  79. package/src/worktree/lane-core.js +641 -0
  80. package/src/worktree/lane-views.js +147 -0
  81. package/src/worktree/lane-workflows.js +853 -0
  82. package/src/worktree/lanes.js +24 -1513
  83. package/src/worktree/runner.js +5 -4
  84. package/src/capabilities/content-pin.js +0 -88
  85. package/src/capabilities/content-refresh.js +0 -331
  86. package/src/capabilities/distribution-manifest.js +0 -78
  87. package/src/capabilities/isolated-exec.js +0 -106
  88. package/src/capabilities/publisher.js +0 -129
  89. package/src/capabilities/skill-admission.js +0 -181
  90. package/src/capabilities/update-lifecycle.js +0 -106
  91. package/src/edit-lock/remote-service.js +0 -28
  92. package/src/worktree/contract-guidance.test.js +0 -100
@@ -10,7 +10,9 @@ window.__ModuleLoader__.load({
10
10
  // Edit Lock maintenance panel (settings "Editing" group): the
11
11
  // profile-wide switch status (SAVED vs actually mounted, restart
12
12
  // semantics) plus read-only authority inspection per server-derived
13
- // domain root, over raw fetch ("/api/weir-edit-lock/maintenance/*").
13
+ // domain root, over the composition root's HTTP channel adapter
14
+ // (props.channel: (endpoint, payload?) => Promise<{ ok: true, value } |
15
+ // { ok: false, error }>, never rejecting; "/api/weir-edit-lock/maintenance/*").
14
16
  // The panel never sends a path the server did not list first, and it
15
17
  // offers recovery only while the manager is mounted. The switch itself
16
18
  // field. Style constants are verbatim copies of the settings-page
@@ -28,136 +30,39 @@ window.__ModuleLoader__.load({
28
30
  const chainButtonStyle = { background: "none", border: "none", cursor: "pointer", fontSize: "12px", color: "var(--dsw-alias-label-secondary)", textDecoration: "underline" };
29
31
  const domainRowStyle = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", padding: "6px 0" };
30
32
  const monoStyle = { fontSize: "12px", lineHeight: "16px", fontFamily: "monospace", wordBreak: "break-all" };
31
- // ---- Online administrative recovery (design D4) --------------------
32
- // The SAME scoped confirmation algorithm as the offline ADMIN
33
- // OVERRIDE, implemented client-side so the panel COMPUTES and
34
- // DISPLAYS the digest: canonical JSON (object keys sorted, arrays in
35
- // order, JSON scalar spelling, no whitespace) of
36
- // {root, owner, expectedRevision, operationIds sorted, risk}, SHA-256
37
- // lowercase hex, prefixed "ADMIN OVERRIDE ". The operator never types
38
- // a hash: the click submits the computed confirmation.
39
- const LATE_WRITER_RISK = "Detached historic writers may still modify files after this override.";
40
- /** @param {unknown} value @returns {string} */
41
- function canonicalJson(value) {
42
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
43
- if (value !== null && typeof value === "object") {
44
- const object = /** @type {Record<string, unknown>} */ (value);
45
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
46
- }
47
- return JSON.stringify(value);
48
- }
49
- /** @param {string} text @returns {number[]} */
50
- function utf8Bytes(text) {
51
- /** @type {number[]} */
52
- const bytes = [];
53
- for (let i = 0; i < text.length; i++) {
54
- let code = text.charCodeAt(i);
55
- if (code >= 0xd800 && code <= 0xdbff && i + 1 < text.length) {
56
- const next = text.charCodeAt(i + 1);
57
- if (next >= 0xdc00 && next <= 0xdfff) {
58
- code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
59
- i++;
60
- }
61
- }
62
- if (code < 0x80) bytes.push(code);
63
- else if (code < 0x800) bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
64
- else if (code < 0x10000) bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
65
- else bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
66
- }
67
- return bytes;
68
- }
69
- /** @param {number} x @param {number} n */
70
- function rotr(x, n) { return ((x >>> n) | (x << (32 - n))) >>> 0; }
71
- const SHA256_K = [
72
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
73
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
74
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
75
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
76
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
77
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
78
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
79
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
80
- ];
81
- /** Pure-JS SHA-256 of the UTF-8 encoding, lowercase hex (the client has
82
- * no node:crypto; the chunk test pins this against the server digest).
83
- * @param {string} text @returns {string} */
84
- function sha256Hex(text) {
85
- const bytes = utf8Bytes(text);
86
- const bitLength = bytes.length * 8;
87
- bytes.push(0x80);
88
- while (bytes.length % 64 !== 56) bytes.push(0);
89
- for (let i = 7; i >= 0; i--) bytes.push(Math.floor(bitLength / 2 ** (8 * i)) % 256);
90
- let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a;
91
- let h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
92
- const w = new Array(64).fill(0);
93
- for (let block = 0; block < bytes.length; block += 64) {
94
- for (let i = 0; i < 16; i++) w[i] = ((bytes[block + 4 * i] << 24) | (bytes[block + 4 * i + 1] << 16) | (bytes[block + 4 * i + 2] << 8) | bytes[block + 4 * i + 3]) >>> 0;
95
- for (let i = 16; i < 64; i++) {
96
- const s0 = (rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3)) >>> 0;
97
- const s1 = (rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10)) >>> 0;
98
- w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
99
- }
100
- let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
101
- for (let i = 0; i < 64; i++) {
102
- const S1 = (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0;
103
- const ch = ((e & f) ^ (~e & g)) >>> 0;
104
- const t1 = (h + S1 + ch + SHA256_K[i] + w[i]) >>> 0;
105
- const S0 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0;
106
- const maj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
107
- const t2 = (S0 + maj) >>> 0;
108
- h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
109
- }
110
- h0 = (h0 + a) >>> 0; h1 = (h1 + b) >>> 0; h2 = (h2 + c) >>> 0; h3 = (h3 + d) >>> 0;
111
- h4 = (h4 + e) >>> 0; h5 = (h5 + f) >>> 0; h6 = (h6 + g) >>> 0; h7 = (h7 + h) >>> 0;
112
- }
113
- return [h0, h1, h2, h3, h4, h5, h6, h7].map((word) => word.toString(16).padStart(8, "0")).join("");
114
- }
115
- /** The exact offline confirmation string for one recovery scope.
116
- * @param {{root: string, owner: string, expectedRevision: number, operationIds: string[]}} scope */
117
- function recoveryConfirmation(scope) {
118
- return `ADMIN OVERRIDE ${sha256Hex(canonicalJson({ root: scope.root, owner: scope.owner, expectedRevision: scope.expectedRevision, operationIds: [...scope.operationIds].sort(), risk: LATE_WRITER_RISK }))}`;
119
- }
33
+ // ---- Online administrative recovery (proposals from the host) ------
34
+ // The recovery derivation — eligibility, the scoped confirmation
35
+ // digest, the failing-precondition diagnosis — lives HOST-side
36
+ // (src/edit-lock/inspect.js recoveryProposals), computed from the same
37
+ // parsed authority image the inspection summarizes. The panel renders
38
+ // the server proposals and submits their scope and confirmation
39
+ // VERBATIM; it never re-derives a safety-sensitive rule. The manager
40
+ // re-verifies every precondition at commit time either way.
41
+ /** The unmet reason CODES (schemaVersion 1) mapped to dictionary keys;
42
+ * an unknown code falls back to a generic message naming the code. */
43
+ const RECOVERY_REASON_KEYS = {
44
+ "disposition-recorded": "editLockMaintRecoverReasonDispositionRecorded",
45
+ "owner-unknown": "editLockMaintRecoverReasonOwnerUnknown",
46
+ "not-interrupted": "editLockMaintRecoverReasonNotInterrupted",
47
+ "prepared-outstanding": "editLockMaintRecoverReasonPreparedOutstanding",
48
+ "publishing-outstanding": "editLockMaintRecoverReasonPublishingOutstanding",
49
+ "no-unresolved": "editLockMaintRecoverReasonNoUnresolved",
50
+ };
51
+ /** The fail-closed gate (a new client meeting an old or evolved host):
52
+ * only a schemaVersion-1 proposals payload is rendered at all; anything
53
+ * else disables the online recovery action with refresh guidance.
54
+ * @param {any} value the inspect response value */
55
+ const supportedProposals = (value) => value?.recoveryProposals?.schemaVersion === 1 ? value.recoveryProposals : null;
120
56
  /** Recovery state is scoped exactly like the authority: per domain root
121
57
  * AND owner. Session ids are not unique across independent domains, so
122
58
  * one root's completed recovery must never disable another root's. */
123
59
  const recoveryKey = (root, owner) => `${root}\n${owner}`;
124
- /** Eligible one-click recoveries from one inspection: each interrupted
125
- * owner whose every unresolved operation is a still-blocking unknown.
126
- * @param {string} root @param {any} snapshot */
127
- function recoveryCandidates(root, snapshot) {
128
- const interrupted = new Set((Array.isArray(snapshot?.sessions) ? snapshot.sessions : [])
129
- .filter((/** @type {any} */ session) => session?.interrupted === true)
130
- .map((/** @type {any} */ session) => session.sessionId));
131
- /** @type {Map<string, any[]>} */
132
- const byOwner = new Map();
133
- for (const op of Array.isArray(snapshot?.unresolved) ? snapshot.unresolved : []) {
134
- const owner = op?.key?.sessionId;
135
- if (typeof owner !== "string") continue;
136
- const list = byOwner.get(owner) ?? [];
137
- list.push(op);
138
- byOwner.set(owner, list);
139
- }
140
- /** @type {{owner: string, scope: {root: string, owner: string, expectedRevision: number, operationIds: string[]}, confirmation: string}[]} */
141
- const candidates = [];
142
- // An owner with a prepared operation can never be recovered online
143
- // (the manager requires every unresolved operation to be unknown), so
144
- // offering the action would present a click that always refuses.
145
- const preparedOwners = new Set((Array.isArray(snapshot?.prepared) ? snapshot.prepared : [])
146
- .map((op) => op?.key?.sessionId)
147
- .filter((owner) => typeof owner === "string"));
148
- for (const [owner, ops] of byOwner) {
149
- if (!interrupted.has(owner)) continue;
150
- if (preparedOwners.has(owner)) continue;
151
- if (!(ops.length > 0 && ops.every((op) => op?.phase === "unknown" && op?.admissionBlocked === true))) continue;
152
- const scope = { root, owner, expectedRevision: snapshot.revision, operationIds: ops.map((op) => op.key.operationId).sort() };
153
- candidates.push({ owner, scope, confirmation: recoveryConfirmation(scope) });
154
- }
155
- return candidates;
156
- }
157
60
  /** Recovery-UX zone copy (D1/D2). The entry dictionaries (client.js) are
158
- * owned by the entry chunk, so the new zone copy lives HERE as
159
- * template-layer English per the text discipline; file paths, session ids
160
- * and statuses stay dynamic instance content. */
61
+ * owned by the entry chunk, so the zone copy lives HERE as
62
+ * template-layer English per the text discipline; the unmet reason prose
63
+ * lives in the dictionaries below (server reason CODES are localized),
64
+ * while file paths, session ids and statuses stay dynamic instance
65
+ * content. */
161
66
  const RECOVERY_UX = Object.freeze({
162
67
  blockedHeader: "Blocked files — one-click force release",
163
68
  blockedHint: "Opening the panel ran the read-only inspection automatically. Each row settles the holder's unresolved publication fence (when present) and releases the lock in one operator-confirmed step.",
@@ -167,88 +72,7 @@ window.__ModuleLoader__.load({
167
72
  stateFence: "publication fence",
168
73
  stateInterrupted: "interrupted",
169
74
  stateActive: "active",
170
- reasonNotInterrupted: "The holding session is not durably interrupted; the online administrative recovery can only override an interrupted historical owner. Stop the session first, or ask it to release the file.",
171
- reasonPrepared: "The owner still has a prepared operation; recover to stable unknown history before an administrative override.",
172
- reasonPublishing: "The owner still has an operation in the publishing phase; only unknown outcomes can be overridden online.",
173
- reasonDisposition: "An administrative recovery is already recorded for this owner; admission for its operations is no longer blocked.",
174
- reasonNoUnresolved: "The owner has no unresolved operations; an ordinary unlock may be enough.",
175
- reasonUnknownSession: "The owner session is not recorded in this authority image.",
176
75
  });
177
- /** The first failing precondition of an ineligible blocking owner, in the
178
- * manager's refusal order (revoked/disposition → owner unknown → not
179
- * interrupted → prepared → publishing → no unresolved operations).
180
- * @param {string} owner @param {any} snapshot @returns {string} */
181
- function unmetReason(owner, snapshot) {
182
- const unresolved = Array.isArray(snapshot?.unresolved) ? snapshot.unresolved : [];
183
- const ownerOps = unresolved.filter((op) => op?.key?.sessionId === owner);
184
- const prepared = (Array.isArray(snapshot?.prepared) ? snapshot.prepared : []).filter((op) => op?.key?.sessionId === owner);
185
- const session = (Array.isArray(snapshot?.sessions) ? snapshot.sessions : []).find((row) => row?.sessionId === owner);
186
- if (ownerOps.some((op) => op?.admissionBlocked === false)) return RECOVERY_UX.reasonDisposition;
187
- if (!session) return RECOVERY_UX.reasonUnknownSession;
188
- if (session.interrupted !== true) return RECOVERY_UX.reasonNotInterrupted;
189
- if (prepared.length > 0) return RECOVERY_UX.reasonPrepared;
190
- if (ownerOps.some((op) => op?.phase === "publishing")) return RECOVERY_UX.reasonPublishing;
191
- return RECOVERY_UX.reasonNoUnresolved;
192
- }
193
- /** Recovery-UX diagnosis (D1/D2): the three-zone classification the panel
194
- * renders from one inspection. Eligibility is recoveryCandidates — the
195
- * SAME scope and confirmation algorithm — and every ineligible blocking
196
- * owner carries the named failing precondition. Blocking artifacts are
197
- * retained locks plus still-blocking (admissionBlocked) unresolved
198
- * publications; file-scoped fences become rows, wider fences name their
199
- * scope path in the unmet zone. Pure: no fetch, no state, no clock.
200
- * @param {string} root @param {any} snapshot */
201
- function recoveryDiagnosis(root, snapshot) {
202
- const candidates = recoveryCandidates(root, snapshot);
203
- const candidateByOwner = new Map(candidates.map((candidate) => [candidate.owner, candidate]));
204
- const sessions = Array.isArray(snapshot?.sessions) ? snapshot.sessions : [];
205
- const interruptedByOwner = new Map(sessions.map((session) => [session.sessionId, session?.interrupted === true]));
206
- const retained = Array.isArray(snapshot?.retainedLocks) ? snapshot.retainedLocks : [];
207
- /** @type {Map<string, {resourceId: string, owner: string, lockStatus: string|null, fence: boolean, interrupted: boolean}>} */
208
- const rowsByResource = new Map();
209
- const blockedOwners = new Set();
210
- for (const lock of retained) {
211
- if (typeof lock?.resourceId !== "string" || typeof lock?.owner !== "string") continue;
212
- blockedOwners.add(lock.owner);
213
- const row = rowsByResource.get(lock.resourceId) ?? { resourceId: lock.resourceId, owner: lock.owner, lockStatus: null, fence: false, interrupted: interruptedByOwner.get(lock.owner) ?? false };
214
- row.lockStatus = row.lockStatus ?? (typeof lock.status === "string" ? lock.status : null);
215
- rowsByResource.set(lock.resourceId, row);
216
- }
217
- /** @type {Map<string, {owner: string, scopePaths: string[]}>} */
218
- const fencedOwners = new Map();
219
- for (const op of Array.isArray(snapshot?.unresolved) ? snapshot.unresolved : []) {
220
- if (op?.admissionBlocked !== true || typeof op?.key?.sessionId !== "string") continue;
221
- blockedOwners.add(op.key.sessionId);
222
- const scope = op?.scope ?? null;
223
- if (scope?.kind === "file" && typeof scope.path === "string") {
224
- const existing = rowsByResource.get(scope.path);
225
- if (existing) {
226
- // Same owner locks AND fences the file (the incident shape): one row.
227
- if (existing.owner === op.key.sessionId) existing.fence = true;
228
- else {
229
- const entry = fencedOwners.get(op.key.sessionId) ?? { owner: op.key.sessionId, scopePaths: /** @type {string[]} */ ([]) };
230
- if (!entry.scopePaths.includes(scope.path)) entry.scopePaths.push(scope.path);
231
- fencedOwners.set(op.key.sessionId, entry);
232
- }
233
- } else {
234
- rowsByResource.set(scope.path, { resourceId: scope.path, owner: op.key.sessionId, lockStatus: null, fence: true, interrupted: interruptedByOwner.get(op.key.sessionId) ?? false });
235
- }
236
- } else {
237
- const entry = fencedOwners.get(op.key.sessionId) ?? { owner: op.key.sessionId, scopePaths: /** @type {string[]} */ ([]) };
238
- if (typeof scope?.path === "string" && !entry.scopePaths.includes(scope.path)) entry.scopePaths.push(scope.path);
239
- fencedOwners.set(op.key.sessionId, entry);
240
- }
241
- }
242
- const rows = [...rowsByResource.values()];
243
- const blocked = rows.filter((row) => candidateByOwner.has(row.owner));
244
- blocked.sort((a, b) => a.resourceId.localeCompare(b.resourceId));
245
- const unmet = [];
246
- for (const owner of [...blockedOwners].sort()) {
247
- if (candidateByOwner.has(owner)) continue;
248
- unmet.push({ owner, rows: rows.filter((row) => row.owner === owner), scopePaths: fencedOwners.get(owner)?.scopePaths ?? [], reason: unmetReason(owner, snapshot) });
249
- }
250
- return { candidates, blocked, unmet };
251
- }
252
76
  /** Error boundary isolating the maintenance panel from the settings page. */
253
77
  class EditLockMaintenanceBoundary extends react.Component {
254
78
  constructor(props) {
@@ -268,11 +92,6 @@ window.__ModuleLoader__.load({
268
92
  /** Switch-state banner keys (state names contain hyphens). */
269
93
  const STATE_KEYS = { enforced: "Enforced", "disable-requested": "DisableRequested", "enable-requested": "EnableRequested", disabled: "Disabled", unknown: "Unknown" };
270
94
  const onlineRecoveryAvailable = (state) => state === "enforced" || state === "disable-requested";
271
- const post = (url, body) => fetch(url, {
272
- method: "POST",
273
- credentials: "include",
274
- ...(body === undefined ? {} : { headers: { "content-type": "application/json" }, body: JSON.stringify(body) })
275
- }).then((response) => response.json());
276
95
  // Invoke the render callback only during descendant traversal, never while
277
96
  // the parent constructs the boundary element.
278
97
  function MaintenanceBody({ render }) {
@@ -286,14 +105,14 @@ window.__ModuleLoader__.load({
286
105
  const [expanded, setExpanded] = react.useState(/** @type {string | null} */ (null));
287
106
  const [inspections, setInspections] = react.useState({});
288
107
  const [recoveries, setRecoveries] = react.useState(/** @type {Record<string, {status: 'busy'} | {status: 'done', revision: number, idempotent: boolean} | {status: 'error', message: string}>} */ ({}));
289
- /** The one explicit action: submit the displayed, client-computed
290
- * confirmation for one displayed scope. No hash typing, no restart.
108
+ /** The one explicit action: submit the SERVER-computed confirmation
109
+ * for one displayed scope, unchanged. No hash typing, no restart.
291
110
  * A refusal is shown next to a freshly reloaded scope.
292
- * @param {string} root @param {ReturnType<typeof recoveryCandidates>[number]} candidate */
111
+ * @param {string} root @param {ReturnType<typeof import('../src/edit-lock/inspect.js').recoveryProposals>['candidates'][number]} candidate */
293
112
  const recover = (root, candidate) => {
294
113
  if (view?.status !== "ready" || !onlineRecoveryAvailable(view.value.state)) return;
295
114
  setRecoveries((previous) => ({ ...previous, [recoveryKey(root, candidate.owner)]: { status: "busy" } }));
296
- post("api/weir-edit-lock/maintenance/recover-online", {
115
+ props.channel("api/weir-edit-lock/maintenance/recover-online", {
297
116
  root,
298
117
  owner: candidate.owner,
299
118
  expectedRevision: candidate.scope.expectedRevision,
@@ -302,10 +121,10 @@ window.__ModuleLoader__.load({
302
121
  reason: "Confirmed in the Edit Lock maintenance panel: the operator reviewed the displayed scope and accepted the displayed late-writer risk.",
303
122
  acceptLateWriterRisk: true,
304
123
  confirmation: candidate.confirmation,
305
- }).then((payload) => {
306
- setRecoveries((previous) => ({ ...previous, [recoveryKey(root, candidate.owner)]: payload?.ok
307
- ? { status: "done", revision: payload.value.revision, idempotent: payload.value.idempotent === true }
308
- : { status: "error", message: payload?.error?.detail ?? payload?.error?.message ?? "unknown" } }));
124
+ }).then((result) => {
125
+ setRecoveries((previous) => ({ ...previous, [recoveryKey(root, candidate.owner)]: result.ok === true
126
+ ? { status: "done", revision: result.value.revision, idempotent: result.value.idempotent === true }
127
+ : { status: "error", message: result.error?.detail ?? result.error?.message ?? "unknown" } }));
309
128
  inspect(root);
310
129
  }).catch((error) => {
311
130
  setRecoveries((previous) => ({ ...previous, [recoveryKey(root, candidate.owner)]: { status: "error", message: String(error?.message ?? error) } }));
@@ -314,14 +133,14 @@ window.__ModuleLoader__.load({
314
133
  };
315
134
  const load = () => {
316
135
  setView({ status: "loading" });
317
- post("api/weir-edit-lock/maintenance/status")
318
- .then((payload) => {
319
- setView(payload?.ok ? { status: "ready", value: payload.value } : { status: "error", message: payload?.error?.message ?? "unknown" });
136
+ props.channel("api/weir-edit-lock/maintenance/status")
137
+ .then((result) => {
138
+ setView(result.ok === true ? { status: "ready", value: result.value } : { status: "error", message: result.error.message });
320
139
  // Recovery UX D1: opening the panel auto-runs the read-only
321
140
  // inspection for every server-derived domain root, so the
322
141
  // blocked/unmet zones render without any manual step. The
323
142
  // inspection itself stays read-only (the same endpoint).
324
- if (payload?.ok && Array.isArray(payload.value?.domains)) payload.value.domains.forEach((domain) => inspect(domain.root, false));
143
+ if (result.ok === true && Array.isArray(result.value?.domains)) result.value.domains.forEach((domain) => inspect(domain.root, false));
325
144
  })
326
145
  .catch((error) => setView({ status: "error", message: String(error?.message ?? error) }));
327
146
  };
@@ -335,9 +154,9 @@ window.__ModuleLoader__.load({
335
154
  const inspect = (root, expand = true) => {
336
155
  if (expand) setExpanded(root);
337
156
  setInspections((previous) => ({ ...previous, [root]: { status: "loading" } }));
338
- post("api/weir-edit-lock/maintenance/inspect", { root })
339
- .then((payload) => {
340
- setInspections((previous) => ({ ...previous, [root]: payload?.ok ? { status: "ready", value: payload.value } : { status: "error", message: payload?.error?.message ?? "unknown" } }));
157
+ props.channel("api/weir-edit-lock/maintenance/inspect", { root })
158
+ .then((result) => {
159
+ setInspections((previous) => ({ ...previous, [root]: result.ok === true ? { status: "ready", value: result.value } : { status: "error", message: result.error.message } }));
341
160
  })
342
161
  .catch((error) => setInspections((previous) => ({ ...previous, [root]: { status: "error", message: String(error?.message ?? error) } })));
343
162
  };
@@ -347,14 +166,20 @@ window.__ModuleLoader__.load({
347
166
  if (scope?.kind === "domain") return t("editLockMaintScopeDomain");
348
167
  return t("editLockMaintScopeNone");
349
168
  };
169
+ /** The localized explanation for one server reason CODE; an unknown
170
+ * code falls back to a generic message naming the code itself. */
171
+ const reasonText = (reason) => {
172
+ const key = RECOVERY_REASON_KEYS[reason];
173
+ return key ? t(key) : t("editLockMaintRecoverReasonUnknown").replace("{code}", String(reason ?? ""));
174
+ };
350
175
  const presenceRow = (value) => {
351
176
  if (value.presence === "valid") return null;
352
177
  const key = { none: "editLockMaintPresenceNone", empty: "editLockMaintPresenceEmpty", "no-committed-snapshot": "editLockMaintPresenceJunk", "not-a-file": "editLockMaintPresenceNotAFile", corrupt: "editLockMaintPresenceCorrupt", unreadable: "editLockMaintPresenceUnreadable" }[value.presence];
353
178
  return react_jsx_runtime.jsx("div", { style: value.presence === "corrupt" || value.presence === "not-a-file" || value.presence === "no-committed-snapshot" ? dangerStyle : hintStyle, children: `${t(key) ?? value.presence}${value.message ? ` ${value.message}` : ""}` });
354
179
  };
355
- /** Recovery card for one eligible owner: the FULL scope, the risk text
356
- * verbatim and the computed digest, then the one explicit button.
357
- * @param {string} root @param {ReturnType<typeof recoveryCandidates>[number]} candidate */
180
+ /** Recovery card for one eligible owner: the FULL server scope, the
181
+ * risk text and the server digest, then the one explicit button.
182
+ * @param {string} root @param {ReturnType<typeof import('../src/edit-lock/inspect.js').recoveryProposals>['candidates'][number]} candidate */
358
183
  const recoveryCard = (root, candidate) => {
359
184
  const state = recoveries[recoveryKey(root, candidate.owner)];
360
185
  return react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "8px", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "var(--dsw-radius-md)" }, children: [
@@ -365,7 +190,7 @@ window.__ModuleLoader__.load({
365
190
  react_jsx_runtime.jsx("div", { children: `${t("editLockMaintRecoverOperations")}:` }),
366
191
  ...candidate.scope.operationIds.map((id, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: id }, index))
367
192
  ] }),
368
- react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverRisk")}: ${LATE_WRITER_RISK}` }),
193
+ react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverRisk")}: ${candidate.risk}` }),
369
194
  react_jsx_runtime.jsx("div", { style: monoStyle, children: `${t("editLockMaintRecoverDigest")}: ${candidate.confirmation}` }),
370
195
  state?.status === "done"
371
196
  ? react_jsx_runtime.jsx("div", { style: hintStyle, children: t("editLockMaintRecoverDone").replace("{revision}", String(state.revision)) })
@@ -419,7 +244,7 @@ window.__ModuleLoader__.load({
419
244
  react_jsx_runtime.jsx("div", { children: `${t("editLockMaintRecoverOperations")}:` }),
420
245
  ...candidate.scope.operationIds.map((id, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: id }, index))
421
246
  ] }),
422
- react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverRisk")}: ${LATE_WRITER_RISK}` }),
247
+ react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverRisk")}: ${candidate.risk}` }),
423
248
  react_jsx_runtime.jsx("div", { style: monoStyle, children: `${t("editLockMaintRecoverDigest")}: ${candidate.confirmation}` }),
424
249
  state?.status === "error"
425
250
  ? react_jsx_runtime.jsx("div", { style: dangerStyle, children: `${t("editLockMaintRecoverFailed")} ${state.message}` })
@@ -432,9 +257,12 @@ window.__ModuleLoader__.load({
432
257
  react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${t("editLockMaintRecoverOwner")}: ${entry.owner}` }),
433
258
  ...entry.rows.map((row, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: `${row.resourceId} · ${RECOVERY_UX.holder} ${row.owner} · ${holderStateText(row)}` }, index)),
434
259
  ...entry.scopePaths.map((path, index) => react_jsx_runtime.jsx("div", { style: monoStyle, children: path }, index)),
435
- react_jsx_runtime.jsx("div", { style: dangerStyle, children: entry.reason })
260
+ react_jsx_runtime.jsx("div", { style: dangerStyle, children: reasonText(entry.reason) })
436
261
  ], key: `${root}\n${entry.owner}\nunmet` });
437
- const snapshotSection = (/** @type {string} */ root, /** @type {any} */ snapshot) => react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [
262
+ const snapshotSection = (/** @type {string} */ root, /** @type {any} */ value) => {
263
+ const snapshot = value.snapshot;
264
+ const proposals = supportedProposals(value);
265
+ return react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [
438
266
  react_jsx_runtime.jsx("div", { style: chainDescStyle, children: t("editLockMaintCounts")
439
267
  .replace("{sessions}", String(snapshot.counts.sessions))
440
268
  .replace("{locks}", String(snapshot.counts.locks))
@@ -453,13 +281,14 @@ window.__ModuleLoader__.load({
453
281
  ...snapshot.retainedLocks.map((lock, index) => react_jsx_runtime.jsx("div", { style: chainDescStyle, children: `${lock.resourceId} · ${lock.owner} · ${lock.status}${lock.reason ? `: ${lock.reason}` : ""} · gen ${lock.generation}` }, index))
454
282
  ] })
455
283
  : null,
456
- onlineRecoveryAvailable(view?.status === "ready" ? view.value.state : null) && recoveryCandidates(root, snapshot).length > 0
284
+ proposals !== null && onlineRecoveryAvailable(view?.status === "ready" ? view.value.state : null) && Array.isArray(proposals.candidates) && proposals.candidates.length > 0
457
285
  ? react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [
458
286
  react_jsx_runtime.jsx("div", { style: chainCategoryStyle, children: t("editLockMaintRecoveries") }),
459
287
  react_jsx_runtime.jsx("div", { style: chainDescStyle, children: t("editLockMaintRecoverHint") }),
460
- ...recoveryCandidates(root, snapshot).map((candidate) => recoveryCard(root, candidate))
288
+ ...proposals.candidates.map((candidate) => recoveryCard(root, candidate))
461
289
  ] }) : null
462
- ] });
290
+ ] });
291
+ };
463
292
  const inspectionBody = (root) => {
464
293
  const inspection = inspections[root];
465
294
  if (!inspection) return null;
@@ -474,7 +303,7 @@ window.__ModuleLoader__.load({
474
303
  return react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px", paddingTop: "4px" }, children: [
475
304
  value.reservation === true ? react_jsx_runtime.jsx("div", { style: hintStyle, children: t("editLockMaintReservation") }) : null,
476
305
  presenceRow(value),
477
- value.presence === "valid" ? snapshotSection(root, value.snapshot) : null
306
+ value.presence === "valid" ? snapshotSection(root, value) : null
478
307
  ] });
479
308
  };
480
309
  const domainRow = (domain) => react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
@@ -506,18 +335,23 @@ window.__ModuleLoader__.load({
506
335
  const online = onlineRecoveryAvailable(value.state);
507
336
  const zoneGroups = new Map();
508
337
  const zoneUnmet = [];
338
+ // Fail closed: a missing or unsupported proposals payload disables
339
+ // every zone derived from it — the panel never re-derives the
340
+ // recovery surface by itself.
341
+ let proposalsUnsupported = false;
509
342
  for (const root of Object.keys(inspections)) {
510
343
  const inspection = inspections[root];
511
344
  if (inspection?.status !== "ready" || inspection.value?.presence !== "valid") continue;
512
- const diagnosis = recoveryDiagnosis(root, inspection.value.snapshot);
513
- for (const row of diagnosis.blocked) {
514
- const candidate = diagnosis.candidates.find((entry) => entry.owner === row.owner);
515
- const key = recoveryKey(root, row.owner);
345
+ const proposals = supportedProposals(inspection.value);
346
+ if (!proposals) { proposalsUnsupported = true; continue; }
347
+ for (const candidate of Array.isArray(proposals.candidates) ? proposals.candidates : []) {
348
+ if (!candidate || typeof candidate.owner !== "string" || !Array.isArray(candidate.rows) || candidate.rows.length === 0) continue;
349
+ const key = recoveryKey(root, candidate.owner);
516
350
  const group = zoneGroups.get(key) ?? { root, candidate, rows: [] };
517
- group.rows.push(row);
351
+ for (const row of candidate.rows) group.rows.push(row);
518
352
  zoneGroups.set(key, group);
519
353
  }
520
- for (const entry of diagnosis.unmet) zoneUnmet.push({ root, entry });
354
+ for (const entry of Array.isArray(proposals.unmet) ? proposals.unmet : []) zoneUnmet.push({ root, entry });
521
355
  }
522
356
  const blockedGroups = [...zoneGroups.values()];
523
357
  zoneUnmet.sort((a, b) => a.entry.owner.localeCompare(b.entry.owner));
@@ -549,6 +383,9 @@ window.__ModuleLoader__.load({
549
383
  react_jsx_runtime.jsx("span", { style: warnStyle, children: t("editLockMaintWarnKeepHistory") }),
550
384
  react_jsx_runtime.jsx("span", { style: warnStyle, children: t("editLockMaintWarnUnlock") })
551
385
  ] }),
386
+ proposalsUnsupported
387
+ ? react_jsx_runtime.jsx("div", { style: warnStyle, children: t("editLockMaintRecoverUpgrade") })
388
+ : null,
552
389
  online && blockedGroups.length > 0
553
390
  ? react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px" }, children: [
554
391
  react_jsx_runtime.jsx("span", { style: chainCategoryStyle, children: RECOVERY_UX.blockedHeader }),
@@ -590,10 +427,166 @@ window.__ModuleLoader__.load({
590
427
  open ? react_jsx_runtime.jsx(EditLockMaintenanceBoundary, { t, children: react_jsx_runtime.jsx(MaintenanceBody, { render: panelBody }) }) : null
591
428
  ] });
592
429
  }
430
+ // Locale data face (pure data, NO ctx calls — a chunk factory re-executes
431
+ // under HMR, so a chunk-local register would throw on the duplicate
432
+ // (namespace, locale) pair): the entry registers these under "settings.weir.page"
433
+ // inside a ctx.effect when this chunk arrives (lib/client.js
434
+ // registerChunkDictionaries). Pinned by test/client.test.js's dictionary
435
+ // union pin.
436
+ // (The entry merges every settings-batch slice into the single "settings.weir.page"
437
+ // registration — the whole settings card tree shares one t.)
438
+ const dictionaries = {
439
+ en: {
440
+ editLockMaint: "Edit Lock maintenance",
441
+ editLockMaintHint: "Profile-wide enforcement status and read-only diagnostics.",
442
+ editLockMaintFailed: "Edit Lock maintenance failed",
443
+ editLockMaintLoading: "Loading…",
444
+ editLockMaintUnavailable: "Maintenance data unavailable:",
445
+ editLockMaintRetry: "Retry",
446
+ editLockMaintRefresh: "Refresh",
447
+ editLockMaintInspect: "Inspect",
448
+ editLockMaintStateEnforced: "Enforced",
449
+ editLockMaintStateEnforcedHint: "The switch is on and the Edit Lock manager is mounted in this process.",
450
+ editLockMaintStateDisableRequested: "Disable requested — restart required",
451
+ editLockMaintStateDisableRequestedHint: "You turned the switch off, but the manager is still mounted and enforcing. The change applies after restarting DeepSeek Harness.",
452
+ editLockMaintStateEnableRequested: "Enable requested — restart required",
453
+ editLockMaintStateEnableRequestedHint: "You turned the switch on, but no manager is mounted in this process. The change applies after restarting DeepSeek Harness.",
454
+ editLockMaintStateDisabled: "Enforcement disabled",
455
+ editLockMaintStateDisabledHint: "The switch is off and every recorded, fully installed row reports enforcement disabled.",
456
+ editLockMaintStateUnknown: "Unknown",
457
+ editLockMaintStateUnknownHint: "Mount evidence is absent, inconsistent, installing, disposing or failed. The saved value alone never proves enforcement or disablement.",
458
+ editLockMaintRestart: "Applies after restarting DeepSeek Harness.",
459
+ editLockMaintPinned: "This composition pins the Edit Lock row configuration; the settings switch does not govern it here.",
460
+ editLockMaintScopeNote: "The switch is profile-wide: it covers every Weir session in this profile, not one workspace.",
461
+ editLockMaintWarnKeepHistory: "Turning enforcement off does not clear unresolved operations, fences or history — turning it back on can block the same files again.",
462
+ editLockMaintWarnUnlock: "An ordinary unlock frees one lock without validating content; it is not historical recovery and never settles an unknown publication.",
463
+ editLockMaintOffRisk: "Edit Lock is not mounted. Ordinary edits are UNPROTECTED and can overwrite another session's work. Retained locks and fences do not block off-period edits; they may block files again after you re-enable and restart.",
464
+ editLockMaintUnknownRisk: "Edit Lock mount status cannot be verified. If no manager is mounted, ordinary edits are UNPROTECTED and can overwrite another session's work. Do not treat retained fences as proof of current protection.",
465
+ editLockMaintHistory: "Retained history — not current blocks",
466
+ editLockMaintHistoryHint: "These old locks and publication fences remain on disk for inspection. Without a mounted manager they do not block ordinary edits; after re-enabling they may block files again. Online force release is unavailable here; separate offline administrator recovery remains available.",
467
+ editLockMaintUnknownHistory: "Retained authority — current effect unverified",
468
+ editLockMaintUnknownHistoryHint: "The saved value and old fences cannot establish whether a manager is mounted. Inspect this history read-only; online force release is unavailable until enforcement is verified. Separate offline administrator recovery remains available.",
469
+ editLockMaintBlocked: "Initialization blocked",
470
+ editLockMaintDomains: "Domains",
471
+ editLockMaintNoDomains: "No live session reports an Edit Lock domain in this profile.",
472
+ editLockMaintAuthorityYes: "authority present",
473
+ editLockMaintAuthorityNo: "authority currently absent",
474
+ editLockMaintReservation: "A publisher reservation is present: this or another Harness holds (or stranded) the publishing claim for this domain.",
475
+ editLockMaintPresenceNone: "No authority directory is currently present. Prior initialization cannot be determined.",
476
+ editLockMaintPresenceEmpty: "The authority directory is currently empty. Prior initialization cannot be determined.",
477
+ editLockMaintPresenceJunk: "The authority directory has contents but no committed snapshot; the store would refuse to open it. Nothing was changed.",
478
+ editLockMaintPresenceNotAFile: "snapshot.json is not a regular file; the store would refuse it. Nothing was changed.",
479
+ editLockMaintPresenceCorrupt: "The authority image failed integrity validation and was left untouched — no automatic repair exists. Refusal:",
480
+ editLockMaintPresenceUnreadable: "The authority could not be read:",
481
+ editLockMaintCounts: "{sessions} session(s) · {locks} lock(s) · {operations} recorded operation(s)",
482
+ editLockMaintUnresolved: "Unresolved operations",
483
+ editLockMaintUnresolvedNone: "No unresolved operations.",
484
+ editLockMaintRetained: "Retained locks (interrupted / abnormal / pending)",
485
+ editLockMaintScopeFile: "one file",
486
+ editLockMaintScopeSubtree: "a directory subtree",
487
+ editLockMaintScopeDomain: "the whole work directory",
488
+ editLockMaintScopeNone: "no recorded scope",
489
+ editLockMaintRecoveries: "Online administrative recovery",
490
+ editLockMaintRecoverHint: "Settle an interrupted owner's unknown publications in the running manager: one explicit click, no restart, no hash typing. The unknown outcome stays in history. If the scope changed since it was loaded, the manager refuses and the panel reloads it.",
491
+ editLockMaintRecoverRoot: "Root",
492
+ editLockMaintRecoverOwner: "Owner session",
493
+ editLockMaintRecoverRevision: "Authority revision",
494
+ editLockMaintRecoverOperations: "Unresolved operation IDs",
495
+ editLockMaintRecoverRisk: "Risk you accept",
496
+ editLockMaintRecoverDigest: "Confirmation digest",
497
+ editLockMaintRecoverConfirm: "Settle this owner online",
498
+ editLockMaintRecoverBusy: "Settling…",
499
+ editLockMaintRecoverDone: "Settled at revision {revision}; admission is unblocked and the unknown outcome stays in history.",
500
+ editLockMaintRecoverFailed: "Recovery not acknowledged:",
501
+ editLockMaintRecoverUpgrade: "Online administrative recovery is unavailable: the server did not return recovery proposals this panel understands (missing or unsupported version). Refresh this panel or upgrade DeepSeek Harness, then inspect again.",
502
+ editLockMaintRecoverReasonDispositionRecorded: "An administrative recovery is already recorded for this owner; admission for its operations is no longer blocked.",
503
+ editLockMaintRecoverReasonOwnerUnknown: "The owner session is not recorded in this authority image.",
504
+ editLockMaintRecoverReasonNotInterrupted: "The holding session is not durably interrupted; the online administrative recovery can only override an interrupted historical owner. Stop the session first, or ask it to release the file.",
505
+ editLockMaintRecoverReasonPreparedOutstanding: "The owner still has a prepared operation; recover to stable unknown history before an administrative override.",
506
+ editLockMaintRecoverReasonPublishingOutstanding: "The owner still has an operation in the publishing phase; only unknown outcomes can be overridden online.",
507
+ editLockMaintRecoverReasonNoUnresolved: "The owner has no unresolved operations; an ordinary unlock may be enough.",
508
+ editLockMaintRecoverReasonUnknown: "The server reported an unrecognized recovery precondition (code: {code}). Refresh or upgrade DeepSeek Harness for the localized explanation.",
509
+ },
510
+ zh: {
511
+ editLockMaint: "编辑锁维护",
512
+ editLockMaintHint: "查看全 profile 的强制执行状态与只读诊断。",
513
+ editLockMaintFailed: "编辑锁维护面板加载失败",
514
+ editLockMaintLoading: "加载中……",
515
+ editLockMaintUnavailable: "维护数据不可用:",
516
+ editLockMaintRetry: "重试",
517
+ editLockMaintRefresh: "刷新",
518
+ editLockMaintInspect: "检查",
519
+ editLockMaintStateEnforced: "强制执行中",
520
+ editLockMaintStateEnforcedHint: "开关已打开,且编辑锁管理器已在本进程中挂载。",
521
+ editLockMaintStateDisableRequested: "已请求停用——需要重启",
522
+ editLockMaintStateDisableRequestedHint: "你已关闭开关,但管理器仍处于挂载并强制执行状态。重启 DeepSeek Harness 后生效。",
523
+ editLockMaintStateEnableRequested: "已请求启用——需要重启",
524
+ editLockMaintStateEnableRequestedHint: "你已打开开关,但本进程尚未挂载管理器。重启 DeepSeek Harness 后生效。",
525
+ editLockMaintStateDisabled: "强制执行已停用",
526
+ editLockMaintStateDisabledHint: "开关关闭,所有已记录且完成安装的行均报告强制执行已停用。",
527
+ editLockMaintStateUnknown: "未知",
528
+ editLockMaintStateUnknownHint: "挂载证据缺失、不一致,或仍在安装、卸载、失败状态。仅凭已保存的值不能证明正在强制执行或已经停用。",
529
+ editLockMaintRestart: "重启 DeepSeek Harness 后生效。",
530
+ editLockMaintPinned: "当前组合在编辑锁行上钉死了配置;这里的设置开关对它不生效。",
531
+ editLockMaintScopeNote: "该开关对整个 profile 生效:覆盖此 profile 下的所有 Weir 会话,而非某一个工作区。",
532
+ editLockMaintWarnKeepHistory: "关闭强制执行不会清除未决操作、围栏或历史——重新打开后同一批文件可能再次被阻塞。",
533
+ editLockMaintWarnUnlock: "普通解锁只释放一把锁且不校验内容;它不是历史恢复,永远不会结清未知的发布。",
534
+ editLockMaintOffRisk: "编辑锁管理器未挂载。普通编辑不受保护,可能覆盖其他会话的工作。保留的锁和围栏不会阻止停用期间的编辑;重新启用并重启后可能再次阻塞文件。",
535
+ editLockMaintUnknownRisk: "无法核实编辑锁的挂载状态。如果管理器未挂载,普通编辑不受保护,可能覆盖其他会话的工作。不能把保留的围栏当成当前仍受保护的证据。",
536
+ editLockMaintHistory: "保留的历史——不是当前阻塞",
537
+ editLockMaintHistoryHint: "这些旧锁与发布围栏保留在磁盘供检查。没有挂载管理器时,它们不会阻止普通编辑;重新启用后可能再次阻塞文件。此处不可在线强制释放;独立的离线管理员恢复端点仍可使用。",
538
+ editLockMaintUnknownHistory: "保留的权威记录——当前作用未经核实",
539
+ editLockMaintUnknownHistoryHint: "仅凭已保存的值与旧围栏无法确认管理器是否挂载。可只读检查这些历史;核实强制执行前不可在线强制释放。独立的离线管理员恢复端点仍可使用。",
540
+ editLockMaintBlocked: "初始化受阻",
541
+ editLockMaintDomains: "管理域",
542
+ editLockMaintNoDomains: "此 profile 中没有活动会话报告编辑锁管理域。",
543
+ editLockMaintAuthorityYes: "权威已存在",
544
+ editLockMaintAuthorityNo: "当前无权威",
545
+ editLockMaintReservation: "存在发布者预留:本进程或另一个 Harness 持有(或遗留)了该域的发布权。",
546
+ editLockMaintPresenceNone: "当前没有权威目录。无法据此判断此前是否初始化过。",
547
+ editLockMaintPresenceEmpty: "权威目录当前为空。无法据此判断此前是否初始化过。",
548
+ editLockMaintPresenceJunk: "权威目录有内容但没有已提交的快照;存储层会拒绝打开它。未做任何改动。",
549
+ editLockMaintPresenceNotAFile: "snapshot.json 不是常规文件;存储层会拒绝它。未做任何改动。",
550
+ editLockMaintPresenceCorrupt: "权威镜像未通过完整性校验,已原样保留——不存在自动修复。拒绝原因:",
551
+ editLockMaintPresenceUnreadable: "无法读取权威:",
552
+ editLockMaintCounts: "{sessions} 个会话 · {locks} 把锁 · {operations} 条已记录操作",
553
+ editLockMaintUnresolved: "未决操作",
554
+ editLockMaintUnresolvedNone: "没有未决操作。",
555
+ editLockMaintRetained: "保留的锁(中断 / 异常 / 待确认)",
556
+ editLockMaintScopeFile: "单个文件",
557
+ editLockMaintScopeSubtree: "一个目录子树",
558
+ editLockMaintScopeDomain: "整个工作目录",
559
+ editLockMaintScopeNone: "无记录范围",
560
+ editLockMaintRecoveries: "在线管理员恢复",
561
+ editLockMaintRecoverHint: "在运行中的管理器里结清已中断 owner 的未知发布:一次明确点击,无需重启、无需手打哈希;历史中的未知结论原样保留。若范围自加载后发生变化,管理器会拒绝,面板会重新加载。",
562
+ editLockMaintRecoverRoot: "管理根",
563
+ editLockMaintRecoverOwner: "属主会话",
564
+ editLockMaintRecoverRevision: "权威 revision",
565
+ editLockMaintRecoverOperations: "未决操作 ID",
566
+ editLockMaintRecoverRisk: "你接受的风险",
567
+ editLockMaintRecoverDigest: "确认摘要",
568
+ editLockMaintRecoverConfirm: "在线结清该 owner",
569
+ editLockMaintRecoverBusy: "结清中……",
570
+ editLockMaintRecoverDone: "已在 revision {revision} 结清;准入阻塞解除,历史仍保持未知。",
571
+ editLockMaintRecoverFailed: "恢复未确认:",
572
+ editLockMaintRecoverUpgrade: "在线管理员恢复不可用:服务端未返回本面板可理解的恢复提案(缺失或版本不受支持)。请刷新本面板或升级 DeepSeek Harness,然后重新检查。",
573
+ editLockMaintRecoverReasonDispositionRecorded: "该 owner 已有记录在案的管理员恢复;其操作的准入已不再阻塞。",
574
+ editLockMaintRecoverReasonOwnerUnknown: "该 owner 会话未记录在此权威镜像中。",
575
+ editLockMaintRecoverReasonNotInterrupted: "持有会话尚未持久中断;在线管理员恢复只能撤权已中断的历史 owner。请先停止该会话,或请它释放文件。",
576
+ editLockMaintRecoverReasonPreparedOutstanding: "该 owner 仍持有 prepared 操作;请先恢复到稳定的 unknown 历史,再进行管理员撤权。",
577
+ editLockMaintRecoverReasonPublishingOutstanding: "该 owner 仍有处于 publishing 阶段的操作;只有 unknown 结论可以在线撤权。",
578
+ editLockMaintRecoverReasonNoUnresolved: "该 owner 没有未决操作;普通解锁或许就已足够。",
579
+ editLockMaintRecoverReasonUnknown: "服务端报告了本面板不认识的恢复前置条件(代码:{code})。请刷新或升级 DeepSeek Harness 以获取对应说明。",
580
+ },
581
+ };
582
+
583
+ exports.dictionaries = dictionaries;
593
584
  exports.EditLockMaintenanceBoundary = EditLockMaintenanceBoundary;
594
585
  exports.EditLockMaintenanceField = EditLockMaintenanceField;
595
- // Exported for the chunk test's 对拍 against the server-side digest.
596
- exports.editLockRecovery = Object.freeze({ canonicalJson, sha256Hex, recoveryConfirmation, recoveryCandidates, recoveryDiagnosis, unmetReason, recoveryKey, LATE_WRITER_RISK, RECOVERY_UX });
586
+ // Exported for the chunk tests: the recovery progress key and the zone
587
+ // copy. The recovery derivation lives host-side (inspect.js); the chunk
588
+ // renders and forwards, never computes.
589
+ exports.editLockRecovery = Object.freeze({ recoveryKey, RECOVERY_UX });
597
590
  return module.exports;
598
591
  }
599
592
  });