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.
- package/.github/plugin/marketplace.json +2 -2
- package/.github/skills/sdd-change/SKILL.md +28 -45
- package/CHANGELOG.md +126 -0
- package/README-ja.md +171 -5
- package/README.md +185 -8
- package/dist/packages/analysis/src/adapters.d.ts +2 -2
- package/dist/packages/analysis/src/adapters.js +33 -3
- package/dist/packages/analysis/src/adapters.js.map +1 -1
- package/dist/packages/analysis/src/approval-record.js +4 -0
- package/dist/packages/analysis/src/approval-record.js.map +1 -1
- package/dist/packages/analysis/src/approval.d.ts +51 -0
- package/dist/packages/analysis/src/approval.js +341 -2
- package/dist/packages/analysis/src/approval.js.map +1 -1
- package/dist/packages/analysis/src/attestation.js +4 -0
- package/dist/packages/analysis/src/attestation.js.map +1 -1
- package/dist/packages/analysis/src/change-evidence.d.ts +65 -8
- package/dist/packages/analysis/src/change-evidence.js +173 -46
- package/dist/packages/analysis/src/change-evidence.js.map +1 -1
- package/dist/packages/analysis/src/change-waiver.d.ts +46 -13
- package/dist/packages/analysis/src/change-waiver.js +481 -152
- package/dist/packages/analysis/src/change-waiver.js.map +1 -1
- package/dist/packages/analysis/src/change.d.ts +1 -0
- package/dist/packages/analysis/src/change.js +194 -66
- package/dist/packages/analysis/src/change.js.map +1 -1
- package/dist/packages/analysis/src/evidence-merge-guard.d.ts +9 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js +62 -0
- package/dist/packages/analysis/src/evidence-merge-guard.js.map +1 -0
- package/dist/packages/analysis/src/evidence-merge.d.ts +55 -0
- package/dist/packages/analysis/src/evidence-merge.js +1182 -0
- package/dist/packages/analysis/src/evidence-merge.js.map +1 -0
- package/dist/packages/analysis/src/evidence-writer-lock.d.ts +189 -0
- package/dist/packages/analysis/src/evidence-writer-lock.js +843 -0
- package/dist/packages/analysis/src/evidence-writer-lock.js.map +1 -0
- package/dist/packages/analysis/src/files.js +11 -0
- package/dist/packages/analysis/src/files.js.map +1 -1
- package/dist/packages/analysis/src/formal.js +7 -0
- package/dist/packages/analysis/src/formal.js.map +1 -1
- package/dist/packages/analysis/src/gate.d.ts +1 -1
- package/dist/packages/analysis/src/gate.js +23 -6
- package/dist/packages/analysis/src/gate.js.map +1 -1
- package/dist/packages/analysis/src/graph.js +6 -0
- package/dist/packages/analysis/src/graph.js.map +1 -1
- package/dist/packages/analysis/src/index.d.ts +3 -0
- package/dist/packages/analysis/src/index.js +3 -0
- package/dist/packages/analysis/src/index.js.map +1 -1
- package/dist/packages/analysis/src/knowledge.js +4 -0
- package/dist/packages/analysis/src/knowledge.js.map +1 -1
- package/dist/packages/analysis/src/model-correspondence.js +5 -1
- package/dist/packages/analysis/src/model-correspondence.js.map +1 -1
- package/dist/packages/analysis/src/mutation.js +4 -0
- package/dist/packages/analysis/src/mutation.js.map +1 -1
- package/dist/packages/analysis/src/order.d.ts +16 -1
- package/dist/packages/analysis/src/order.js +10 -0
- package/dist/packages/analysis/src/order.js.map +1 -1
- package/dist/packages/analysis/src/performance.js +5 -1
- package/dist/packages/analysis/src/performance.js.map +1 -1
- package/dist/packages/analysis/src/quality-refresh.d.ts +18 -0
- package/dist/packages/analysis/src/quality-refresh.js +312 -0
- package/dist/packages/analysis/src/quality-refresh.js.map +1 -0
- package/dist/packages/analysis/src/tdd.d.ts +86 -3
- package/dist/packages/analysis/src/tdd.js +604 -52
- package/dist/packages/analysis/src/tdd.js.map +1 -1
- package/dist/packages/analysis/src/trace.d.ts +24 -0
- package/dist/packages/analysis/src/trace.js +101 -3
- package/dist/packages/analysis/src/trace.js.map +1 -1
- package/dist/packages/analysis/src/workflow-waiver.d.ts +6 -2
- package/dist/packages/analysis/src/workflow-waiver.js +9 -5
- package/dist/packages/analysis/src/workflow-waiver.js.map +1 -1
- package/dist/packages/analysis/src/workflow.d.ts +1 -1
- package/dist/packages/analysis/src/workflow.js +103 -21
- package/dist/packages/analysis/src/workflow.js.map +1 -1
- package/dist/packages/cli/src/install.js +51 -6
- package/dist/packages/cli/src/install.js.map +1 -1
- package/dist/packages/cli/src/main.js +252 -79
- package/dist/packages/cli/src/main.js.map +1 -1
- package/dist/packages/domain/src/design.js +8 -1
- package/dist/packages/domain/src/design.js.map +1 -1
- package/dist/packages/domain/src/types.d.ts +1 -0
- package/package.json +7 -2
- 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
|