@skillsmith/core 0.11.4 → 0.11.5

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 (183) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/api/client.d.ts.map +1 -1
  5. package/dist/src/api/client.js +15 -5
  6. package/dist/src/api/client.js.map +1 -1
  7. package/dist/src/api/client.private-registry.d.ts +80 -0
  8. package/dist/src/api/client.private-registry.d.ts.map +1 -0
  9. package/dist/src/api/client.private-registry.js +97 -0
  10. package/dist/src/api/client.private-registry.js.map +1 -0
  11. package/dist/src/api/client.private-registry.test.d.ts +9 -0
  12. package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
  13. package/dist/src/api/client.private-registry.test.js +127 -0
  14. package/dist/src/api/client.private-registry.test.js.map +1 -0
  15. package/dist/src/api/client.toSkill.test.d.ts +25 -0
  16. package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
  17. package/dist/src/api/client.toSkill.test.js +97 -0
  18. package/dist/src/api/client.toSkill.test.js.map +1 -0
  19. package/dist/src/api/client.token-refresh.d.ts +8 -0
  20. package/dist/src/api/client.token-refresh.d.ts.map +1 -1
  21. package/dist/src/api/client.token-refresh.js +26 -0
  22. package/dist/src/api/client.token-refresh.js.map +1 -1
  23. package/dist/src/api/client.token-refresh.test.js +82 -0
  24. package/dist/src/api/client.token-refresh.test.js.map +1 -1
  25. package/dist/src/api/index.d.ts +2 -0
  26. package/dist/src/api/index.d.ts.map +1 -1
  27. package/dist/src/api/index.js +9 -0
  28. package/dist/src/api/index.js.map +1 -1
  29. package/dist/src/api/security-summary.d.ts +73 -0
  30. package/dist/src/api/security-summary.d.ts.map +1 -0
  31. package/dist/src/api/security-summary.js +88 -0
  32. package/dist/src/api/security-summary.js.map +1 -0
  33. package/dist/src/api/security-summary.test.d.ts +9 -0
  34. package/dist/src/api/security-summary.test.d.ts.map +1 -0
  35. package/dist/src/api/security-summary.test.js +94 -0
  36. package/dist/src/api/security-summary.test.js.map +1 -0
  37. package/dist/src/config/config-atomic-write.d.ts +30 -10
  38. package/dist/src/config/config-atomic-write.d.ts.map +1 -1
  39. package/dist/src/config/config-atomic-write.js +33 -80
  40. package/dist/src/config/config-atomic-write.js.map +1 -1
  41. package/dist/src/config/config-atomic-write.test.d.ts +8 -0
  42. package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
  43. package/dist/src/config/config-atomic-write.test.js +64 -12
  44. package/dist/src/config/config-atomic-write.test.js.map +1 -1
  45. package/dist/src/config/owned-lock.acquire.d.ts +53 -0
  46. package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
  47. package/dist/src/config/owned-lock.acquire.js +140 -0
  48. package/dist/src/config/owned-lock.acquire.js.map +1 -0
  49. package/dist/src/config/owned-lock.claim.d.ts +84 -0
  50. package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
  51. package/dist/src/config/owned-lock.claim.js +310 -0
  52. package/dist/src/config/owned-lock.claim.js.map +1 -0
  53. package/dist/src/config/owned-lock.d.ts +113 -0
  54. package/dist/src/config/owned-lock.d.ts.map +1 -0
  55. package/dist/src/config/owned-lock.js +121 -0
  56. package/dist/src/config/owned-lock.js.map +1 -0
  57. package/dist/src/config/owned-lock.test.d.ts +12 -0
  58. package/dist/src/config/owned-lock.test.d.ts.map +1 -0
  59. package/dist/src/config/owned-lock.test.js +412 -0
  60. package/dist/src/config/owned-lock.test.js.map +1 -0
  61. package/dist/src/config/owned-lock.types.d.ts +78 -0
  62. package/dist/src/config/owned-lock.types.d.ts.map +1 -0
  63. package/dist/src/config/owned-lock.types.js +30 -0
  64. package/dist/src/config/owned-lock.types.js.map +1 -0
  65. package/dist/src/db/createDatabase.d.ts.map +1 -1
  66. package/dist/src/db/createDatabase.js +19 -7
  67. package/dist/src/db/createDatabase.js.map +1 -1
  68. package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
  69. package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
  70. package/dist/src/embeddings/hnsw-store.exports.js +10 -0
  71. package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
  72. package/dist/src/embeddings/index.d.ts +1 -2
  73. package/dist/src/embeddings/index.d.ts.map +1 -1
  74. package/dist/src/embeddings/index.js +17 -3
  75. package/dist/src/embeddings/index.js.map +1 -1
  76. package/dist/src/embeddings/probe.d.ts.map +1 -1
  77. package/dist/src/embeddings/probe.js +4 -8
  78. package/dist/src/embeddings/probe.js.map +1 -1
  79. package/dist/src/exports/services.d.ts +4 -1
  80. package/dist/src/exports/services.d.ts.map +1 -1
  81. package/dist/src/exports/services.js +22 -0
  82. package/dist/src/exports/services.js.map +1 -1
  83. package/dist/src/index.d.ts +3 -3
  84. package/dist/src/index.d.ts.map +1 -1
  85. package/dist/src/index.js +9 -3
  86. package/dist/src/index.js.map +1 -1
  87. package/dist/src/install/index.d.ts +1 -1
  88. package/dist/src/install/index.d.ts.map +1 -1
  89. package/dist/src/install/index.js +1 -1
  90. package/dist/src/install/index.js.map +1 -1
  91. package/dist/src/install/paths.d.ts +10 -0
  92. package/dist/src/install/paths.d.ts.map +1 -1
  93. package/dist/src/install/paths.js +19 -0
  94. package/dist/src/install/paths.js.map +1 -1
  95. package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
  96. package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
  97. package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
  98. package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
  99. package/dist/src/services/recommend-guard.d.ts +28 -0
  100. package/dist/src/services/recommend-guard.d.ts.map +1 -0
  101. package/dist/src/services/recommend-guard.js +34 -0
  102. package/dist/src/services/recommend-guard.js.map +1 -0
  103. package/dist/src/services/recommend-guard.test.d.ts +2 -0
  104. package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
  105. package/dist/src/services/recommend-guard.test.js +28 -0
  106. package/dist/src/services/recommend-guard.test.js.map +1 -0
  107. package/dist/src/services/skill-installation.content.d.ts +88 -0
  108. package/dist/src/services/skill-installation.content.d.ts.map +1 -0
  109. package/dist/src/services/skill-installation.content.js +319 -0
  110. package/dist/src/services/skill-installation.content.js.map +1 -0
  111. package/dist/src/services/skill-installation.helpers.d.ts +32 -1
  112. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  113. package/dist/src/services/skill-installation.helpers.js +39 -6
  114. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  115. package/dist/src/services/skill-installation.helpers.test.js +34 -2
  116. package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
  117. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  118. package/dist/src/services/skill-installation.io.js +74 -1
  119. package/dist/src/services/skill-installation.io.js.map +1 -1
  120. package/dist/src/services/skill-installation.service.d.ts +20 -1
  121. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  122. package/dist/src/services/skill-installation.service.js +26 -4
  123. package/dist/src/services/skill-installation.service.js.map +1 -1
  124. package/dist/src/services/skill-installation.types.d.ts +42 -1
  125. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  126. package/dist/src/services/skill-installation.types.js.map +1 -1
  127. package/dist/src/services/skill-resolution.d.ts +67 -0
  128. package/dist/src/services/skill-resolution.d.ts.map +1 -0
  129. package/dist/src/services/skill-resolution.js +67 -0
  130. package/dist/src/services/skill-resolution.js.map +1 -0
  131. package/dist/src/services/skill-resolution.test.d.ts +2 -0
  132. package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
  133. package/dist/src/services/skill-resolution.test.js +143 -0
  134. package/dist/src/services/skill-resolution.test.js.map +1 -0
  135. package/dist/src/utils/index.d.ts +1 -0
  136. package/dist/src/utils/index.d.ts.map +1 -1
  137. package/dist/src/utils/index.js +4 -0
  138. package/dist/src/utils/index.js.map +1 -1
  139. package/dist/src/utils/quiet-mode.d.ts +21 -0
  140. package/dist/src/utils/quiet-mode.d.ts.map +1 -0
  141. package/dist/src/utils/quiet-mode.js +26 -0
  142. package/dist/src/utils/quiet-mode.js.map +1 -0
  143. package/dist/tests/EmbeddingService.test.js +58 -1
  144. package/dist/tests/EmbeddingService.test.js.map +1 -1
  145. package/dist/tests/SecurityScanner.performance.test.js +5 -2
  146. package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
  147. package/dist/tests/db/database-abstraction.test.js +45 -1
  148. package/dist/tests/db/database-abstraction.test.js.map +1 -1
  149. package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
  150. package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
  151. package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
  152. package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
  153. package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
  154. package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
  155. package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
  156. package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
  157. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
  158. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
  159. package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
  160. package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
  161. package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
  162. package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
  163. package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
  164. package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
  165. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
  166. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
  167. package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
  168. package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
  169. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
  170. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
  171. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
  172. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
  173. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
  174. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
  175. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
  176. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
  177. package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
  178. package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
  179. package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
  180. package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
  181. package/dist/tests/utils/quiet-mode.test.js +42 -0
  182. package/dist/tests/utils/quiet-mode.test.js.map +1 -0
  183. package/package.json +17 -5
