musubix3 0.1.19 → 0.1.21

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 (80) hide show
  1. package/.github/plugin/marketplace.json +2 -2
  2. package/.github/skills/sdd-change/SKILL.md +28 -45
  3. package/CHANGELOG.md +126 -0
  4. package/README-ja.md +171 -5
  5. package/README.md +185 -8
  6. package/dist/packages/analysis/src/adapters.d.ts +2 -2
  7. package/dist/packages/analysis/src/adapters.js +33 -3
  8. package/dist/packages/analysis/src/adapters.js.map +1 -1
  9. package/dist/packages/analysis/src/approval-record.js +4 -0
  10. package/dist/packages/analysis/src/approval-record.js.map +1 -1
  11. package/dist/packages/analysis/src/approval.d.ts +51 -0
  12. package/dist/packages/analysis/src/approval.js +341 -2
  13. package/dist/packages/analysis/src/approval.js.map +1 -1
  14. package/dist/packages/analysis/src/attestation.js +4 -0
  15. package/dist/packages/analysis/src/attestation.js.map +1 -1
  16. package/dist/packages/analysis/src/change-evidence.d.ts +65 -8
  17. package/dist/packages/analysis/src/change-evidence.js +173 -46
  18. package/dist/packages/analysis/src/change-evidence.js.map +1 -1
  19. package/dist/packages/analysis/src/change-waiver.d.ts +46 -13
  20. package/dist/packages/analysis/src/change-waiver.js +481 -152
  21. package/dist/packages/analysis/src/change-waiver.js.map +1 -1
  22. package/dist/packages/analysis/src/change.d.ts +1 -0
  23. package/dist/packages/analysis/src/change.js +194 -66
  24. package/dist/packages/analysis/src/change.js.map +1 -1
  25. package/dist/packages/analysis/src/evidence-merge-guard.d.ts +9 -0
  26. package/dist/packages/analysis/src/evidence-merge-guard.js +62 -0
  27. package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -0
  28. package/dist/packages/analysis/src/evidence-merge.d.ts +55 -0
  29. package/dist/packages/analysis/src/evidence-merge.js +1182 -0
  30. package/dist/packages/analysis/src/evidence-merge.js.map +1 -0
  31. package/dist/packages/analysis/src/evidence-writer-lock.d.ts +189 -0
  32. package/dist/packages/analysis/src/evidence-writer-lock.js +843 -0
  33. package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -0
  34. package/dist/packages/analysis/src/files.js +11 -0
  35. package/dist/packages/analysis/src/files.js.map +1 -1
  36. package/dist/packages/analysis/src/formal.js +7 -0
  37. package/dist/packages/analysis/src/formal.js.map +1 -1
  38. package/dist/packages/analysis/src/gate.d.ts +1 -1
  39. package/dist/packages/analysis/src/gate.js +23 -6
  40. package/dist/packages/analysis/src/gate.js.map +1 -1
  41. package/dist/packages/analysis/src/graph.js +6 -0
  42. package/dist/packages/analysis/src/graph.js.map +1 -1
  43. package/dist/packages/analysis/src/index.d.ts +3 -0
  44. package/dist/packages/analysis/src/index.js +3 -0
  45. package/dist/packages/analysis/src/index.js.map +1 -1
  46. package/dist/packages/analysis/src/knowledge.js +4 -0
  47. package/dist/packages/analysis/src/knowledge.js.map +1 -1
  48. package/dist/packages/analysis/src/model-correspondence.js +5 -1
  49. package/dist/packages/analysis/src/model-correspondence.js.map +1 -1
  50. package/dist/packages/analysis/src/mutation.js +4 -0
  51. package/dist/packages/analysis/src/mutation.js.map +1 -1
  52. package/dist/packages/analysis/src/order.d.ts +16 -1
  53. package/dist/packages/analysis/src/order.js +10 -0
  54. package/dist/packages/analysis/src/order.js.map +1 -1
  55. package/dist/packages/analysis/src/performance.js +5 -1
  56. package/dist/packages/analysis/src/performance.js.map +1 -1
  57. package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
  58. package/dist/packages/analysis/src/quality-refresh.js +312 -0
  59. package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
  60. package/dist/packages/analysis/src/tdd.d.ts +86 -3
  61. package/dist/packages/analysis/src/tdd.js +604 -52
  62. package/dist/packages/analysis/src/tdd.js.map +1 -1
  63. package/dist/packages/analysis/src/trace.d.ts +24 -0
  64. package/dist/packages/analysis/src/trace.js +101 -3
  65. package/dist/packages/analysis/src/trace.js.map +1 -1
  66. package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
  67. package/dist/packages/analysis/src/workflow-waiver.js +9 -5
  68. package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
  69. package/dist/packages/analysis/src/workflow.d.ts +1 -1
  70. package/dist/packages/analysis/src/workflow.js +103 -21
  71. package/dist/packages/analysis/src/workflow.js.map +1 -1
  72. package/dist/packages/cli/src/install.js +51 -6
  73. package/dist/packages/cli/src/install.js.map +1 -1
  74. package/dist/packages/cli/src/main.js +252 -79
  75. package/dist/packages/cli/src/main.js.map +1 -1
  76. package/dist/packages/domain/src/design.js +8 -1
  77. package/dist/packages/domain/src/design.js.map +1 -1
  78. package/dist/packages/domain/src/types.d.ts +1 -0
  79. package/package.json +7 -2
  80. package/plugin.json +1 -1
