@reconcrap/boss-recommend-mcp 2.1.23 → 2.1.25

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 (67) hide show
  1. package/README.md +5 -0
  2. package/bin/boss-recommend-mcp.js +4 -4
  3. package/package.json +6 -1
  4. package/scripts/install-macos.sh +280 -280
  5. package/scripts/postinstall.cjs +44 -44
  6. package/skills/boss-chat/README.md +43 -42
  7. package/skills/boss-chat/SKILL.md +107 -106
  8. package/skills/boss-recommend-pipeline/README.md +13 -13
  9. package/skills/boss-recommend-pipeline/SKILL.md +47 -47
  10. package/skills/boss-recruit-pipeline/README.md +19 -19
  11. package/skills/boss-recruit-pipeline/SKILL.md +89 -89
  12. package/src/chat-mcp.js +301 -127
  13. package/src/chat-runtime-config.js +7 -5
  14. package/src/core/boss-cards/index.js +199 -199
  15. package/src/core/browser/index.js +302 -145
  16. package/src/core/capture/index.js +2930 -1201
  17. package/src/core/cv-acquisition/index.js +512 -238
  18. package/src/core/cv-capture-target/index.js +513 -299
  19. package/src/core/greet-quota/index.js +71 -71
  20. package/src/core/infinite-list/index.js +31 -31
  21. package/src/core/reporting/legacy-csv.js +12 -12
  22. package/src/core/run/detached-launcher.js +305 -0
  23. package/src/core/run/index.js +109 -55
  24. package/src/core/run/timing.js +33 -33
  25. package/src/core/run/windows-detached-worker.ps1 +106 -0
  26. package/src/core/screening/index.js +2400 -2135
  27. package/src/core/self-heal/index.js +989 -973
  28. package/src/core/self-heal/viewport.js +1505 -564
  29. package/src/detached-worker.js +99 -99
  30. package/src/domains/chat/action-journal.js +536 -0
  31. package/src/domains/chat/cards.js +137 -137
  32. package/src/domains/chat/constants.js +9 -9
  33. package/src/domains/chat/detail.js +1684 -401
  34. package/src/domains/chat/index.js +8 -7
  35. package/src/domains/chat/jobs.js +620 -620
  36. package/src/domains/chat/page-guard.js +157 -122
  37. package/src/domains/chat/roots.js +56 -56
  38. package/src/domains/chat/run-service.js +1801 -760
  39. package/src/domains/common/account-rights-panel.js +314 -314
  40. package/src/domains/common/recovery-settle.js +159 -159
  41. package/src/domains/recommend/actions.js +1219 -472
  42. package/src/domains/recommend/cards.js +243 -243
  43. package/src/domains/recommend/colleague-contact.js +1079 -333
  44. package/src/domains/recommend/constants.js +92 -92
  45. package/src/domains/recommend/detail.js +4037 -136
  46. package/src/domains/recommend/filters.js +608 -590
  47. package/src/domains/recommend/index.js +9 -9
  48. package/src/domains/recommend/jobs.js +571 -542
  49. package/src/domains/recommend/location.js +754 -707
  50. package/src/domains/recommend/refresh.js +677 -392
  51. package/src/domains/recommend/roots.js +80 -80
  52. package/src/domains/recommend/run-service.js +4048 -1447
  53. package/src/domains/recommend/scopes.js +246 -246
  54. package/src/domains/recruit/actions.js +277 -277
  55. package/src/domains/recruit/cards.js +74 -74
  56. package/src/domains/recruit/constants.js +236 -236
  57. package/src/domains/recruit/detail.js +588 -588
  58. package/src/domains/recruit/index.js +9 -9
  59. package/src/domains/recruit/instruction-parser.js +866 -866
  60. package/src/domains/recruit/refresh.js +45 -45
  61. package/src/domains/recruit/roots.js +68 -68
  62. package/src/domains/recruit/run-service.js +1817 -1620
  63. package/src/domains/recruit/search.js +3229 -3229
  64. package/src/index.js +16 -1
  65. package/src/parser.js +1296 -1296
  66. package/src/recommend-mcp.js +1061 -450
  67. package/src/recommend-scheduler.js +75 -75