@@ -17,9 +17,17 @@
17
17
  * principle observe a partially-written file.
18
18
  *
19
19
  * This module fixes both:
20
- * - {@link acquireConfigLock} serializes ALL writers (cross-process, via a
21
- * create-exclusive `.lock` sentinel file) so a caller's full
22
- * read-modify-write sequence is one atomic critical section.
20
+ * - {@link acquireConfigLock} serializes ALL writers (cross-process) so a
21
+ * caller's full read-modify-write sequence is one atomic critical
22
+ * section. SMI-5883 Wave 2: this is now a thin wrapper over the shared
23
+ * two-level {@link acquireOwnedLock} primitive (`owned-lock.ts`) —
24
+ * staleness is determined by OWNER LIVENESS (not file age), and a stale
25
+ * lock is reclaimed only from inside a second, strict-no-auto-reclaim
26
+ * lock that serializes reclaim decisions. The prior age-based
27
+ * `STALE_LOCK_AGE_MS` force-clear is REMOVED, not tuned — a lock held
28
+ * legitimately longer than any timeout now correctly times out instead
29
+ * of being force-cleared out from under its live holder. See
30
+ * `owned-lock.ts`'s module docstring for the full soundness argument.
23
31
  * - {@link atomicWriteFile} writes to a temp file in the same directory,
24
32
  * then `renameSync`s it into place — POSIX guarantees `rename(2)` is
