wendkeep 0.77.0 → 0.79.0

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 (37) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.en.md +58 -3
  3. package/README.md +58 -3
  4. package/docs/en/commands/changes-and-verification.md +74 -2
  5. package/docs/en/commands/operating-profiles.md +49 -5
  6. package/docs/en/commands/verify.md +67 -5
  7. package/docs/en/commands/worktrees.md +39 -4
  8. package/docs/pt-BR/commands/changes-and-verification.md +73 -2
  9. package/docs/pt-BR/commands/operating-profiles.md +51 -5
  10. package/docs/pt-BR/commands/verify.md +67 -6
  11. package/docs/pt-BR/commands/worktrees.md +38 -3
  12. package/hooks/active-context-store.mjs +530 -2
  13. package/hooks/change-core.mjs +203 -123
  14. package/hooks/harness-doctor.mjs +51 -1
  15. package/hooks/obsidian-common.mjs +175 -9
  16. package/hooks/spec-core.mjs +118 -36
  17. package/package.json +2 -2
  18. package/packages/harness/src/sensors-core.mjs +57 -3
  19. package/packages/vault/src/evidence-envelope.mjs +73 -0
  20. package/packages/vault/src/index.mjs +1 -0
  21. package/packages/vault/src/memory-handoff.mjs +46 -5
  22. package/packages/vault/src/vault-path-safety.mjs +11 -0
  23. package/schema/wendkeep.evidence-envelope-v2.schema.json +92 -0
  24. package/schema/wendkeep.provenance-receipt-v2.schema.json +66 -0
  25. package/src/archive-operation-lock.mjs +235 -0
  26. package/src/change.mjs +1832 -48
  27. package/src/delivery.mjs +724 -67
  28. package/src/evidence-envelope.mjs +288 -0
  29. package/src/memory.mjs +2 -1
  30. package/src/provenance-gate.mjs +575 -0
  31. package/src/provenance-sources.mjs +547 -0
  32. package/src/receipt-ledger.mjs +841 -0
  33. package/src/release-provenance.mjs +48 -0
  34. package/src/skills-seed.mjs +11 -5
  35. package/src/verify.mjs +85 -22
  36. package/src/worktree-cleanup.mjs +1733 -118
  37. package/src/worktree.mjs +94 -5
@@ -5,6 +5,12 @@ import { basename, join, relative } from 'node:path';
5
5
 
6
6
  import { sanitizeMemoryText } from './memory-schema.mjs';
7
7
  import { scopeForMemoryKey } from './memory-scope.mjs';
8
+ import {
9
+ evaluateEvidenceBinding,
10
+ evidenceCheckoutBinding,
11
+ evidenceCheckoutBindingMatches,
12
+ evidenceSensors,
13
+ } from './evidence-envelope.mjs';
8
14
 