@@ -0,0 +1,536 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ export const CHAT_ACTION_JOURNAL_SCHEMA_VERSION = 1;
7
+
8
+ export const CHAT_ACTION_STATES = Object.freeze([
9
+ "pre_action",
10
+ "greeting_send_in_flight",
11
+ "greeting_confirmed",
12
+ "request_in_flight",
13
+ "request_confirmed",
14
+ "outcome_unknown"
15
+ ]);
16
+
17
+ const CHAT_ACTION_STATE_SET = new Set(CHAT_ACTION_STATES);
18
+ const INITIAL_STATE = "pre_action";
19
+ const ALLOWED_TRANSITIONS = Object.freeze({
20
+ pre_action: new Set(["greeting_send_in_flight"]),
21
+ greeting_send_in_flight: new Set(["greeting_confirmed", "outcome_unknown"]),
22
+ greeting_confirmed: new Set(["request_in_flight"]),
23
+ request_in_flight: new Set(["request_confirmed", "outcome_unknown"]),
24
+ request_confirmed: new Set(),
25
+ outcome_unknown: new Set(["greeting_confirmed", "request_confirmed"])
26
+ });
27
+
28
+ function normalizeText(value) {
29
+ return String(value ?? "").trim();
30
+ }
31
+
32
+ function normalizeGreeting(value) {
33
+ return String(value ?? "").replace(/\s+/gu, " ").trim();
34
+ }
35
+
36
+ function sha256(value) {
37
+ return crypto.createHash("sha256").update(String(value), "utf8").digest("hex");
38
+ }
39
+
40
+ function journalError(code, message, details = {}) {
41
+ const error = new Error(message);
42
+ error.code = code;
43
+ Object.assign(error, details);
44
+ return error;
45
+ }
46
+
47
+ function requireScope(scope) {
48
+ const normalized = normalizeText(scope);
49
+ if (!normalized) {
50
+ throw journalError(
51
+ "CHAT_ACTION_SCOPE_REQUIRED",
52
+ "A stable Boss chat account/profile scope is required for the action journal."
53
+ );
54
+ }
55
+ return normalized;
56
+ }
57
+
58
+ function requireCandidateId(candidateId) {
59
+ const normalized = normalizeText(candidateId);
60
+ if (!normalized) {
61
+ throw journalError(
62
+ "CHAT_ACTION_CANDIDATE_ID_REQUIRED",
63
+ "A stable Boss candidate ID is required before an outbound chat action can be journaled."
64
+ );
65
+ }
66
+ return normalized;
67
+ }
68
+
69
+ function requireState(state) {
70
+ const normalized = normalizeText(state);
71
+ if (!CHAT_ACTION_STATE_SET.has(normalized)) {
72
+ throw journalError(
73
+ "CHAT_ACTION_STATE_INVALID",
74
+ `Unsupported Boss chat action journal state: ${normalized || "<empty>"}.`,
75
+ { state: normalized || null }
76
+ );
77
+ }
78
+ return normalized;
79
+ }
80
+
81
+ function defaultBaseDir() {
82
+ const configuredHome = normalizeText(process.env.BOSS_CHAT_HOME);
83
+ const chatHome = configuredHome || path.join(os.homedir(), ".boss-recommend-mcp", "boss-chat");
84
+ return path.join(chatHome, "action-journal");
85
+ }
86
+
87
+ function nowIso(now) {
88
+ const raw = now();
89
+ const date = raw instanceof Date ? raw : new Date(raw);
90
+ if (Number.isNaN(date.getTime())) {
91
+ throw journalError(
92
+ "CHAT_ACTION_JOURNAL_CLOCK_INVALID",
93
+ `Boss chat action journal clock returned an invalid value: ${String(raw)}.`
94
+ );
95
+ }
96
+ return date.toISOString();
97
+ }
98
+
99
+ function cloneRecord(record) {
100
+ return record == null ? null : JSON.parse(JSON.stringify(record));
101
+ }
102
+
103
+ function readJsonRecord(filePath) {
104
+ if (!fs.existsSync(filePath)) return null;
105
+ try {
106
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
107
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
108
+ throw new Error("journal payload is not an object");
109
+ }
110
+ return parsed;
111
+ } catch (error) {
112
+ throw journalError(
113
+ "CHAT_ACTION_JOURNAL_CORRUPT",
114
+ `Unable to read Boss chat action journal record: ${error?.message || error}.`,
115
+ { file_path: filePath }
116
+ );
117
+ }
118
+ }
119
+
120
+ function writeJsonAtomic(filePath, payload) {
121
+ const directory = path.dirname(filePath);
122
+ fs.mkdirSync(directory, { recursive: true });
123
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
124
+ let fileHandle = null;
125
+ try {
126
+ fileHandle = fs.openSync(tempPath, "wx", 0o600);
127
+ fs.writeFileSync(fileHandle, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
128
+ fs.fsyncSync(fileHandle);
129
+ fs.closeSync(fileHandle);
130
+ fileHandle = null;
131
+ fs.renameSync(tempPath, filePath);
132
+
133
+ // Directory fsync is unsupported on some Windows/filesystem combinations.
134
+ // The record itself has already been atomically replaced, so this is best-effort.
135
+ let directoryHandle = null;
136
+ try {
137
+ directoryHandle = fs.openSync(directory, "r");
138
+ fs.fsyncSync(directoryHandle);
139
+ } catch {
140
+ // Keep the portable temp+rename guarantee when directory fsync is unavailable.
141
+ } finally {
142
+ if (directoryHandle != null) fs.closeSync(directoryHandle);
143
+ }
144
+ } finally {
145
+ if (fileHandle != null) fs.closeSync(fileHandle);
146
+ try {
147
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
148
+ } catch {
149
+ // A leftover temp file is never treated as a committed journal record.
150
+ }
151
+ }
152
+ }
153
+
154
+ function acquireRecordLock(lockPath) {
155
+ let handle = null;
156
+ try {
157
+ handle = fs.openSync(lockPath, "wx", 0o600);
158
+ fs.writeFileSync(handle, `${process.pid}\n`, "utf8");
159
+ fs.fsyncSync(handle);
160
+ return handle;
161
+ } catch (error) {
162
+ if (handle != null) {
163
+ try {
164
+ fs.closeSync(handle);
165
+ } catch {}
166
+ try {
167
+ fs.unlinkSync(lockPath);
168
+ } catch {}
169
+ }
170
+ if (error?.code === "EEXIST") {
171
+ throw journalError(
172
+ "CHAT_ACTION_JOURNAL_BUSY",
173
+ "The Boss chat action journal record is locked by another writer; outbound action must fail closed.",
174
+ { lock_path: lockPath }
175
+ );
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ function releaseRecordLock(lockPath, handle) {
182
+ try {
183
+ if (handle != null) fs.closeSync(handle);
184
+ } finally {
185
+ try {
186
+ fs.unlinkSync(lockPath);
187
+ } catch (error) {
188
+ if (error?.code !== "ENOENT") throw error;
189
+ }
190
+ }
191
+ }
192
+
193
+ function outcomeUnknownOrigin(record) {
194
+ const history = Array.isArray(record?.history) ? record.history : [];
195
+ const last = history[history.length - 1];
196
+ return last?.state === "outcome_unknown" ? last.from_state : null;
197
+ }
198
+
199
+ function isAllowedTransition(record, nextState) {
200
+ if (!record) return nextState === INITIAL_STATE;
201
+ if (record.state === nextState) return true;
202
+ if (record.state === "outcome_unknown") {
203
+ const origin = outcomeUnknownOrigin(record);
204
+ return (
205
+ (origin === "greeting_send_in_flight" && nextState === "greeting_confirmed")
206
+ || (origin === "request_in_flight" && nextState === "request_confirmed")
207
+ );
208
+ }
209
+ return ALLOWED_TRANSITIONS[record.state]?.has(nextState) === true;
210
+ }
211
+
212
+ function validateStoredRecord(record, identity, filePath) {
213
+ const valid = (
214
+ record.schema_version === CHAT_ACTION_JOURNAL_SCHEMA_VERSION
215
+ && record.action_key === identity.actionKey
216
+ && record.scope_sha256 === identity.scopeSha256
217
+ && record.candidate_id === identity.candidateId
218
+ && CHAT_ACTION_STATE_SET.has(record.state)
219
+ && typeof record.greeting_sha256 === "string"
220
+ && record.greeting_sha256.length === 64
221
+ && Array.isArray(record.history)
222
+ );
223
+ if (!valid) {
224
+ throw journalError(
225
+ "CHAT_ACTION_JOURNAL_IDENTITY_MISMATCH",
226
+ "Boss chat action journal record identity or schema does not match the requested candidate.",
227
+ { file_path: filePath }
228
+ );
229
+ }
230
+ return record;
231
+ }
232
+
233
+ function appendRunId(runIds, runId) {
234
+ const normalized = normalizeText(runId);
235
+ const current = Array.isArray(runIds) ? runIds.filter(Boolean) : [];
236
+ if (!normalized || current.includes(normalized)) return current;
237
+ return [...current, normalized];
238
+ }
239
+
240
+ const EVIDENCE_KEYS = new Set([
241
+ "action",
242
+ "action_hit_test_attempt_count",
243
+ "action_hit_test_last_hit_backend_node_id",
244
+ "action_hit_test_reason",
245
+ "active_candidate_id",
246
+ "ask_error",
247
+ "ask_ok",
248
+ "confirm_confirmed",
249
+ "confirm_error",
250
+ "control_backend_node_id",
251
+ "control_center_x",
252
+ "control_center_y",
253
+ "control_label",
254
+ "control_node_id",
255
+ "control_root_backend_node_id",
256
+ "control_root_node_id",
257
+ "control_rect_height",
258
+ "control_rect_width",
259
+ "control_rect_x",
260
+ "control_rect_y",
261
+ "control_root",
262
+ "greeting_baseline_count",
263
+ "greeting_evidence_readable",
264
+ "message_observed",
265
+ "operation_id",
266
+ "pre_input_cdp_method",
267
+ "request_confirmation_source",
268
+ "request_ready_state_observed",
269
+ "reason",
270
+ "request_after_count",
271
+ "request_baseline_count",
272
+ "resume_attachment_after_count",
273
+ "resume_attachment_baseline_count",
274
+ "send_method"
275
+ ]);
276
+
277
+ function sanitizeEvidence(value = {}) {
278
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
279
+ const result = {};
280
+ for (const [key, raw] of Object.entries(value)) {
281
+ if (!EVIDENCE_KEYS.has(key)) continue;
282
+ if (typeof raw === "boolean" || typeof raw === "number" || raw === null) {
283
+ result[key] = raw;
284
+ continue;
285
+ }
286
+ if (typeof raw === "string") result[key] = raw.slice(0, 240);
287
+ }
288
+ return result;
289
+ }
290
+
291
+ function recordRevision(record) {
292
+ if (!record) return 0;
293
+ const historyLength = Array.isArray(record.history) ? record.history.length : 0;
294
+ const storedRevision = record.revision;
295
+ if (storedRevision == null) return historyLength;
296
+ const normalizedRevision = Number(storedRevision);
297
+ if (
298
+ !Number.isSafeInteger(normalizedRevision)
299
+ || normalizedRevision < 1
300
+ || normalizedRevision !== historyLength
301
+ ) {
302
+ throw journalError(
303
+ "CHAT_ACTION_JOURNAL_CORRUPT",
304
+ "Boss chat action journal revision does not exactly match its append-only history.",
305
+ {
306
+ stored_revision: storedRevision,
307
+ history_length: historyLength
308
+ }
309
+ );
310
+ }
311
+ return normalizedRevision;
312
+ }
313
+
314
+ function withRecordRevision(record) {
315
+ if (!record) return null;
316
+ return {
317
+ ...record,
318
+ revision: recordRevision(record)
319
+ };
320
+ }
321
+
322
+ export function hashChatActionGreeting(greeting) {
323
+ const normalized = normalizeGreeting(greeting);
324
+ if (!normalized) {
325
+ throw journalError(
326
+ "CHAT_ACTION_GREETING_REQUIRED",
327
+ "A non-empty greeting is required to initialize a Boss chat outbound action journal record."
328
+ );
329
+ }
330
+ return sha256(`boss-chat-greeting-v1\u0000${normalized}`);
331
+ }
332
+
333
+ export function createChatActionIdentity({ scope, candidateId } = {}) {
334
+ const normalizedScope = requireScope(scope);
335
+ const normalizedCandidateId = requireCandidateId(candidateId);
336
+ const scopeSha256 = sha256(`boss-chat-scope-v1\u0000${normalizedScope}`);
337
+ const actionKey = sha256(
338
+ `boss-chat-request-cv-v1\u0000${normalizedScope}\u0000${normalizedCandidateId}`
339
+ );
340
+ return {
341
+ actionKey,
342
+ scopeSha256,
343
+ candidateId: normalizedCandidateId
344
+ };
345
+ }
346
+
347
+ export function createChatActionJournal({
348
+ baseDir = defaultBaseDir(),
349
+ now = () => new Date()
350
+ } = {}) {
351
+ const resolvedBaseDir = path.resolve(normalizeText(baseDir) || defaultBaseDir());
352
+ if (typeof now !== "function") {
353
+ throw journalError(
354
+ "CHAT_ACTION_JOURNAL_CLOCK_INVALID",
355
+ "Boss chat action journal now must be a function."
356
+ );
357
+ }
358
+
359
+ function entryPath(input = {}) {
360
+ const identity = createChatActionIdentity(input);
361
+ return path.join(resolvedBaseDir, `${identity.actionKey}.json`);
362
+ }
363
+
364
+ function read(input = {}) {
365
+ const identity = createChatActionIdentity(input);
366
+ const filePath = path.join(resolvedBaseDir, `${identity.actionKey}.json`);
367
+ const stored = readJsonRecord(filePath);
368
+ if (!stored) return null;
369
+ return cloneRecord(withRecordRevision(validateStoredRecord(stored, identity, filePath)));
370
+ }
371
+
372
+ function transition({
373
+ scope,
374
+ candidateId,
375
+ state,
376
+ runId = "",
377
+ greeting,
378
+ evidence = {},
379
+ recordIdempotent = false,
380
+ expectedUpdatedAt = null,
381
+ expectedRevision = null
382
+ } = {}) {
383
+ const identity = createChatActionIdentity({ scope, candidateId });
384
+ const nextState = requireState(state);
385
+ const filePath = path.join(resolvedBaseDir, `${identity.actionKey}.json`);
386
+ const lockPath = `${filePath}.lock`;
387
+ fs.mkdirSync(resolvedBaseDir, { recursive: true });
388
+ const lockHandle = acquireRecordLock(lockPath);
389
+ try {
390
+ const stored = readJsonRecord(filePath);
391
+ const existing = stored
392
+ ? withRecordRevision(validateStoredRecord(stored, identity, filePath))
393
+ : null;
394
+ const observedRevision = recordRevision(existing);
395
+ if (expectedRevision != null) {
396
+ const normalizedExpectedRevision = Number(expectedRevision);
397
+ if (!Number.isSafeInteger(normalizedExpectedRevision) || normalizedExpectedRevision < 0) {
398
+ throw journalError(
399
+ "CHAT_ACTION_JOURNAL_REVISION_INVALID",
400
+ "Boss action journal expected revision must be a non-negative safe integer.",
401
+ { expected_revision: expectedRevision }
402
+ );
403
+ }
404
+ if (observedRevision !== normalizedExpectedRevision) {
405
+ throw journalError(
406
+ "CHAT_ACTION_JOURNAL_CONCURRENT_UPDATE",
407
+ "Boss action journal changed after it was read; this operation does not own the outbound action.",
408
+ {
409
+ action_key: identity.actionKey,
410
+ expected_revision: normalizedExpectedRevision,
411
+ observed_revision: observedRevision,
412
+ expected_updated_at: normalizeText(expectedUpdatedAt) || null,
413
+ observed_updated_at: normalizeText(existing?.updated_at) || null
414
+ }
415
+ );
416
+ }
417
+ }
418
+ if (
419
+ expectedUpdatedAt != null
420
+ && normalizeText(existing?.updated_at) !== normalizeText(expectedUpdatedAt)
421
+ ) {
422
+ throw journalError(
423
+ "CHAT_ACTION_JOURNAL_CONCURRENT_UPDATE",
424
+ "Boss action journal changed after it was read; this operation does not own the outbound action.",
425
+ {
426
+ action_key: identity.actionKey,
427
+ expected_updated_at: normalizeText(expectedUpdatedAt) || null,
428
+ observed_updated_at: normalizeText(existing?.updated_at) || null
429
+ }
430
+ );
431
+ }
432
+ const suppliedGreeting = greeting == null ? "" : normalizeGreeting(greeting);
433
+ const suppliedGreetingSha256 = suppliedGreeting ? hashChatActionGreeting(suppliedGreeting) : "";
434
+
435
+ if (!existing && !suppliedGreetingSha256) {
436
+ throw journalError(
437
+ "CHAT_ACTION_GREETING_REQUIRED",
438
+ "The initial Boss chat action journal transition must include the greeting to hash."
439
+ );
440
+ }
441
+ if (
442
+ existing
443
+ && suppliedGreetingSha256
444
+ && suppliedGreetingSha256 !== existing.greeting_sha256
445
+ ) {
446
+ throw journalError(
447
+ "CHAT_ACTION_GREETING_HASH_CONFLICT",
448
+ "The greeting does not match the greeting already journaled for this candidate.",
449
+ { action_key: identity.actionKey }
450
+ );
451
+ }
452
+ if (!isAllowedTransition(existing, nextState)) {
453
+ throw journalError(
454
+ "CHAT_ACTION_TRANSITION_INVALID",
455
+ `Invalid Boss chat action journal transition: ${existing?.state || "<none>"} -> ${nextState}.`,
456
+ {
457
+ action_key: identity.actionKey,
458
+ current_state: existing?.state || null,
459
+ requested_state: nextState
460
+ }
461
+ );
462
+ }
463
+ if (existing?.state === nextState && recordIdempotent !== true) {
464
+ return {
465
+ changed: false,
466
+ idempotent: true,
467
+ file_path: filePath,
468
+ record: cloneRecord(existing)
469
+ };
470
+ }
471
+
472
+ const at = nowIso(now);
473
+ const normalizedRunId = normalizeText(runId);
474
+ const safeEvidence = sanitizeEvidence(evidence);
475
+ const record = existing ? {
476
+ ...existing,
477
+ state: nextState,
478
+ revision: observedRevision + 1,
479
+ updated_at: at,
480
+ evidence: {
481
+ ...(existing.evidence && typeof existing.evidence === "object" ? existing.evidence : {}),
482
+ ...safeEvidence
483
+ },
484
+ last_run_id: normalizedRunId || existing.last_run_id || null,
485
+ run_ids: appendRunId(existing.run_ids, normalizedRunId),
486
+ history: [
487
+ ...existing.history,
488
+ {
489
+ from_state: existing.state,
490
+ state: nextState,
491
+ at,
492
+ run_id: normalizedRunId || null,
493
+ evidence: safeEvidence
494
+ }
495
+ ]
496
+ } : {
497
+ schema_version: CHAT_ACTION_JOURNAL_SCHEMA_VERSION,
498
+ action_key: identity.actionKey,
499
+ scope_sha256: identity.scopeSha256,
500
+ candidate_id: identity.candidateId,
501
+ state: nextState,
502
+ revision: 1,
503
+ greeting_sha256: suppliedGreetingSha256,
504
+ evidence: safeEvidence,
505
+ created_at: at,
506
+ updated_at: at,
507
+ first_run_id: normalizedRunId || null,
508
+ last_run_id: normalizedRunId || null,
509
+ run_ids: appendRunId([], normalizedRunId),
510
+ history: [{
511
+ from_state: null,
512
+ state: nextState,
513
+ at,
514
+ run_id: normalizedRunId || null,
515
+ evidence: safeEvidence
516
+ }]
517
+ };
518
+ writeJsonAtomic(filePath, record);
519
+ return {
520
+ changed: true,
521
+ idempotent: false,
522
+ file_path: filePath,
523
+ record: cloneRecord(record)
524
+ };
525
+ } finally {
526
+ releaseRecordLock(lockPath, lockHandle);
527
+ }
528
+ }
529
+
530
+ return Object.freeze({
531
+ base_dir: resolvedBaseDir,
532
+ entryPath,
533
+ read,
534
+ transition
535
+ });
536
+ }