25
33
  * atomic, so a concurrent reader (which never needs the lock — only
@@ -27,42 +35,36 @@
27
35
  * never a torn write.
28
36
  */
29
37
  import { randomBytes } from 'node:crypto';
30
- import { openSync, closeSync, unlinkSync, writeFileSync, renameSync, statSync, chmodSync, } from 'node:fs';
38
+ import { writeFileSync, renameSync, chmodSync } from 'node:fs';
31
39
  import { dirname, join } from 'node:path';
40
+ import { acquireOwnedLock } from './owned-lock.js';
32
41
  /**
33
42
  * Max time to wait for a lock before giving up (ms). This guards a
34
43
  * user-facing CLI/MCP-server startup path, so it must fail loudly with a
35
44
  * clear error rather than hang indefinitely.
36
45
  */
37
46
  const LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
38
- /** Backoff between lock-acquisition retries (ms). */
39
- const LOCK_RETRY_DELAY_MS = 20;
40
47
  /**
41
- * A lock file older than this is assumed to be left behind by a crashed
42
- * process (stale) and is force-removed. A healthy holder releases the lock
43
- * within milliseconds of a local JSON read+write, so this is a generous
44
- * margin, not a tight race.
45
- */
46
- const STALE_LOCK_AGE_MS = 10_000;
47
- /**
48
- * Synchronously block the calling thread for `ms` milliseconds without
49
- * spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
50
- * standard synchronous-sleep primitive in Node (unlike browsers, Node does
51
- * not forbid calling it on the main thread).
52
- */
53
- function sleepSync(ms) {
54
- const view = new Int32Array(new SharedArrayBuffer(4));
55
- Atomics.wait(view, 0, 0, ms);
56
- }
57
- /**
58
- * Acquire an exclusive, cross-process lock guarding `configPath`.
48
+ * Acquire an exclusive, cross-process lock guarding `configPath`. Thin
49
+ * wrapper over {@link acquireOwnedLock} (SMI-5883 Wave 2) preserves this
50
+ * function's signature and its `${configPath}.lock` path so no caller needs
51
+ * to change. Behaviour deltas vs the pre-SMI-5883 implementation (each
52
+ * covered by a test in `config-atomic-write.test.ts`):
59
53
  *
60
- * Uses atomic create-exclusive (the `wx` flag fails with `EEXIST` if the
61
- * lock file already exists) as the mutual-exclusion primitive: portable
62
- * (identical behavior on macOS/Linux/Windows) and needs no extra
63
- * dependency. Retries with a short backoff until `LOCK_ACQUIRE_TIMEOUT_MS`
64
- * elapses. If the held lock is older than `STALE_LOCK_AGE_MS` it is assumed
65
- * abandoned by a crashed process and force-cleared once, then retried.
54
+ * 1. A stale lock is cleared on OWNER DEATH, not on AGE. A lock held by a
55
+ * live process for longer than the old 10s threshold is no longer
56
+ * force-cleared it now correctly waits, then times out.
57
+ * 2. A legacy bare-PID lock (this module's OWN pre-SMI-5883 on-disk format)
58
+ * is no longer cleared at all `StuckLockError { reason:
59
+ * 'unreclaimable_legacy' }` (D-5: a legacy claim carries no `host`, so
60
+ * "dead on this host" cannot establish "dead").
61
+ * 3. The on-disk lock content changes from a bare PID to a v1 JSON record.
62
+ * No in-repo consumer parses it directly (only this module's own
63
+ * `acquireConfigLock`/release touch the file).
64
+ * 4. The timeout message keeps the stable prefix "Timed out waiting for
65
+ * config lock" (via `label: 'config lock'`) so existing assertions
66
+ * still match, with the failure reason and the manual unstick procedure
67
+ * appended.
66
68
  *
67
69
  * @param configPath - Path to the config file being guarded (NOT the lock
68
70
  * file itself — the lock file is `${configPath}.lock`).
@@ -72,56 +74,7 @@ function sleepSync(ms) {
72
74
  * `finally` block, so the lock is released even if the guarded work throws.
73
75
  */
74
76
  export function acquireConfigLock(configPath, timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS) {
75
- const lockPath = `${configPath}.lock`;
76
- const deadline = Date.now() + timeoutMs;
77
- let staleClearAttempted = false;
78
- for (;;) {
79
- try {
80
- const fd = openSync(lockPath, 'wx', 0o600);
81
- try {
82
- writeFileSync(fd, String(process.pid));
83
- }
84
- finally {
85
- closeSync(fd);
86
- }
87
- return () => {
88
- try {
89
- unlinkSync(lockPath);
90
- }
91
- catch {
92
- // Already gone. Releasing an already-released lock is a no-op,
93
- // not an error (defensive — should not happen in practice since
94
- // each acquire pairs with exactly one release).
95
- }
96
- };
97
- }
98
- catch (err) {
99
- if (err.code !== 'EEXIST')
100
- throw err;
101
- if (!staleClearAttempted) {
102
- staleClearAttempted = true;
103
- try {
104
- const age = Date.now() - statSync(lockPath).mtimeMs;
105
- if (age > STALE_LOCK_AGE_MS) {
106
- unlinkSync(lockPath);
107
- continue; // retry immediately — don't burn the backoff budget on this attempt
108
- }
109
- }
110
- catch {
111
- // Lock file vanished between the failed open and this stat (the
112
- // holder released it concurrently, or another waiter already
113
- // cleared it) — just retry the open below.
114
- continue;
115
- }
116
- }
117
- if (Date.now() >= deadline) {
118
- throw new Error(`[skillsmith] Timed out waiting for config lock at ${lockPath} after ` +
119
- `${timeoutMs}ms. If this persists, a crashed process may have left a ` +
120
- `stale lock — verify no other skillsmith process is running, then remove ${lockPath} manually.`);
121
- }
122
- sleepSync(LOCK_RETRY_DELAY_MS);
123
- }
124
- }
77
+ return acquireOwnedLock(configPath, { timeoutMs, label: 'config lock' });
125
78
  }
126
79
  /**
127
80
  * Write `content` to `filePath` atomically: write to a temp file in the same
@@ -1 +1 @@
1
- {"version":3,"file":"config-atomic-write.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,UAAU,EACV,QAAQ,EACR,SAAS,GACV,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC;;;;GAIG;AACH,MAAM,uBAAuB,GAAG,KAAK,CAAA;AAErC,qDAAqD;AACrD,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAE9B;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAA;AAEhC;;;;;GAKG;AACH,SAAS,SAAS,CAAC,EAAU;IAC3B,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAA;IACrD,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAC9B,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,YAAoB,uBAAuB;IAE3C,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAA;IACvC,IAAI,mBAAmB,GAAG,KAAK,CAAA;IAE/B,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;YAC1C,IAAI,CAAC;gBACH,aAAa,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;YACxC,CAAC;oBAAS,CAAC;gBACT,SAAS,CAAC,EAAE,CAAC,CAAA;YACf,CAAC;YACD,OAAO,GAAG,EAAE;gBACV,IAAI,CAAC;oBACH,UAAU,CAAC,QAAQ,CAAC,CAAA;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,+DAA+D;oBAC/D,gEAAgE;oBAChE,gDAAgD;gBAClD,CAAC;YACH,CAAC,CAAA;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;YAE/D,IAAI,CAAC,mBAAmB,EAAE,CAAC;gBACzB,mBAAmB,GAAG,IAAI,CAAA;gBAC1B,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAA;oBACnD,IAAI,GAAG,GAAG,iBAAiB,EAAE,CAAC;wBAC5B,UAAU,CAAC,QAAQ,CAAC,CAAA;wBACpB,SAAQ,CAAC,oEAAoE;oBAC/E,CAAC;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,gEAAgE;oBAChE,6DAA6D;oBAC7D,2CAA2C;oBAC3C,SAAQ;gBACV,CAAC;YACH,CAAC;YAED,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CACb,qDAAqD,QAAQ,SAAS;oBACpE,GAAG,SAAS,0DAA0D;oBACtE,2EAA2E,QAAQ,YAAY,CAClG,CAAA;YACH,CAAC;YACD,SAAS,CAAC,mBAAmB,CAAC,CAAA;QAChC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,OAAe,EAAE,IAAY;IAC7E,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IACnE,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5D,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC7B,uEAAuE;IACvE,2EAA2E;IAC3E,mCAAmC;IACnC,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"config-atomic-write.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;GAIG;AACH,MAAM,uBAAuB,GAAG,KAAK,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,iBAAiB,CAC/B,UAAkB,EAClB,YAAoB,uBAAuB;IAE3C,OAAO,gBAAgB,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAA;AAC1E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,OAAe,EAAE,IAAY;IAC7E,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;IACnE,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5D,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IAC7B,uEAAuE;IACvE,2EAA2E;IAC3E,mCAAmC;IACnC,IAAI,CAAC;QACH,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,kCAAkC;IACpC,CAAC;AACH,CAAC"}
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * SMI-5531: Atomic config-write primitive tests.
3
+ * SMI-5883 Wave 2 (§8d/§8e): `acquireConfigLock` is now a thin wrapper over
4
+ * the shared two-level `acquireOwnedLock` primitive (`owned-lock.ts`). The
5
+ * age-based stale-lock test below is DELETED (its premise — clearing on file
6
+ * age — is the behavior being removed), replaced by three tests asserting
7
+ * the new owner-liveness-based contract: a v1 dead-PID lock reclaims
8
+ * quickly, a v1 live-PID lock times out regardless of a backdated mtime, and
9
+ * a legacy bare-PID lock (this module's OWN pre-SMI-5883 format) is NEVER
10
+ * auto-reclaimed even when definitely dead (D-5).
3
11
  *
4
12
  * Uses the same tmpdir-per-test isolation harness as device-identity.test.ts
5
13
  * / index.test.ts (no shared HOME mutation needed here — these primitives
@@ -1 +1 @@
1
- {"version":3,"file":"config-atomic-write.test.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
1
+ {"version":3,"file":"config-atomic-write.test.d.ts","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * SMI-5531: Atomic config-write primitive tests.
3
+ * SMI-5883 Wave 2 (§8d/§8e): `acquireConfigLock` is now a thin wrapper over
4
+ * the shared two-level `acquireOwnedLock` primitive (`owned-lock.ts`). The
5
+ * age-based stale-lock test below is DELETED (its premise — clearing on file
6
+ * age — is the behavior being removed), replaced by three tests asserting
7
+ * the new owner-liveness-based contract: a v1 dead-PID lock reclaims
8
+ * quickly, a v1 live-PID lock times out regardless of a backdated mtime, and
9
+ * a legacy bare-PID lock (this module's OWN pre-SMI-5883 format) is NEVER
10
+ * auto-reclaimed even when definitely dead (D-5).
3
11
  *
4
12
  * Uses the same tmpdir-per-test isolation harness as device-identity.test.ts
5
13
  * / index.test.ts (no shared HOME mutation needed here — these primitives
@@ -9,7 +17,9 @@ import { describe, it, expect } from 'vitest';
9
17
  import { existsSync, mkdirSync, readFileSync, readdirSync, utimesSync, writeFileSync } from 'fs';
10
18
  import * as path from 'path';
11
19
  import * as os from 'os';
20
+ import { hostname } from 'node:os';
12
21
  import { acquireConfigLock, atomicWriteFile } from './config-atomic-write.js';
22
+ import { mintDeadPid } from '../../tests/helpers/deterministic-dead-pid.js';
13
23
  function makeTempDir() {
14
24
  const dir = path.join(os.tmpdir(), `skillsmith-atomic-write-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
15
25
  mkdirSync(dir, { recursive: true });
@@ -36,29 +46,71 @@ describe('acquireConfigLock — mutual exclusion', () => {
36
46
  const secondRelease = acquireConfigLock(configPath, 200);
37
47
  secondRelease();
38
48
  });
39
- it('force-clears a stale lock (older than the staleness threshold) instead of waiting out the full timeout', () => {
49
+ it('release() is idempotent calling it twice does not throw', () => {
50
+ const dir = makeTempDir();
51
+ const configPath = path.join(dir, 'config.json');
52
+ const release = acquireConfigLock(configPath);
53
+ release();
54
+ expect(() => release()).not.toThrow();
55
+ });
56
+ it('reclaims a v1 dead-PID lock quickly (owner-liveness based, not age)', () => {
40
57
  const dir = makeTempDir();
41
58
  const configPath = path.join(dir, 'config.json');
42
59
  const lockPath = `${configPath}.lock`;
43
- // Simulate a lock abandoned by a crashed process: create it, then
44
- // backdate its mtime well past the staleness threshold.
45
- writeFileSync(lockPath, '99999');
46
- const longAgo = new Date(Date.now() - 60_000);
47
- utimesSync(lockPath, longAgo, longAgo);
60
+ const deadPid = mintDeadPid();
61
+ writeFileSync(lockPath, JSON.stringify({
62
+ v: 1,
63
+ pid: deadPid,
64
+ token: 'a'.repeat(16),
65
+ host: hostname(),
66
+ acquiredAt: Date.now(),
67
+ }) + '\n');
48
68
  const start = Date.now();
49
- // Even with a tight timeout budget, stale-lock recovery should let this
50
- // succeed almost immediately rather than burning the whole window.
51
- const release = acquireConfigLock(configPath, 500);
69
+ const release = acquireConfigLock(configPath, 5_000);
52
70
  const elapsedMs = Date.now() - start;
53
71
  release();
54
- expect(elapsedMs).toBeLessThan(500);
72
+ expect(elapsedMs).toBeLessThan(2_000);
55
73
  });
56
- it('release() is idempotent calling it twice does not throw', () => {
74
+ it('does NOT force-clear a v1 live-PID lock even with a backdated mtime — age is irrelevant now', () => {
75
+ const dir = makeTempDir();
76
+ const configPath = path.join(dir, 'config.json');
77
+ const lockPath = `${configPath}.lock`;
78
+ writeFileSync(lockPath, JSON.stringify({
79
+ v: 1,
80
+ pid: process.pid, // definitively alive for the duration of this test
81
+ token: 'b'.repeat(16),
82
+ host: hostname(),
83
+ acquiredAt: Date.now() - 60_000,
84
+ }) + '\n');
85
+ const longAgo = new Date(Date.now() - 60_000);
86
+ utimesSync(lockPath, longAgo, longAgo);
87
+ const before = readFileSync(lockPath);
88
+ expect(() => acquireConfigLock(configPath, 200)).toThrow(/Timed out waiting for config lock/);
89
+ expect(readFileSync(lockPath).equals(before)).toBe(true);
90
+ });
91
+ it('never auto-reclaims a legacy bare-PID lock even when the PID is definitely live (D-5, §8d)', () => {
57
92
  const dir = makeTempDir();
58
93
  const configPath = path.join(dir, 'config.json');
94
+ const lockPath = `${configPath}.lock`;
95
+ writeFileSync(lockPath, String(process.pid)); // bare integer, no JSON, definitely live
96
+ const before = readFileSync(lockPath);
97
+ expect(() => acquireConfigLock(configPath, 150)).toThrow(/Timed out waiting for config lock/);
98
+ expect(readFileSync(lockPath).equals(before)).toBe(true); // byte-identical
99
+ expect(existsSync(`${lockPath}.reclaim`)).toBe(false); // no orphan left behind
100
+ expect(readdirSync(dir).filter((f) => f.endsWith('.tmp'))).toEqual([]);
101
+ });
102
+ it('the on-disk lock format is a v1 JSON record, not a bare PID integer', () => {
103
+ const dir = makeTempDir();
104
+ const configPath = path.join(dir, 'config.json');
105
+ const lockPath = `${configPath}.lock`;
59
106
  const release = acquireConfigLock(configPath);
107
+ const raw = readFileSync(lockPath, 'utf-8');
60
108
  release();
61
- expect(() => release()).not.toThrow();
109
+ const parsed = JSON.parse(raw);
110
+ expect(parsed.v).toBe(1);
111
+ expect(parsed.pid).toBe(process.pid);
112
+ expect(typeof parsed.token).toBe('string');
113
+ expect(parsed.host).toBe(hostname());
62
114
  });
63
115
  });
64
116
  describe('atomicWriteFile', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"config-atomic-write.test.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config-atomic-write.test.js","sourceRoot":"","sources":["../../../src/config/config-atomic-write.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAChG,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAC5B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,+CAA+C,CAAA;AAE3E,SAAS,WAAW;IAClB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CACnB,EAAE,CAAC,MAAM,EAAE,EACX,gCAAgC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CACpF,CAAA;IACD,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACnC,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC/F,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,OAAO,EAAE,CAAA;QAET,8DAA8D;QAC9D,MAAM,aAAa,GAAG,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;QACxD,aAAa,EAAE,CAAA;IACjB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAEhD,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,OAAO,EAAE,CAAA;QACT,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QACrC,MAAM,OAAO,GAAG,WAAW,EAAE,CAAA;QAE7B,aAAa,CACX,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC;YACb,CAAC,EAAE,CAAC;YACJ,GAAG,EAAE,OAAO;YACZ,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrB,IAAI,EAAE,QAAQ,EAAE;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;SACvB,CAAC,GAAG,IAAI,CACV,CAAA;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACxB,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;QACpD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;QAEpC,OAAO,EAAE,CAAA;QACT,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6FAA6F,EAAE,GAAG,EAAE;QACrG,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,aAAa,CACX,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC;YACb,CAAC,EAAE,CAAC;YACJ,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,mDAAmD;YACrE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrB,IAAI,EAAE,QAAQ,EAAE;YAChB,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM;SAChC,CAAC,GAAG,IAAI,CACV,CAAA;QACD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAA;QAC7C,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QACtC,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;QAErC,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC7F,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4FAA4F,EAAE,GAAG,EAAE;QACpG,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA,CAAC,yCAAyC;QACtF,MAAM,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;QAErC,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAA;QAC7F,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAC,iBAAiB;QAC1E,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA,CAAC,wBAAwB;QAC9E,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAChD,MAAM,QAAQ,GAAG,GAAG,UAAU,OAAO,CAAA;QAErC,MAAM,OAAO,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;QAC7C,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC3C,OAAO,EAAE,CAAA;QAET,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4D,CAAA;QACzF,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACxB,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACpC,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAC1C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAE1D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAEtC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAA;QAChC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAC1D,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;QAE1D,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAA;QAE9C,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;QAEtC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Core acquire loop + `StuckLockError` for the owned-lock primitive.
3
+ * @module @skillsmith/core/config/owned-lock.acquire
4
+ * @see owned-lock.ts for the full soundness argument and the PUBLIC API.
5
+ * @see owned-lock.claim.ts for claim parsing, exclusive creation, and reclaim.
6
+ *
7
+ * INTERNAL module -- not part of the public surface (no `package.json`
8
+ * subpath export), split out of `owned-lock.claim.ts` purely to keep both
9
+ * files under the repo's 500-line-per-file gate. `owned-lock.ts`'s public
10
+ * `acquireOwnedLock` is a thin wrapper over this file's
11
+ * {@link acquireOwnedLockCore}; the cross-process race-test child harness
12
+ * imports directly from here, by relative path, specifically to reach the
13
+ * two destructive test-only options
14
+ * ({@link AcquireOwnedLockCoreOptions.unsafeSkipReclaimRevalidation} and
15
+ * `.linkSyncOverride`) that must NEVER be reachable via the public
16
+ * `@skillsmith/core/config/owned-lock` subpath.
17
+ */
18
+ import type { Claim, ReclaimOutcome, StuckLockReason } from './owned-lock.types.js';
19
+ /**
20
+ * Thrown when {@link acquireOwnedLockCore} (and, through it, the public
21
+ * `acquireOwnedLock`) times out. `reason` is a stable discriminant for
22
+ * mechanical triage (never prose-matching); the message embeds the manual
23
+ * unstick procedure verbatim.
24
+ */
25
+ export declare class StuckLockError extends Error {
26
+ readonly lockPath: string;
27
+ readonly reclaimPath: string;
28
+ readonly reason: StuckLockReason;
29
+ constructor(lockPath: string, reclaimPath: string, label: string, reason: StuckLockReason, claim: Claim);
30
+ }
31
+ /**
32
+ * Full internal option set, including the two options the public
33
+ * `AcquireOwnedLockOptions` (owned-lock.types.ts) deliberately omits.
34
+ */
35
+ export interface AcquireOwnedLockCoreOptions {
36
+ timeoutMs?: number;
37
+ label?: string;
38
+ reclaimProbeAfterMs?: number;
39
+ onReclaimBoundary?: () => void;
40
+ onReclaimOutcome?: (outcome: ReclaimOutcome) => void;
41
+ /** @internal NEGATIVE CONTROL ONLY (owned-lock-reclaim-race.test.ts §8b). Removes the authoritative re-read that makes this mechanism sound -- reintroduces the round-3 lock-theft race on purpose. Never set outside that spec, and never reachable via the public acquireOwnedLock(). */
42
+ unsafeSkipReclaimRevalidation?: boolean;
43
+ /** @internal test seam (owned-lock.test.ts item 14). Never reachable via the public acquireOwnedLock(). */
44
+ linkSyncOverride?: (existingPath: string, newPath: string) => void;
45
+ }
46
+ /**
47
+ * The full acquire loop. `owned-lock.ts`'s public `acquireOwnedLock` is a
48
+ * thin wrapper over this that only ever forwards the PUBLIC-SAFE option
49
+ * subset -- see the module-level comment above for why the two unsafe
50
+ * options must never be reachable from there.
51
+ */
52
+ export declare function acquireOwnedLockCore(target: string, opts?: AcquireOwnedLockCoreOptions): () => void;
53
+ //# sourceMappingURL=owned-lock.acquire.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.acquire.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.acquire.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAoBH,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAmB,eAAe,EAAE,MAAM,uBAAuB,CAAA;AA2BpG;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAA;gBAG9B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,KAAK;CAcf;AAmBD;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC9B,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IACpD,2RAA2R;IAC3R,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,2GAA2G;IAC3G,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,2BAAgC,GACrC,MAAM,IAAI,CA4DZ"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Core acquire loop + `StuckLockError` for the owned-lock primitive.
3
+ * @module @skillsmith/core/config/owned-lock.acquire
4
+ * @see owned-lock.ts for the full soundness argument and the PUBLIC API.
5
+ * @see owned-lock.claim.ts for claim parsing, exclusive creation, and reclaim.
6
+ *
7
+ * INTERNAL module -- not part of the public surface (no `package.json`
8
+ * subpath export), split out of `owned-lock.claim.ts` purely to keep both
9
+ * files under the repo's 500-line-per-file gate. `owned-lock.ts`'s public
10
+ * `acquireOwnedLock` is a thin wrapper over this file's
11
+ * {@link acquireOwnedLockCore}; the cross-process race-test child harness
12
+ * imports directly from here, by relative path, specifically to reach the
13
+ * two destructive test-only options
14
+ * ({@link AcquireOwnedLockCoreOptions.unsafeSkipReclaimRevalidation} and
15
+ * `.linkSyncOverride`) that must NEVER be reachable via the public
16
+ * `@skillsmith/core/config/owned-lock` subpath.
17
+ */
18
+ import { hostname } from 'node:os';
19
+ import { classifyRefusal, createLockExclusive, isOwnerDefinitelyDead, makeRelease, randomHex, readClaim, sleepSync, tryReclaimUnderLock, } from './owned-lock.claim.js';
20
+ import { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
21
+ function describeReason(reason, claim, reclaimPath) {
22
+ switch (reason) {
23
+ case 'held':
24
+ return claim.kind === 'v1'
25
+ ? `held by pid ${claim.pid} on host '${claim.host}' (still alive)`
26
+ : 'held by another process';
27
+ case 'unreclaimable_legacy':
28
+ return ('held by a legacy (pre-v1) claim' +
29
+ (claim.kind === 'legacy' ? ` (pid ${claim.pid})` : '') +
30
+ ' -- legacy claims carry no host attribution and are NEVER auto-reclaimed (SMI-5883 D-5)');
31
+ case 'unreclaimable_unparseable':
32
+ return 'the lock file could not be parsed as a recognized claim -- never auto-reclaimed';
33
+ case 'reclaim_unavailable':
34
+ return `the reclaim lock at ${reclaimPath} is held or was orphaned by a crash inside the reclaim critical section (residual R1)`;
35
+ case 'reclaim_disabled':
36
+ return 'auto-reclaim is disabled (SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1)';
37
+ default: {
38
+ const exhaustive = reason;
39
+ return exhaustive;
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Thrown when {@link acquireOwnedLockCore} (and, through it, the public
45
+ * `acquireOwnedLock`) times out. `reason` is a stable discriminant for
46
+ * mechanical triage (never prose-matching); the message embeds the manual
47
+ * unstick procedure verbatim.
48
+ */
49
+ export class StuckLockError extends Error {
50
+ lockPath;
51
+ reclaimPath;
52
+ reason;
53
+ constructor(lockPath, reclaimPath, label, reason, claim) {
54
+ const namesReclaim = reason === 'reclaim_unavailable';
55
+ const message = `[skillsmith] Timed out waiting for ${label} at ${lockPath}: ${describeReason(reason, claim, reclaimPath)}. ` +
56
+ `Manual unstick -- 1) confirm no skillsmith process is running: ps -ax | grep -E '[s]killsmith|[s]klx'; ` +
57
+ `2) inspect (read-only): cat ${lockPath}${namesReclaim ? ` ; cat ${reclaimPath}` : ''}; ` +
58
+ `3) remove ONLY the file(s) named above: rm ${lockPath}${namesReclaim ? ` ; rm ${reclaimPath}` : ''}.`;
59
+ super(message);
60
+ this.name = 'StuckLockError';
61
+ this.lockPath = lockPath;
62
+ this.reclaimPath = reclaimPath;
63
+ this.reason = reason;
64
+ }
65
+ }
66
+ function mapRefusalToReason(refusal) {
67
+ switch (refusal) {
68
+ case 'legacy':
69
+ return 'unreclaimable_legacy';
70
+ case 'unparseable':
71
+ return 'unreclaimable_unparseable';
72
+ case 'reclaim-disabled':
73
+ return 'reclaim_disabled';
74
+ case 'unavailable':
75
+ return 'reclaim_unavailable';
76
+ default:
77
+ // 'held' | 'not-stale' | 'gone' | 'reclaimed' (the latter two never
78
+ // reach the caller as a refusal -- they trigger an immediate retry).
79
+ return 'held';
80
+ }
81
+ }
82
+ /**
83
+ * The full acquire loop. `owned-lock.ts`'s public `acquireOwnedLock` is a
84
+ * thin wrapper over this that only ever forwards the PUBLIC-SAFE option
85
+ * subset -- see the module-level comment above for why the two unsafe
86
+ * options must never be reachable from there.
87
+ */
88
+ export function acquireOwnedLockCore(target, opts = {}) {
89
+ const lockPath = `${target}.lock`;
90
+ const reclaimPath = `${lockPath}.reclaim`;
91
+ const token = randomHex(8);
92
+ const label = opts.label ?? 'lock';
93
+ const timeoutMs = opts.timeoutMs ?? LOCK_ACQUIRE_TIMEOUT_MS;
94
+ const started = Date.now();
95
+ const deadline = started + timeoutMs;
96
+ let nextProbeAt = started + (opts.reclaimProbeAfterMs ?? RECLAIM_PROBE_AFTER_MS);
97
+ let lastRefusal = 'held'; // safe default: EEXIST already implies SOMETHING is there
98
+ let lastObservedClaim = { kind: 'absent' };
99
+ for (;;) {
100
+ const record = JSON.stringify({ v: 1, pid: process.pid, token, host: hostname(), acquiredAt: Date.now() }) +
101
+ '\n';
102
+ if (createLockExclusive(lockPath, record, opts.linkSyncOverride)) {
103
+ return makeRelease(lockPath, token);
104
+ }
105
+ // ---- contended ----
106
+ if (Date.now() >= nextProbeAt) {
107
+ const claim = readClaim(lockPath);
108
+ lastObservedClaim = claim;
109
+ if (isOwnerDefinitelyDead(claim)) {
110
+ opts.onReclaimBoundary?.();
111
+ const outcome = tryReclaimUnderLock(lockPath, reclaimPath, {
112
+ unsafeSkipRevalidation: opts.unsafeSkipReclaimRevalidation,
113
+ linkSyncOverride: opts.linkSyncOverride,
114
+ });
115
+ opts.onReclaimOutcome?.(outcome);
116
+ if (outcome === 'reclaimed' || outcome === 'gone') {
117
+ nextProbeAt = 0; // retry create at once -- don't burn the backoff budget
118
+ continue;
119
+ }
120
+ lastRefusal = outcome; // 'not-stale' | 'unavailable'
121
+ }
122
+ else {
123
+ lastRefusal = classifyRefusal(claim);
124
+ }
125
+ nextProbeAt = Date.now() + RECLAIM_PROBE_INTERVAL_MS;
126
+ }
127
+ if (Date.now() >= deadline) {
128
+ // A final, read-only claim fetch purely for an accurate message -- does
129
+ // NOT affect the reclaim decision or `lastRefusal`. Needed because a
130
+ // very tight `timeoutMs` (shorter than `reclaimProbeAfterMs`) can
131
+ // otherwise expire before the periodic probe above ever runs once.
132
+ if (lastObservedClaim.kind === 'absent') {
133
+ lastObservedClaim = readClaim(lockPath);
134
+ }
135
+ throw new StuckLockError(lockPath, reclaimPath, label, mapRefusalToReason(lastRefusal), lastObservedClaim);
136
+ }
137
+ sleepSync(LOCK_RETRY_DELAY_MS);
138
+ }
139
+ }
140
+ //# sourceMappingURL=owned-lock.acquire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.acquire.js","sourceRoot":"","sources":["../../../src/config/owned-lock.acquire.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Claim parsing and lock-file mechanics (exclusive creation, reclaim,
3
+ * release) for the owned-lock primitive.
4
+ * @module @skillsmith/core/config/owned-lock.claim
5
+ * @see owned-lock.ts for the full soundness argument and the PUBLIC API.
6
+ * @see owned-lock.acquire.ts for the core acquire loop + `StuckLockError`,
7
+ * split into a sibling file purely to keep both under the repo's
8
+ * 500-line-per-file gate.
9
+ *
10
+ * INTERNAL module -- not part of the public surface (no `package.json`
11
+ * subpath export). Co-located tests and the cross-process race-test child
12
+ * harness import directly from here, by relative path, specifically to
13
+ * reach `createLockExclusive`'s `linkSyncOverride` test seam -- see
14
+ * `owned-lock.acquire.ts` for the analogous rationale on the acquire loop's
15
+ * own destructive test-only options.
16
+ */
17
+ import type { Claim, ReclaimOutcome, RefusalCategory } from './owned-lock.types.js';
18
+ export declare function randomHex(bytes: number): string;
19
+ /**
20
+ * Synchronously block the calling thread for `ms` milliseconds without
21
+ * spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
22
+ * standard synchronous-sleep primitive in Node (unlike browsers, Node does
23
+ * not forbid calling it on the main thread).
24
+ */
25
+ export declare function sleepSync(ms: number): void;
26
+ /**
27
+ * Parse raw lock-file text into a {@link Claim}. A v1 claim is one line of
28
+ * canonical JSON; a legacy claim is a bare decimal integer (today's
29
+ * `acquireConfigLock` format, `String(process.pid)`); anything else --
30
+ * including valid JSON at a DIFFERENT `v` -- is `unparseable` and therefore
31
+ * NEVER auto-reclaimed.
32
+ */
33
+ export declare function parseClaim(text: string): Claim;
34
+ /**
35
+ * Bounded, TOCTOU-free claim read: open once, `fstat` THAT fd (not the
36
+ * path), refuse above {@link MAX_LOCK_BYTES}, read from the same fd. Returns
37
+ * `{ kind: 'absent' }` on `ENOENT` (or any other open failure).
38
+ */
39
+ export declare function readClaim(path: string): Claim;
40
+ export declare function isAutoReclaimDisabled(): boolean;
41
+ /**
42
+ * Conservative in every ambiguous direction: PID reuse and a not-our-signal
43
+ * `EPERM` both read as ALIVE (declining to reclaim costs a timeout; an
44
+ * incorrect reclaim costs a lost caller). `pid <= 0` is rejected WITHOUT
45
+ * probing -- `kill(0, 0)` signals the process GROUP, which would make the
46
+ * liveness probe meaningless.
47
+ */
48
+ export declare function isOwnerDefinitelyDead(claim: Claim, killProbe?: typeof process.kill): boolean;
49
+ export declare function classifyRefusal(claim: Claim): RefusalCategory;
50
+ /**
51
+ * Create `path` exclusively via a temp file + `linkSync` (atomic; `EEXIST`
52
+ * if `path` exists) so a lock file is NEVER observable without a complete
53
+ * claim -- closing R2 (a writer could otherwise crash between create and
54
+ * write, leaving a permanently unreclaimable main lock). On filesystems
55
+ * without hardlink support this throws rather than falling back to a
56
+ * non-atomic `openSync('wx')` + separate write -- that two-step sequence
57
+ * would itself reopen R2 (an observer between the two steps, or a crash in
58
+ * between, sees an empty/truncated lock that is then PERMANENTLY
59
+ * unreclaimable, since an `unparseable` claim is never auto-reclaimed by
60
+ * design). Failing closed on an unsupported filesystem is the sound
61
+ * resolution the reviewer required; hardlink support is effectively
62
+ * universal on the filesystems Node actually runs on.
63
+ *
64
+ * @param linkSyncOverride - @internal test seam (owned-lock.test.ts item 14)
65
+ * to exercise the hardlink-unavailable fail-closed path deterministically.
66
+ * Never set outside that test.
67
+ */
68
+ export declare function createLockExclusive(path: string, recordJson: string, linkSyncOverride?: (existingPath: string, newPath: string) => void): boolean;
69
+ export interface ReclaimInternalOptions {
70
+ /** @internal NEGATIVE CONTROL ONLY (owned-lock-reclaim-race.test.ts) -- removes the authoritative re-read that makes this mechanism sound. Never set outside that spec. */
71
+ unsafeSkipRevalidation?: boolean;
72
+ linkSyncOverride?: (existingPath: string, newPath: string) => void;
73
+ }
74
+ /**
75
+ * Validate and destroy a stale main lock inside a region from which every
76
+ * other reclaimer is excluded (the reclaim lock). This is the ONLY code path
77
+ * that may ever unlink `<target>.lock` on the strength of a liveness
78
+ * inference: a caller's own pre-filter is advisory and discarded; only the
79
+ * re-read performed HERE, under the reclaim lock, authorizes the unlink.
80
+ */
81
+ export declare function tryReclaimUnderLock(lockPath: string, reclaimPath: string, opts: ReclaimInternalOptions): ReclaimOutcome;
82
+ export declare function releaseOwned(path: string, token: string): void;
83
+ export declare function makeRelease(lockPath: string, token: string): () => void;
84
+ //# sourceMappingURL=owned-lock.claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.claim.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAsBH,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEnF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAG1C;AAMD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAiC9C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAkB7C;AAED,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,KAAK,EACZ,SAAS,GAAE,OAAO,OAAO,CAAC,IAAmB,GAC5C,OAAO,CAYT;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAK7D;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACjE,OAAO,CA0BT;AAMD,MAAM,WAAW,sBAAsB;IACrC,2KAA2K;IAC3K,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,gBAAgB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACnE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,sBAAsB,GAC3B,cAAc,CAmChB;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAmB9D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CAOvE"}