9
15
  const SHARED_HANDOFF_FIELDS = Object.freeze([
10
16
  ['objective', 'objective.current'],
@@ -177,9 +183,33 @@ export function collectLifecycleEvidence(vaultBase, { changeSlug = '', summary =
177
183
  };
178
184
  }
179
185
  const sensorPath = join(archivedDir, 'evidencia.json');
180
- const sensors = readJson(sensorPath);
181
- if (Array.isArray(sensors) && sensors.length && sensors.every((item) => item?.status === 'green')) {
186
+ const sensorEnvelope = readJson(sensorPath);
187
+ const sensors = evidenceSensors(sensorEnvelope);
188
+ if (sensors.length && sensors.every((item) => item?.status === 'green')) {
182
189
  evidence.sensors = [...new Set(sensors.map((item) => String(item.id || '')).filter(Boolean))].sort();
190
+ evidence.sensors_path = vaultRel(vaultBase, sensorPath);
191
+ const assessed = evaluateEvidenceBinding(sensorEnvelope, { change_slug: slug });
192
+ evidence.sensors_binding = assessed.state;
193
+ if (sensorEnvelope?.schema_version === 2 && assessed.state === 'bound') {
194
+ const checkoutBinding = evidenceCheckoutBinding(sensorEnvelope);
195
+ const verification = readJson(join(archivedDir, 'verificacao.json'));
196
+ const crossBound = verification?.evidenceEnvelopeId === sensorEnvelope.envelope_id
197
+ && verdict?.evidenceEnvelopeId === sensorEnvelope.envelope_id
198
+ && evidenceCheckoutBindingMatches(verification?.evidenceBinding, checkoutBinding)
199
+ && evidenceCheckoutBindingMatches(verdict?.evidenceBinding, checkoutBinding);
200
+ if (!crossBound) {
201
+ evidence.sensors_binding = 'stale';
202
+ evidence.sensors_binding_reasons = ['archived verification/verdict binding mismatch'];
203
+ }
204
+ } else if (assessed.reasons.length) {
205
+ evidence.sensors_binding_reasons = assessed.reasons;
206
+ }
207
+ if (sensorEnvelope?.schema_version === 2) {
208
+ evidence.sensors_envelope_id = sensorEnvelope.envelope_id;
209
+ evidence.sensors_tasks_hash = sensorEnvelope.tasks_sha256;
210
+ evidence.sensors_repository_id = sensorEnvelope.repository_id;
211
+ evidence.sensors_worktree_id = sensorEnvelope.worktree_id;
212
+ }
183
213
  }
184
214
  }
185
215
  }
@@ -279,12 +309,23 @@ export function buildSessionMemoryEvents({
279
309
  if (Array.isArray(evidence.sensors) && evidence.sensors.length) {
280
310
  events.push(makeEvent(context, {
281
311
  memoryKey: 'quality.latest-sensors',
282
- value: [...new Set(evidence.sensors.map(String))].sort(),
283
- authority: 'verified',
284
- evidence: evidence.sensors,
312
+ value: {
313
+ ids: [...new Set(evidence.sensors.map(String))].sort(),
314
+ evidence_state: evidence.sensors_binding || 'legacy-unbound',
315
+ ...(evidence.sensors_envelope_id ? { envelope_id: evidence.sensors_envelope_id } : {}),
316
+ ...(evidence.sensors_binding && !['bound', 'legacy-unbound'].includes(evidence.sensors_binding)
317
+ ? { recovery: 'reabra a change e rode wendkeep verify --deep + wk-verify' }
318
+ : {}),
319
+ },
320
+ authority: evidence.sensors_binding === 'bound' ? 'verified' : 'reported',
321
+ evidence: [evidence.sensors_path].filter(Boolean),
285
322
  scopeContext: {
286
323
  changeSlug: evidence.change?.slug || normalizedShared?.change_slug,
287
324
  tasksHash: evidence.sensors_tasks_hash || normalizedShared?.tasks_hash,
325
+ repositoryId: evidence.sensors_repository_id,
326
+ worktreeId: evidence.sensors_worktree_id,
327
+ evidenceEnvelopeId: evidence.sensors_envelope_id,
328
+ evidenceState: evidence.sensors_binding,
288
329
  },
289
330
  }));
290
331
  }
@@ -225,10 +225,20 @@ export function writeVaultFileSync(vaultBase, targetPath, content, encoding = 'u
225
225
  export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding = 'utf8', {
226
226
  label = 'arquivo atômico do Vault',
227
227
  code = 'VAULT_PATH_UNSAFE',
228
+ scopeRoot = '',
229
+ beforeRename,
228
230
  } = {}) {
229
231
  const checked = assertVaultPathSafe(vaultBase, targetPath, {
230
232
  expectedType: 'file', label, code,
231
233
  });
234
+ if (scopeRoot) {
235
+ const scope = assertVaultPathSafe(vaultBase, scopeRoot, {
236
+ allowMissing: false, expectedType: 'directory', label: `escopo de ${label}`, code,
237
+ });
238
+ if (!containedBy(scope.target, checked.target)) {
239
+ throw unsafe(`${label} escapa do escopo autorizado: ${checked.target}`, code);
240
+ }
241
+ }
232
242
  assertVaultPathSafe(vaultBase, dirname(checked.target), {
233
243
  allowMissing: false, expectedType: 'directory', label: `ancestral de ${label}`, code,
234
244
  });
@@ -245,6 +255,7 @@ export function writeVaultFileAtomic(vaultBase, targetPath, content, encoding =
245
255
  allowMissing: false, expectedType: 'file', label: `temporário de ${label}`, code,
246
256
  });
247
257
  assertVaultPathSafe(vaultBase, checked.target, { expectedType: 'file', label, code });
258
+ if (typeof beforeRename === 'function') beforeRename({ temporary: tmp, target: checked.target });
248
259
  renameSync(tmp, checked.target);
249
260
  tmpCreated = false;
250
261
  assertVaultPathSafe(vaultBase, checked.target, {
@@ -0,0 +1,92 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/rogersialves/wendkeep/schema/wendkeep.evidence-envelope-v2.schema.json",
4
+ "title": "WendKeep Evidence Envelope v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "project_id",
10
+ "repository_id",
11
+ "worktree_id",
12
+ "work_session_id",
13
+ "change_slug",
14
+ "branch",
15
+ "base_sha",
16
+ "head_sha",
17
+ "index_tree_sha",
18
+ "worktree_digest",
19
+ "dirty",
20
+ "tasks_sha256",
21
+ "effective_spec_sha256",
22
+ "sensor_config_sha256",
23
+ "wendkeep_version",
24
+ "platform",
25
+ "started_at",
26
+ "finished_at",
27
+ "sensors",
28
+ "envelope_id"
29
+ ],
30
+ "properties": {
31
+ "schema_version": { "const": 2 },
32
+ "project_id": { "type": "string", "minLength": 1 },
33
+ "repository_id": { "type": "string", "minLength": 1 },
34
+ "worktree_id": { "type": "string", "minLength": 1 },
35
+ "work_session_id": { "type": "string", "minLength": 1 },
36
+ "change_slug": { "type": "string", "minLength": 1 },
37
+ "branch": { "type": "string", "minLength": 1 },
38
+ "base_sha": { "$ref": "#/$defs/gitObject" },
39
+ "head_sha": { "$ref": "#/$defs/gitObject" },
40
+ "index_tree_sha": { "$ref": "#/$defs/gitObject" },
41
+ "worktree_digest": { "$ref": "#/$defs/sha256" },
42
+ "dirty": { "type": "boolean" },
43
+ "tasks_sha256": { "$ref": "#/$defs/sha256" },
44
+ "effective_spec_sha256": { "$ref": "#/$defs/sha256" },
45
+ "sensor_config_sha256": { "$ref": "#/$defs/sha256" },
46
+ "wendkeep_version": { "type": "string", "minLength": 1 },
47
+ "platform": { "type": "string", "minLength": 1 },
48
+ "started_at": { "type": "string", "format": "date-time" },
49
+ "finished_at": { "type": "string", "format": "date-time" },
50
+ "envelope_id": { "$ref": "#/$defs/sha256" },
51
+ "sensors": {
52
+ "type": "array",
53
+ "items": { "$ref": "#/$defs/sensor" }
54
+ }
55
+ },
56
+ "$defs": {
57
+ "gitObject": { "type": "string", "pattern": "^[a-f0-9]{40,64}$" },
58
+ "sha256": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
59
+ "sensor": {
60
+ "type": "object",
61
+ "required": [
62
+ "id",
63
+ "status",
64
+ "severity",
65
+ "command",
66
+ "command_sha256",
67
+ "started_at",
68
+ "finished_at",
69
+ "duration_ms",
70
+ "exit_code",
71
+ "output_sha256",
72
+ "output_tail"
73
+ ],
74
+ "properties": {
75
+ "id": { "type": "string", "minLength": 1 },
76
+ "status": { "enum": ["green", "red"] },
77
+ "severity": { "enum": ["critical", "warning"] },
78
+ "command": { "type": "string" },
79
+ "command_sha256": { "$ref": "#/$defs/sha256" },
80
+ "started_at": { "type": "string", "format": "date-time" },
81
+ "finished_at": { "type": "string", "format": "date-time" },
82
+ "duration_ms": { "type": "number", "minimum": 0 },
83
+ "exit_code": { "type": ["integer", "null"] },
84
+ "output_sha256": { "$ref": "#/$defs/sha256" },
85
+ "output_tail": { "type": "string", "maxLength": 2000 },
86
+ "note": { "type": "string", "maxLength": 2000 },
87
+ "survivors": { "type": "array" },
88
+ "ts": { "type": "string", "format": "date-time" }
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/rogersialves/wendkeep/blob/main/schema/wendkeep.provenance-receipt-v2.schema.json",
4
+ "title": "WendKeep Provenance Receipt v2",
5
+ "description": "A canonical, hash-chained receipt for a provenance-gated operation.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version",
10
+ "sequence",
11
+ "receipt_id",
12
+ "previous_hash",
13
+ "receipt_hash",
14
+ "kind",
15
+ "subject",
16
+ "claims",
17
+ "observations",
18
+ "recorded_at"
19
+ ],
20
+ "properties": {
21
+ "schema_version": {
22
+ "const": 2
23
+ },
24
+ "sequence": {
25
+ "type": "integer",
26
+ "minimum": 1
27
+ },
28
+ "receipt_id": {
29
+ "type": "string",
30
+ "pattern": "^sha256:[a-f0-9]{64}$"
31
+ },
32
+ "previous_hash": {
33
+ "type": "string",
34
+ "pattern": "^sha256:[a-f0-9]{64}$",
35
+ "description": "Hash of the previous v2 receipt, or the canonical legacy/empty prefix for genesis."
36
+ },
37
+ "receipt_hash": {
38
+ "type": "string",
39
+ "pattern": "^sha256:[a-f0-9]{64}$",
40
+ "description": "SHA-256 of the canonical receipt without receipt_hash."
41
+ },
42
+ "kind": {
43
+ "type": "string",
44
+ "pattern": "^[a-z][a-z0-9._-]{1,63}$"
45
+ },
46
+ "subject": {
47
+ "type": "object",
48
+ "description": "Sanitized identity and current operation target.",
49
+ "additionalProperties": true
50
+ },
51
+ "claims": {
52
+ "type": "object",
53
+ "description": "Sanitized claims made by the operation.",
54
+ "additionalProperties": true
55
+ },
56
+ "observations": {
57
+ "type": "object",
58
+ "description": "Sanitized source observations keyed by adapter/kind.",
59
+ "additionalProperties": true
60
+ },
61
+ "recorded_at": {
62
+ "type": "string",
63
+ "format": "date-time"
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,235 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync,
4
+ readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync,
5
+ } from 'node:fs';
6
+ import { basename, dirname, join, resolve } from 'node:path';
7
+
8
+ function archiveLockError(code, details = {}) {
9
+ const error = new Error(code);
10
+ error.code = code;
11
+ Object.assign(error, details);
12
+ return error;
13
+ }
14
+
15
+ function samePhysicalFile(left, right) {
16
+ if (!left?.isFile() || !right?.isFile() || left.ino !== right.ino) return false;
17
+ if (left.dev === right.dev) return true;
18
+ return process.platform === 'win32' && (left.dev === 0 || right.dev === 0);
19
+ }
20
+
21
+ function ownerProcessAlive(pid) {
22
+ try { process.kill(pid, 0); return true; }
23
+ catch (error) { return !['ESRCH', 'EINVAL'].includes(error?.code); }
24
+ }
25
+
26
+ function assertLockDirectory(target) {
27
+ const stat = lstatSync(target);
28
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
29
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
30
+ }
31
+ return stat;
32
+ }
33
+
34
+ function readOwnerMarker(markerPath) {
35
+ const stat = lstatSync(markerPath);
36
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
37
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
38
+ }
39
+ let record;
40
+ try { record = JSON.parse(readFileSync(markerPath, 'utf8')); }
41
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE'); }
42
+ if (record?.schema_version !== 1
43
+ || typeof record.owner_token !== 'string' || !record.owner_token
44
+ || !Number.isSafeInteger(record.owner_pid) || record.owner_pid <= 0
45
+ || typeof record.acquired_at !== 'string' || !Number.isFinite(Date.parse(record.acquired_at))) {
46
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
47
+ }
48
+ return { stat, record };
49
+ }
50
+
51
+ function inspectExistingLock(target) {
52
+ try { assertLockDirectory(target); }
53
+ catch (error) {
54
+ if (error?.code === 'ENOENT') return;
55
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
56
+ }
57
+ const entries = readdirSync(target);
58
+ if (entries.length === 0) {
59
+ try { rmdirSync(target); } catch { /* re-observe on the next bounded attempt */ }
60
+ return;
61
+ }
62
+ if (entries.length !== 1 || !/^owner\.[0-9a-f-]+\.json$/i.test(entries[0])) {
63
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
64
+ }
65
+ const markerPath = join(target, entries[0]);
66
+ const observed = readOwnerMarker(markerPath);
67
+ if (basename(markerPath) !== `owner.${observed.record.owner_token}.json`) {
68
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
69
+ }
70
+ if (ownerProcessAlive(observed.record.owner_pid)) {
71
+ throw archiveLockError('WENDKEEP_ARCHIVE_BUSY', { owner_state: 'live' });
72
+ }
73
+ const confirmed = readOwnerMarker(markerPath);
74
+ if (!samePhysicalFile(observed.stat, confirmed.stat)
75
+ || confirmed.record.owner_token !== observed.record.owner_token
76
+ || confirmed.record.owner_pid !== observed.record.owner_pid) {
77
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
78
+ }
79
+ try { unlinkSync(markerPath); }
80
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE'); }
81
+ try { rmdirSync(target); } catch { /* replacement/nonempty directory remains authoritative */ }
82
+ }
83
+
84
+ export function acquireArchiveOperationLock({
85
+ lockPath,
86
+ now = () => Date.now(),
87
+ maxAcquireAttempts = 3,
88
+ faultInjection = {},
89
+ }) {
90
+ if (!lockPath || !Number.isSafeInteger(maxAcquireAttempts) || maxAcquireAttempts < 1) {
91
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_INVALID');
92
+ }
93
+ const runtime = resolve(dirname(lockPath));
94
+ const target = resolve(lockPath);
95
+ mkdirSync(runtime, { recursive: true });
96
+ const runtimeStat = lstatSync(runtime);
97
+ if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()
98
+ || resolve(realpathSync(runtime)).toLowerCase() !== runtime.toLowerCase()) {
99
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
100
+ }
101
+
102
+ const token = randomUUID();
103
+ const markerName = `owner.${token}.json`;
104
+ const pending = `${target}.pending.${process.pid}.${token}`;
105
+ const pendingMarker = join(pending, markerName);
106
+ let descriptor;
107
+ let descriptorIdentity;
108
+ let pendingExists = false;
109
+ let published = false;
110
+ try {
111
+ mkdirSync(pending);
112
+ pendingExists = true;
113
+ descriptor = openSync(
114
+ pendingMarker,
115
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0),
116
+ 0o600,
117
+ );
118
+ writeFileSync(descriptor, `${JSON.stringify({
119
+ schema_version: 1,
120
+ owner_token: token,
121
+ owner_pid: process.pid,
122
+ acquired_at: new Date(now()).toISOString(),
123
+ })}\n`, 'utf8');
124
+ fsyncSync(descriptor);
125
+ closeSync(descriptor);
126
+ descriptor = undefined;
127
+
128
+ for (let attempt = 1; attempt <= maxAcquireAttempts; attempt += 1) {
129
+ try {
130
+ if (typeof faultInjection?.beforePublishRename === 'function') {
131
+ faultInjection.beforePublishRename({ attempt, pending, lockPath: target });
132
+ }
133
+ // The pending directory is already nonempty and durable. Rename publishes metadata and
134
+ // exclusivity together; the canonical namespace never observes a partial marker/hardlink.
135
+ renameSync(pending, target);
136
+ pendingExists = false;
137
+ published = true;
138
+ if (typeof faultInjection?.afterPublishRename === 'function') {
139
+ faultInjection.afterPublishRename({ attempt, lockPath: target });
140
+ }
141
+ break;
142
+ } catch (error) {
143
+ if (published) throw error;
144
+ if (existsSync(target)) inspectExistingLock(target);
145
+ if (attempt === maxAcquireAttempts) {
146
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
147
+ }
148
+ }
149
+ }
150
+
151
+ const markerPath = join(target, markerName);
152
+ descriptor = openSync(
153
+ markerPath,
154
+ constants.O_RDONLY | (constants.O_NOFOLLOW || 0),
155
+ );
156
+ const identity = readOwnerMarker(markerPath);
157
+ descriptorIdentity = fstatSync(descriptor);
158
+ if (!samePhysicalFile(descriptorIdentity, identity.stat)
159
+ || identity.record.owner_token !== token) {
160
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
161
+ }
162
+ } catch (error) {
163
+ if (descriptor !== undefined) {
164
+ try { closeSync(descriptor); } catch { /* original code remains authoritative */ }
165
+ descriptor = undefined;
166
+ }
167
+ if (pendingExists) {
168
+ try { unlinkSync(pendingMarker); } catch { /* marker may be absent */ }
169
+ try { rmdirSync(pending); } catch { /* private pending state is recoverable */ }
170
+ }
171
+ if (error?.code?.startsWith('WENDKEEP_ARCHIVE_')) throw error;
172
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_UNAVAILABLE');
173
+ }
174
+
175
+ const markerPath = join(target, markerName);
176
+ let terminal = false;
177
+ let assertionCount = 0;
178
+ const closeHandle = () => {
179
+ if (descriptor === undefined) return;
180
+ try { closeSync(descriptor); } finally { descriptor = undefined; }
181
+ };
182
+ const assertOwned = () => {
183
+ if (terminal || descriptor === undefined) {
184
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
185
+ }
186
+ try { assertLockDirectory(target); }
187
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST'); }
188
+ let identity;
189
+ try { identity = readOwnerMarker(markerPath); }
190
+ catch { throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST'); }
191
+ const currentDescriptorIdentity = fstatSync(descriptor);
192
+ if (!samePhysicalFile(currentDescriptorIdentity, identity.stat)
193
+ || identity.record.owner_token !== token) {
194
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
195
+ }
196
+ descriptorIdentity = currentDescriptorIdentity;
197
+ assertionCount += 1;
198
+ if (typeof faultInjection?.afterAssertOwned === 'function') {
199
+ faultInjection.afterAssertOwned({ count: assertionCount, lockPath: target, markerPath });
200
+ }
201
+ return true;
202
+ };
203
+
204
+ return Object.freeze({
205
+ token,
206
+ assertOwned,
207
+ release() {
208
+ if (terminal) return;
209
+ try {
210
+ assertOwned();
211
+ if (typeof faultInjection?.beforeReleaseCommit === 'function') {
212
+ faultInjection.beforeReleaseCommit({ lockPath: target, markerPath });
213
+ }
214
+ closeHandle();
215
+ const finalIdentity = readOwnerMarker(markerPath);
216
+ if (!samePhysicalFile(descriptorIdentity, finalIdentity.stat)
217
+ || finalIdentity.record.owner_token !== token) {
218
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
219
+ }
220
+ unlinkSync(markerPath);
221
+ rmdirSync(target);
222
+ } catch {
223
+ terminal = true;
224
+ closeHandle();
225
+ throw archiveLockError('WENDKEEP_ARCHIVE_LOCK_OWNERSHIP_LOST');
226
+ }
227
+ terminal = true;
228
+ closeHandle();
229
+ },
230
+ get active() {
231
+ if (terminal) return false;
232
+ try { return assertOwned(); } catch { return false; }
233
+ },
234
+ });
235
+ }