@@ -0,0 +1,843 @@
1
+ /** @id CODE-EVIDENCE-WRITER-LOCK-001
2
+ * @implements REQ-EVIDENCE-WRITER-LOCK-001 REQ-EVIDENCE-WRITER-LOCK-002 REQ-EVIDENCE-WRITER-LOCK-003 REQ-EVIDENCE-WRITER-LOCK-004 REQ-EVIDENCE-WRITER-LOCK-005
3
+ * @design DES-EVIDENCE-WRITER-LOCK-001 DES-EVIDENCE-WRITER-LOCK-002 DES-EVIDENCE-WRITER-LOCK-004
4
+ */
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { closeSync, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, readlinkSync, realpathSync, unlinkSync, writeFileSync, } from 'node:fs';
8
+ import { lstat, open, readFile, readlink, rmdir, unlink, } from 'node:fs/promises';
9
+ import { hostname as systemHostname } from 'node:os';
10
+ import { dirname, relative, resolve, sep } from 'node:path';
11
+ import { assertEvidenceMergeReady } from './evidence-merge-guard.js';
12
+ const LOCK_RELATIVE_PATH = '.musubix/evidence/.writer-lock.json';
13
+ const STAGING_PREFIX = '.writer-lock.';
14
+ export const EVIDENCE_OPERATION_CLASSIFICATION = {
15
+ writer: [
16
+ 'gate', 'evidence refresh', 'tdd red', 'tdd green', 'tdd refactor', 'tdd migrate', 'tdd void', 'tdd archive',
17
+ 'change waiver record', 'workflow-record', 'workflow-verify',
18
+ 'workflow waiver record', 'workflow waiver record-all', 'approval record',
19
+ 'evidence merge --recover', 'change quality-recover', 'trace build', 'graph index', 'graph gate', 'knowledge build',
20
+ 'formal check', 'formal generate',
21
+ ],
22
+ coordinatedReader: [
23
+ 'status', 'approval prepare', 'approval validate', 'design validate', 'design c4',
24
+ 'trace check', 'trace impact', 'graph impact', 'graph cycles', 'knowledge query',
25
+ 'mutation validate', 'model-correspondence validate', 'attestation payload',
26
+ 'attestation verify', 'tdd validate',
27
+ ],
28
+ exempt: [
29
+ 'help', 'version', 'plugin-install', 'requirements validate', 'requirements scaffold',
30
+ 'constitution validate', 'design scaffold', 'formal doctor', 'config lint', 'config scaffold',
31
+ 'mutation doctor', 'mutation identity', 'workflow-sanitize', 'attestation oidc-audience',
32
+ 'evidence unlock --recover',
33
+ ],
34
+ conditional: ['init', 'install', 'upgrade', 'change-record', 'evidence merge'],
35
+ };
36
+ export const EVIDENCE_CONDITIONAL_OPERATION_MODES = [
37
+ { operation: 'init', options: {}, classification: 'writer' },
38
+ { operation: 'init', options: { dryRun: true }, classification: 'coordinated-reader' },
39
+ { operation: 'install', options: {}, classification: 'writer' },
40
+ { operation: 'install', options: { dryRun: true }, classification: 'coordinated-reader' },
41
+ { operation: 'upgrade', options: {}, classification: 'writer' },
42
+ { operation: 'upgrade', options: { dryRun: true }, classification: 'coordinated-reader' },
43
+ { operation: 'change-record', options: {}, classification: 'writer' },
44
+ { operation: 'change-record', options: { dryRun: true }, classification: 'coordinated-reader' },
45
+ { operation: 'evidence merge', options: {}, classification: 'writer' },
46
+ { operation: 'evidence merge', options: { dryRun: true }, classification: 'coordinated-reader' },
47
+ ];
48
+ export const EVIDENCE_WRITER_ANALYSIS_ENTRIES = [
49
+ 'adapter output capture',
50
+ 'adapter output clear',
51
+ 'approval record',
52
+ 'change waiver record',
53
+ 'change-record',
54
+ 'change quality-recover',
55
+ 'evidence merge',
56
+ 'evidence merge --recover',
57
+ 'evidence order append',
58
+ 'formal check',
59
+ 'formal generate',
60
+ 'gate',
61
+ 'graph index',
62
+ 'knowledge build',
63
+ 'model-correspondence evidence generate',
64
+ 'mutation evidence generate',
65
+ 'performance evidence generate',
66
+ 'protected write',
67
+ 'tdd <phase>',
68
+ 'tdd archive',
69
+ 'tdd migrate',
70
+ 'tdd void',
71
+ 'trace build',
72
+ 'workflow waiver record',
73
+ 'workflow waiver record-all',
74
+ 'workflow-record',
75
+ 'workflow-verify',
76
+ ];
77
+ export function classifyEvidenceOperation(operation, options = {}) {
78
+ if (operation === 'init' || operation === 'install' || operation === 'upgrade') {
79
+ return options.dryRun ? 'coordinated-reader' : 'writer';
80
+ }
81
+ if (operation === 'change-record') {
82
+ return options.dryRun ? 'coordinated-reader' : 'writer';
83
+ }
84
+ if (operation === 'evidence merge') {
85
+ return options.dryRun ? 'coordinated-reader' : 'writer';
86
+ }
87
+ if (EVIDENCE_OPERATION_CLASSIFICATION.writer.includes(operation))
88
+ return 'writer';
89
+ if (EVIDENCE_OPERATION_CLASSIFICATION.coordinatedReader.includes(operation)) {
90
+ return 'coordinated-reader';
91
+ }
92
+ if (EVIDENCE_OPERATION_CLASSIFICATION.exempt.includes(operation))
93
+ return 'exempt';
94
+ throw new Error(`Unclassified evidence operation: ${operation}`);
95
+ }
96
+ export class EvidenceWriterLockError extends Error {
97
+ code;
98
+ lockPath;
99
+ owner;
100
+ guidance;
101
+ constructor(code, message, options) {
102
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
103
+ this.name = 'EvidenceWriterLockError';
104
+ this.code = code;
105
+ this.lockPath = options.lockPath;
106
+ this.owner = options.owner;
107
+ this.guidance = options.guidance;
108
+ }
109
+ }
110
+ export class EvidenceProtectedOutputError extends Error {
111
+ code = 'EVIDENCE_PROTECTED_OUTPUT_REJECTED';
112
+ path;
113
+ constructor(path) {
114
+ super(`Exempt operation output must not target protected project state: ${path}`);
115
+ this.name = 'EvidenceProtectedOutputError';
116
+ this.path = path;
117
+ }
118
+ }
119
+ const ownerContext = new AsyncLocalStorage();
120
+ export function resolveEvidenceWriterCanonicalRoot(root) {
121
+ return realpathSync.native(resolve(root));
122
+ }
123
+ function contextCanonicalRoot(root) {
124
+ const context = ownerContext.getStore();
125
+ if (context === undefined)
126
+ return resolveEvidenceWriterCanonicalRoot(root);
127
+ const absolute = resolve(root);
128
+ const cached = context.canonicalRoots.get(absolute);
129
+ if (cached !== undefined)
130
+ return cached;
131
+ const canonicalRoot = context.resolveCanonicalRoot(absolute);
132
+ if (context.resolveCanonicalRoot(absolute) !== canonicalRoot) {
133
+ throw new Error(`Canonical evidence root changed during coordination: ${absolute}`);
134
+ }
135
+ context.canonicalRoots.set(absolute, canonicalRoot);
136
+ return canonicalRoot;
137
+ }
138
+ export function resolveEvidenceWriterCoordinatedRoot(root) {
139
+ return contextCanonicalRoot(root);
140
+ }
141
+ function errno(cause) {
142
+ return cause instanceof Error && 'code' in cause
143
+ ? String(cause.code)
144
+ : undefined;
145
+ }
146
+ function lockPath(canonicalRoot) {
147
+ return resolve(canonicalRoot, LOCK_RELATIVE_PATH);
148
+ }
149
+ function evidenceDirectory(canonicalRoot) {
150
+ return resolve(canonicalRoot, '.musubix/evidence');
151
+ }
152
+ const WINDOWS_UNSUPPORTED_DIRECTORY_SYNC_ERRORS = new Set(['EPERM', 'EINVAL', 'ENOTSUP']);
153
+ function isUnsupportedDirectorySync(cause, platform) {
154
+ return platform === 'win32' && WINDOWS_UNSUPPORTED_DIRECTORY_SYNC_ERRORS.has(errno(cause) ?? '');
155
+ }
156
+ async function syncDirectory(path, platform, policy, synchronize) {
157
+ const handle = await open(path, 'r');
158
+ try {
159
+ try {
160
+ if (synchronize)
161
+ await synchronize(path, policy);
162
+ else
163
+ await handle.sync();
164
+ }
165
+ catch (cause) {
166
+ if (policy !== 'allow-unsupported' || !isUnsupportedDirectorySync(cause, platform))
167
+ throw cause;
168
+ }
169
+ }
170
+ finally {
171
+ await handle.close();
172
+ }
173
+ }
174
+ async function syncRecoveredEvidenceDirectoryStrict(canonicalRoot, dependencies) {
175
+ await syncDirectory(evidenceDirectory(canonicalRoot), (dependencies.platform ?? (() => process.platform))(), 'strict', dependencies.syncEvidenceDirectory);
176
+ }
177
+ function syncDirectorySync(path, platform, policy, synchronize) {
178
+ const descriptor = openSync(path, 'r');
179
+ try {
180
+ try {
181
+ if (synchronize)
182
+ synchronize(path, policy);
183
+ else
184
+ fsyncSync(descriptor);
185
+ }
186
+ catch (cause) {
187
+ if (policy !== 'allow-unsupported' || !isUnsupportedDirectorySync(cause, platform))
188
+ throw cause;
189
+ }
190
+ }
191
+ finally {
192
+ closeSync(descriptor);
193
+ }
194
+ }
195
+ function isLinuxFingerprint(value) {
196
+ return value.platform === 'linux';
197
+ }
198
+ function parseProcessStart(stat) {
199
+ const commandEnd = stat.lastIndexOf(')');
200
+ if (commandEnd === -1)
201
+ throw new Error('Malformed Linux process stat.');
202
+ const fieldsAfterCommand = stat.slice(commandEnd + 2).trim().split(/\s+/);
203
+ const processStart = fieldsAfterCommand[19];
204
+ if (processStart === undefined)
205
+ throw new Error('Linux process stat has no start identity.');
206
+ return processStart;
207
+ }
208
+ async function linuxFingerprint(pid = process.pid) {
209
+ const [bootId, pidNamespace, stat] = await Promise.all([
210
+ readFile('/proc/sys/kernel/random/boot_id', 'utf8'),
211
+ readlink(`/proc/${pid}/ns/pid`),
212
+ readFile(`/proc/${pid}/stat`, 'utf8'),
213
+ ]);
214
+ return {
215
+ platform: 'linux',
216
+ bootId: bootId.trim(),
217
+ pidNamespace,
218
+ processStart: parseProcessStart(stat),
219
+ };
220
+ }
221
+ async function defaultProcessFingerprint() {
222
+ if (process.platform === 'linux')
223
+ return linuxFingerprint();
224
+ return {
225
+ platform: process.platform,
226
+ supported: false,
227
+ };
228
+ }
229
+ function defaultProcessFingerprintSync() {
230
+ if (process.platform === 'linux') {
231
+ return {
232
+ platform: 'linux',
233
+ bootId: readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(),
234
+ pidNamespace: readlinkSync(`/proc/${process.pid}/ns/pid`),
235
+ processStart: parseProcessStart(readFileSync(`/proc/${process.pid}/stat`, 'utf8')),
236
+ };
237
+ }
238
+ return {
239
+ platform: process.platform,
240
+ supported: false,
241
+ };
242
+ }
243
+ function isRecord(value) {
244
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
245
+ }
246
+ function parseFingerprint(value) {
247
+ if (!isRecord(value) || typeof value.platform !== 'string')
248
+ return undefined;
249
+ if (value.platform === 'linux') {
250
+ return typeof value.bootId === 'string'
251
+ && typeof value.pidNamespace === 'string'
252
+ && typeof value.processStart === 'string'
253
+ ? {
254
+ platform: 'linux',
255
+ bootId: value.bootId,
256
+ pidNamespace: value.pidNamespace,
257
+ processStart: value.processStart,
258
+ }
259
+ : undefined;
260
+ }
261
+ return value.supported === false
262
+ ? {
263
+ platform: value.platform,
264
+ supported: false,
265
+ }
266
+ : undefined;
267
+ }
268
+ function parseOwner(value) {
269
+ if (!isRecord(value))
270
+ return undefined;
271
+ const processFingerprint = parseFingerprint(value.processFingerprint);
272
+ if (value.schemaVersion !== 1
273
+ || typeof value.canonicalRoot !== 'string'
274
+ || typeof value.command !== 'string'
275
+ || typeof value.pid !== 'number'
276
+ || !Number.isSafeInteger(value.pid)
277
+ || typeof value.hostname !== 'string'
278
+ || typeof value.acquiredAt !== 'string'
279
+ || typeof value.transactionId !== 'string'
280
+ || processFingerprint === undefined) {
281
+ return undefined;
282
+ }
283
+ return {
284
+ schemaVersion: 1,
285
+ canonicalRoot: value.canonicalRoot,
286
+ command: value.command,
287
+ pid: value.pid,
288
+ hostname: value.hostname,
289
+ acquiredAt: value.acquiredAt,
290
+ transactionId: value.transactionId,
291
+ processFingerprint,
292
+ };
293
+ }
294
+ async function readOwner(path) {
295
+ return parseOwnerText(path, await readFile(path, 'utf8'));
296
+ }
297
+ function parseOwnerText(path, raw) {
298
+ let value;
299
+ try {
300
+ value = JSON.parse(raw);
301
+ }
302
+ catch (cause) {
303
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_RECOVERY_UNSAFE', `Evidence writer lock metadata is malformed: ${path}`, { lockPath: path, cause });
304
+ }
305
+ const owner = parseOwner(value);
306
+ if (owner === undefined) {
307
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_RECOVERY_UNSAFE', `Evidence writer lock metadata is incomplete: ${path}`, { lockPath: path });
308
+ }
309
+ return owner;
310
+ }
311
+ async function defaultLockObservation(path) {
312
+ const handle = await open(path, 'r');
313
+ try {
314
+ const [raw, stats] = await Promise.all([handle.readFile('utf8'), handle.stat()]);
315
+ return {
316
+ owner: parseOwnerText(path, raw),
317
+ identity: { dev: stats.dev, ino: stats.ino },
318
+ };
319
+ }
320
+ finally {
321
+ await handle.close();
322
+ }
323
+ }
324
+ async function defaultLockIdentity(path) {
325
+ const stats = await lstat(path);
326
+ return { dev: stats.dev, ino: stats.ino };
327
+ }
328
+ async function observeLock(path, dependencies) {
329
+ if (dependencies.lockOwner === undefined && dependencies.lockIdentity === undefined) {
330
+ return defaultLockObservation(path);
331
+ }
332
+ const [owner, identity] = await Promise.all([
333
+ (dependencies.lockOwner ?? readOwner)(path),
334
+ (dependencies.lockIdentity ?? defaultLockIdentity)(path),
335
+ ]);
336
+ return { owner, identity };
337
+ }
338
+ async function readContendingOwner(path) {
339
+ for (let attempt = 0; attempt < 2; attempt += 1) {
340
+ try {
341
+ return await readOwner(path);
342
+ }
343
+ catch (cause) {
344
+ if (attempt === 1 || errno(cause) !== 'ENOENT')
345
+ return undefined;
346
+ }
347
+ }
348
+ return undefined;
349
+ }
350
+ function lockedError(path, owner) {
351
+ const resolvedOwner = owner ?? {
352
+ schemaVersion: 'unknown',
353
+ canonicalRoot: 'unknown',
354
+ command: 'unknown',
355
+ pid: 'unknown',
356
+ hostname: 'unknown',
357
+ acquiredAt: 'unknown',
358
+ transactionId: 'unknown',
359
+ processFingerprint: 'unknown',
360
+ };
361
+ const description = owner === undefined
362
+ ? 'owner metadata unavailable'
363
+ : `${owner.command} (pid ${owner.pid}, host ${owner.hostname}, acquired ${owner.acquiredAt}, transaction ${owner.transactionId})`;
364
+ return new EvidenceWriterLockError('EVIDENCE_WRITER_LOCKED', `Evidence writer lock is held at ${path}: ${description}.`, { lockPath: path, owner: resolvedOwner });
365
+ }
366
+ function activeLease(canonicalRoot) {
367
+ const state = ownerContext.getStore()?.leases.get(canonicalRoot);
368
+ return state !== undefined && state.references > 0 ? state : undefined;
369
+ }
370
+ function protectedPath(root, path) {
371
+ const rel = relative(resolve(root), resolve(root, path)).split(sep).join('/');
372
+ return rel === '.musubix/evidence'
373
+ || rel.startsWith('.musubix/evidence/')
374
+ || rel === '.musubix/cache'
375
+ || rel.startsWith('.musubix/cache/')
376
+ || /^\.musubix\/features\/[^/]+\/trace\.json$/.test(rel);
377
+ }
378
+ function configuredReportPattern(path) {
379
+ const escaped = path
380
+ .split(/(\{testId\}|\{testPath\}|\{reportPath\})/)
381
+ .map((part) => {
382
+ if (part === '{testId}')
383
+ return '[^/]+';
384
+ if (part === '{testPath}' || part === '{reportPath}')
385
+ return '.+';
386
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
387
+ })
388
+ .join('');
389
+ return new RegExp(`^${escaped}$`);
390
+ }
391
+ async function configuredReportPath(root, path) {
392
+ const canonicalRoot = contextCanonicalRoot(root);
393
+ const rel = relative(canonicalRoot, resolve(canonicalRoot, path)).split(sep).join('/');
394
+ try {
395
+ const raw = JSON.parse(await readFile(resolve(canonicalRoot, '.musubix/config.json'), 'utf8'));
396
+ if (!isRecord(raw) || !Array.isArray(raw.commands))
397
+ return false;
398
+ for (const command of raw.commands) {
399
+ if (!isRecord(command))
400
+ continue;
401
+ for (const key of ['tddReport', 'testReport', 'mutationReport']) {
402
+ const report = command[key];
403
+ if (isRecord(report) && typeof report.path === 'string' && configuredReportPattern(report.path).test(rel)) {
404
+ return true;
405
+ }
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+ catch (cause) {
411
+ if (errno(cause) === 'ENOENT' || cause instanceof SyntaxError)
412
+ return false;
413
+ throw cause;
414
+ }
415
+ }
416
+ function canonicalizeRootForAcquisition(root, resolveCanonicalRoot = resolveEvidenceWriterCanonicalRoot) {
417
+ const absolute = resolve(root);
418
+ try {
419
+ return { canonicalRoot: resolveCanonicalRoot(absolute), rootExisted: true };
420
+ }
421
+ catch (cause) {
422
+ if (errno(cause) !== 'ENOENT')
423
+ throw cause;
424
+ }
425
+ let ancestor = dirname(absolute);
426
+ while (!existsSync(ancestor)) {
427
+ const parent = dirname(ancestor);
428
+ if (parent === ancestor)
429
+ throw new Error(`No existing ancestor for project root: ${absolute}`);
430
+ ancestor = parent;
431
+ }
432
+ const canonicalAncestor = resolveCanonicalRoot(ancestor);
433
+ return {
434
+ canonicalRoot: resolve(canonicalAncestor, relative(ancestor, absolute)),
435
+ rootExisted: false,
436
+ };
437
+ }
438
+ async function removeOwnPublishedLock(path, transactionId) {
439
+ try {
440
+ const owner = await readOwner(path);
441
+ if (owner.transactionId === transactionId)
442
+ await unlink(path);
443
+ }
444
+ catch (cause) {
445
+ if (errno(cause) !== 'ENOENT')
446
+ throw cause;
447
+ }
448
+ }
449
+ async function removeCreatedDirectoryIfEmpty(path, existed) {
450
+ if (existed)
451
+ return;
452
+ try {
453
+ await rmdir(path);
454
+ }
455
+ catch (cause) {
456
+ if (!['ENOENT', 'ENOTEMPTY'].includes(errno(cause) ?? ''))
457
+ throw cause;
458
+ }
459
+ }
460
+ /** @id CODE-EVIDENCE-WRITER-LOCK-002
461
+ * @implements REQ-EVIDENCE-WRITER-LOCK-001 REQ-EVIDENCE-WRITER-LOCK-002 REQ-EVIDENCE-WRITER-LOCK-004
462
+ * @design DES-EVIDENCE-WRITER-LOCK-001
463
+ */
464
+ export async function acquireEvidenceWriterLock(root, command, dependencies = {}) {
465
+ let canonicalRoot = resolve(root);
466
+ let rootExisted = true;
467
+ let directory = evidenceDirectory(canonicalRoot);
468
+ let canonicalLockPath = lockPath(canonicalRoot);
469
+ let musubixDirectory = resolve(canonicalRoot, '.musubix');
470
+ let musubixDirectoryExisted = true;
471
+ let evidenceDirectoryExisted = true;
472
+ let transactionId = '';
473
+ let stagingPath = '';
474
+ let owner;
475
+ let publishedIdentity;
476
+ let published = false;
477
+ try {
478
+ ({ canonicalRoot, rootExisted } = canonicalizeRootForAcquisition(root));
479
+ directory = evidenceDirectory(canonicalRoot);
480
+ canonicalLockPath = lockPath(canonicalRoot);
481
+ musubixDirectory = resolve(canonicalRoot, '.musubix');
482
+ musubixDirectoryExisted = existsSync(musubixDirectory);
483
+ evidenceDirectoryExisted = existsSync(directory);
484
+ transactionId = randomUUID();
485
+ stagingPath = resolve(directory, `${STAGING_PREFIX}${transactionId}.json`);
486
+ const processFingerprint = dependencies.processFingerprint === undefined
487
+ ? defaultProcessFingerprintSync()
488
+ : await dependencies.processFingerprint();
489
+ owner = {
490
+ schemaVersion: 1,
491
+ canonicalRoot,
492
+ command,
493
+ pid: process.pid,
494
+ hostname: (dependencies.hostname ?? systemHostname)(),
495
+ acquiredAt: new Date().toISOString(),
496
+ transactionId,
497
+ processFingerprint,
498
+ };
499
+ mkdirSync(directory, { recursive: true });
500
+ const staging = openSync(stagingPath, 'wx', 0o600);
501
+ try {
502
+ writeFileSync(staging, `${JSON.stringify(owner, null, 2)}\n`, 'utf8');
503
+ fsyncSync(staging);
504
+ }
505
+ finally {
506
+ closeSync(staging);
507
+ }
508
+ try {
509
+ linkSync(stagingPath, canonicalLockPath);
510
+ published = true;
511
+ }
512
+ catch (cause) {
513
+ if (errno(cause) === 'EEXIST') {
514
+ throw lockedError(canonicalLockPath, await readContendingOwner(canonicalLockPath));
515
+ }
516
+ throw cause;
517
+ }
518
+ syncDirectorySync(directory, (dependencies.platform ?? (() => process.platform))(), 'allow-unsupported', dependencies.syncEvidenceDirectorySync);
519
+ publishedIdentity = await (dependencies.lockIdentity ?? defaultLockIdentity)(canonicalLockPath);
520
+ }
521
+ catch (cause) {
522
+ if (published) {
523
+ try {
524
+ await removeOwnPublishedLock(canonicalLockPath, transactionId);
525
+ }
526
+ catch {
527
+ // The original acquisition failure remains primary.
528
+ }
529
+ }
530
+ if (cause instanceof EvidenceWriterLockError)
531
+ throw cause;
532
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to acquire evidence writer lock at ${canonicalLockPath}.`, { lockPath: canonicalLockPath, cause });
533
+ }
534
+ finally {
535
+ if (stagingPath !== '') {
536
+ try {
537
+ unlinkSync(stagingPath);
538
+ }
539
+ catch (cause) {
540
+ if (errno(cause) !== 'ENOENT' && !published) {
541
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to clean evidence writer lock staging file at ${stagingPath}.`, { lockPath: canonicalLockPath, cause });
542
+ }
543
+ }
544
+ }
545
+ if (!published) {
546
+ try {
547
+ await removeCreatedDirectoryIfEmpty(directory, evidenceDirectoryExisted);
548
+ await removeCreatedDirectoryIfEmpty(musubixDirectory, musubixDirectoryExisted);
549
+ await removeCreatedDirectoryIfEmpty(canonicalRoot, rootExisted);
550
+ }
551
+ catch {
552
+ // Preserve the original acquisition or contention error.
553
+ }
554
+ }
555
+ }
556
+ if (owner === undefined || publishedIdentity === undefined) {
557
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to construct evidence writer lock owner at ${canonicalLockPath}.`, { lockPath: canonicalLockPath });
558
+ }
559
+ let released = false;
560
+ return {
561
+ owner,
562
+ async release() {
563
+ if (released)
564
+ return;
565
+ try {
566
+ const { owner: current, identity: currentIdentity } = await observeLock(canonicalLockPath, dependencies);
567
+ if (current.canonicalRoot !== canonicalRoot
568
+ || current.transactionId !== transactionId
569
+ || currentIdentity.dev !== publishedIdentity.dev
570
+ || currentIdentity.ino !== publishedIdentity.ino) {
571
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_RELEASE_FAILED', `Refusing to release a replaced evidence writer lock at ${canonicalLockPath}.`, { lockPath: canonicalLockPath, owner: current });
572
+ }
573
+ await unlink(canonicalLockPath);
574
+ await syncDirectory(directory, (dependencies.platform ?? (() => process.platform))(), 'allow-unsupported', dependencies.syncEvidenceDirectory);
575
+ released = true;
576
+ }
577
+ catch (cause) {
578
+ if (cause instanceof EvidenceWriterLockError)
579
+ throw cause;
580
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_RELEASE_FAILED', `Failed to release evidence writer lock at ${canonicalLockPath}.`, { lockPath: canonicalLockPath, owner, cause });
581
+ }
582
+ try {
583
+ await removeCreatedDirectoryIfEmpty(directory, evidenceDirectoryExisted);
584
+ await removeCreatedDirectoryIfEmpty(musubixDirectory, musubixDirectoryExisted);
585
+ await removeCreatedDirectoryIfEmpty(canonicalRoot, rootExisted);
586
+ }
587
+ catch {
588
+ // Directory cleanup is best-effort and does not affect lock release.
589
+ }
590
+ },
591
+ };
592
+ }
593
+ async function useContextLease(context, canonicalRoot, command, operation, dependencies) {
594
+ let state = context.leases.get(canonicalRoot);
595
+ if (state === undefined) {
596
+ const acquisition = acquireEvidenceWriterLock(canonicalRoot, command, dependencies);
597
+ acquisition.catch(() => undefined);
598
+ state = { acquisition, references: 0 };
599
+ context.leases.set(canonicalRoot, state);
600
+ }
601
+ let lease;
602
+ try {
603
+ lease = await state.acquisition;
604
+ }
605
+ catch (cause) {
606
+ if (context.leases.get(canonicalRoot) === state)
607
+ context.leases.delete(canonicalRoot);
608
+ throw cause;
609
+ }
610
+ state.references += 1;
611
+ let operationError;
612
+ try {
613
+ return await operation();
614
+ }
615
+ catch (cause) {
616
+ operationError = cause;
617
+ throw cause;
618
+ }
619
+ finally {
620
+ state.references -= 1;
621
+ if (state.references === 0 && context.leases.get(canonicalRoot) === state) {
622
+ context.leases.delete(canonicalRoot);
623
+ try {
624
+ await lease.release();
625
+ }
626
+ catch (releaseError) {
627
+ if (operationError === undefined)
628
+ throw releaseError;
629
+ if (operationError instanceof Error) {
630
+ Object.defineProperty(operationError, 'evidenceWriterReleaseError', {
631
+ configurable: true,
632
+ enumerable: false,
633
+ value: releaseError,
634
+ });
635
+ }
636
+ }
637
+ }
638
+ }
639
+ }
640
+ /** @id CODE-EVIDENCE-WRITER-LOCK-003
641
+ * @implements REQ-EVIDENCE-WRITER-LOCK-003 REQ-EVIDENCE-WRITER-LOCK-004
642
+ * @design DES-EVIDENCE-WRITER-LOCK-002
643
+ */
644
+ export async function withEvidenceWriterLock(root, command, operation, dependencies = {}) {
645
+ const existing = ownerContext.getStore();
646
+ const resolveCanonicalRoot = existing?.resolveCanonicalRoot
647
+ ?? dependencies.resolveCanonicalRoot
648
+ ?? resolveEvidenceWriterCanonicalRoot;
649
+ let canonicalRoot;
650
+ try {
651
+ ({ canonicalRoot } = canonicalizeRootForAcquisition(root, resolveCanonicalRoot));
652
+ }
653
+ catch (cause) {
654
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to resolve evidence writer project root: ${resolve(root)}.`, { lockPath: lockPath(resolve(root)), cause });
655
+ }
656
+ if (existing !== undefined) {
657
+ return useContextLease(existing, canonicalRoot, command, operation, dependencies);
658
+ }
659
+ const context = {
660
+ token: Symbol('evidence-writer-owner'),
661
+ leases: new Map(),
662
+ resolveCanonicalRoot,
663
+ canonicalRoots: new Map(),
664
+ };
665
+ return ownerContext.run(context, () => useContextLease(context, canonicalRoot, command, operation, dependencies));
666
+ }
667
+ /** @id CODE-EVIDENCE-WRITER-LOCK-004
668
+ * @implements REQ-EVIDENCE-WRITER-LOCK-001 REQ-EVIDENCE-WRITER-LOCK-002
669
+ * @design DES-EVIDENCE-WRITER-LOCK-001
670
+ */
671
+ export async function readEvidenceWriterLock(root) {
672
+ const canonicalRoot = contextCanonicalRoot(root);
673
+ const path = lockPath(canonicalRoot);
674
+ try {
675
+ return { locked: true, lockPath: path, owner: await readOwner(path) };
676
+ }
677
+ catch (cause) {
678
+ if (errno(cause) === 'ENOENT')
679
+ return { locked: false, lockPath: path };
680
+ throw cause;
681
+ }
682
+ }
683
+ /** @id CODE-EVIDENCE-WRITER-LOCK-005
684
+ * @implements REQ-EVIDENCE-WRITER-LOCK-003
685
+ * @design DES-EVIDENCE-WRITER-LOCK-002
686
+ */
687
+ export async function assertEvidenceWriterOwned(root) {
688
+ const canonicalRoot = contextCanonicalRoot(root);
689
+ const state = activeLease(canonicalRoot);
690
+ if (state === undefined) {
691
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_CONTEXT_LOST', `Evidence writer ownership context is unavailable for ${canonicalRoot}.`, { lockPath: lockPath(canonicalRoot) });
692
+ }
693
+ await state.acquisition;
694
+ }
695
+ /** @id CODE-EVIDENCE-WRITER-LOCK-007
696
+ * @implements REQ-EVIDENCE-WRITER-LOCK-002 REQ-EVIDENCE-WRITER-LOCK-003
697
+ * @design DES-EVIDENCE-WRITER-LOCK-002 DES-EVIDENCE-WRITER-LOCK-003
698
+ */
699
+ export async function assertCoordinatedEvidenceRead(root) {
700
+ let canonicalRoot;
701
+ try {
702
+ canonicalRoot = contextCanonicalRoot(root);
703
+ }
704
+ catch (cause) {
705
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to resolve evidence reader project root: ${resolve(root)}.`, { lockPath: lockPath(resolve(root)), cause });
706
+ }
707
+ if (activeLease(canonicalRoot) !== undefined)
708
+ return;
709
+ const path = lockPath(canonicalRoot);
710
+ try {
711
+ await lstat(path);
712
+ }
713
+ catch (cause) {
714
+ if (errno(cause) === 'ENOENT')
715
+ return;
716
+ throw new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_ACQUIRE_FAILED', `Failed to inspect evidence writer lock at ${path}.`, { lockPath: path, cause });
717
+ }
718
+ throw lockedError(path, await readContendingOwner(path));
719
+ }
720
+ /** @id CODE-EVIDENCE-WRITER-LOCK-008
721
+ * @implements REQ-EVIDENCE-WRITER-LOCK-001 REQ-EVIDENCE-WRITER-LOCK-002 REQ-EVIDENCE-WRITER-LOCK-003 REQ-EVIDENCE-WRITER-LOCK-005
722
+ * @design DES-EVIDENCE-WRITER-LOCK-002 DES-EVIDENCE-WRITER-LOCK-003 DES-EVIDENCE-WRITER-LOCK-004
723
+ */
724
+ export async function assertProtectedPathReady(root, path, access) {
725
+ if (!protectedPath(root, path) && !await configuredReportPath(root, path))
726
+ return;
727
+ if (access === 'write')
728
+ await assertEvidenceWriterOwned(root);
729
+ else
730
+ await assertCoordinatedEvidenceRead(root);
731
+ await assertEvidenceMergeReady(root);
732
+ }
733
+ export async function withProtectedEvidenceWrite(root, path, operation) {
734
+ if (!protectedPath(root, path) && !await configuredReportPath(root, path))
735
+ return operation();
736
+ const canonicalRoot = contextCanonicalRoot(root);
737
+ if (activeLease(canonicalRoot) !== undefined)
738
+ return operation();
739
+ return withEvidenceWriterLock(root, 'protected write', operation);
740
+ }
741
+ export async function assertEvidenceOutputUnprotected(root, path) {
742
+ if (protectedPath(root, path) || await configuredReportPath(root, path)) {
743
+ throw new EvidenceProtectedOutputError(resolve(path));
744
+ }
745
+ }
746
+ async function defaultProcessState(owner, currentFingerprint) {
747
+ if (!isLinuxFingerprint(currentFingerprint) || !isLinuxFingerprint(owner.processFingerprint)) {
748
+ return 'indeterminate';
749
+ }
750
+ try {
751
+ const target = await linuxFingerprint(owner.pid);
752
+ return target.processStart === owner.processFingerprint.processStart ? 'live' : 'reused';
753
+ }
754
+ catch (cause) {
755
+ return errno(cause) === 'ENOENT' ? 'dead' : 'indeterminate';
756
+ }
757
+ }
758
+ function recoveryUnsafe(path, owner, reason, cause) {
759
+ const guidance = [
760
+ `Inspect ${path} and confirm that no related process is active.`,
761
+ `If recovery remains unsafe, back up the file and remove only ${path} after manual verification.`,
762
+ ];
763
+ return new EvidenceWriterLockError('EVIDENCE_WRITER_LOCK_RECOVERY_UNSAFE', `Cannot safely recover evidence writer lock at ${path}: ${reason} ${guidance.join(' ')}`, {
764
+ lockPath: path,
765
+ ...(owner === undefined ? {} : { owner }),
766
+ ...(cause === undefined ? {} : { cause }),
767
+ guidance,
768
+ });
769
+ }
770
+ /** @id CODE-EVIDENCE-WRITER-LOCK-006
771
+ * @implements REQ-EVIDENCE-WRITER-LOCK-004 REQ-EVIDENCE-WRITER-LOCK-005
772
+ * @design DES-EVIDENCE-WRITER-LOCK-004
773
+ */
774
+ export async function recoverEvidenceWriterLock(root, dependencies = {}) {
775
+ const canonicalRoot = (dependencies.resolveCanonicalRoot
776
+ ?? resolveEvidenceWriterCanonicalRoot)(root);
777
+ const path = lockPath(canonicalRoot);
778
+ let owner;
779
+ let initialIdentity;
780
+ try {
781
+ ({ owner, identity: initialIdentity } = await observeLock(path, dependencies));
782
+ }
783
+ catch (cause) {
784
+ if (errno(cause) === 'ENOENT') {
785
+ return { action: 'nothing-to-recover', recovered: false };
786
+ }
787
+ throw recoveryUnsafe(path, undefined, 'owner metadata could not be inspected.', cause);
788
+ }
789
+ const currentHostname = (dependencies.hostname ?? systemHostname)();
790
+ if (owner.hostname !== currentHostname) {
791
+ throw recoveryUnsafe(path, owner, 'the lock belongs to a different host.');
792
+ }
793
+ if (owner.canonicalRoot !== canonicalRoot) {
794
+ throw recoveryUnsafe(path, owner, 'the recorded canonical root does not match.');
795
+ }
796
+ let currentFingerprint;
797
+ try {
798
+ currentFingerprint = await (dependencies.processFingerprint ?? defaultProcessFingerprint)();
799
+ }
800
+ catch (cause) {
801
+ throw recoveryUnsafe(path, owner, 'the current process fingerprint is unavailable.', cause);
802
+ }
803
+ if (!isLinuxFingerprint(owner.processFingerprint)
804
+ || !isLinuxFingerprint(currentFingerprint)
805
+ || owner.processFingerprint.bootId !== currentFingerprint.bootId
806
+ || owner.processFingerprint.pidNamespace !== currentFingerprint.pidNamespace) {
807
+ throw recoveryUnsafe(path, owner, 'host process identity cannot be matched conclusively.');
808
+ }
809
+ let processState;
810
+ try {
811
+ processState = await (dependencies.processState ?? defaultProcessState)(owner, currentFingerprint);
812
+ }
813
+ catch (cause) {
814
+ throw recoveryUnsafe(path, owner, 'owner process liveness could not be determined.', cause);
815
+ }
816
+ if (processState === 'live')
817
+ throw lockedError(path, owner);
818
+ if (processState !== 'dead') {
819
+ throw recoveryUnsafe(path, owner, `owner process state is ${processState}.`);
820
+ }
821
+ let finalOwner;
822
+ let finalIdentity;
823
+ try {
824
+ ({ owner: finalOwner, identity: finalIdentity } = await observeLock(path, dependencies));
825
+ }
826
+ catch (cause) {
827
+ throw recoveryUnsafe(path, owner, 'the lock could not be revalidated before removal.', cause);
828
+ }
829
+ if (finalOwner.transactionId !== owner.transactionId
830
+ || finalIdentity.dev !== initialIdentity.dev
831
+ || finalIdentity.ino !== initialIdentity.ino) {
832
+ throw recoveryUnsafe(path, finalOwner, 'the lock changed during recovery.');
833
+ }
834
+ try {
835
+ await unlink(path);
836
+ await syncRecoveredEvidenceDirectoryStrict(canonicalRoot, dependencies);
837
+ }
838
+ catch (cause) {
839
+ throw recoveryUnsafe(path, owner, 'the verified lock could not be removed.', cause);
840
+ }
841
+ return { action: 'recovered', recovered: true };
842
+ }
843
+ //# sourceMappingURL=evidence-writer-lock.js.map