bantamkit-mcp 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -0,0 +1,1608 @@
1
+ /**
2
+ * The filesystem calls the memory store makes, with CPython's answers rather than libuv's.
3
+ *
4
+ * WHY THIS EXISTS AS ITS OWN MODULE
5
+ * ---------------------------------
6
+ * `store._listing` is the code job37 spent five units closing, and the whole of that fix is
7
+ * a DECISION taken on the result of a failed directory scan: something at the path means
8
+ * "unreadable", nothing at the path means "empty". Reproducing the decision needs three
9
+ * things Node's `fs` does not hand you — a `strerror` in the C library's words rather than
10
+ * libuv's, an `lexists` that answers `false` for every error and not just ENOENT, and a
11
+ * `Path.exists()` that swallows exactly the errnos `pathlib` swallows. They are here rather
12
+ * than inside `store.ts` because `layers.count_facts` makes the SAME syscalls and a
13
+ * DELIBERATELY DIFFERENT decision on them, so the next unit needs the primitives without
14
+ * inheriting the store's ruling.
15
+ *
16
+ * Nothing here invents a policy. Every function names the CPython call it stands in for.
17
+ *
18
+ * REGISTERED AND NOT DONE — N8's review found five things in this file and N10 fixed two of
19
+ * them. The two were WRONG ANSWERS: `pyDecodeUtf8` ate a UTF-8 BOM its own validator had
20
+ * just accepted, and `OSError.__str__`/the symlink-loop `RuntimeError` spelled `%r` as a
21
+ * hand-written pair of apostrophes. The other three are CONSOLIDATION — one question with
22
+ * two spellings, both of them right — and they are listed here rather than half-started:
23
+ *
24
+ * - FOUR TABLES KEYED ON THE SAME THING, now, not two: `STRERROR` (errno name -> the C
25
+ * library's sentence), `STRERROR_WINDOWS` (the four the UCRT spells differently),
26
+ * `OSERROR_SUBCLASS` (errno name -> the exception class CPython picks) and `WINERROR`
27
+ * (Win32 number -> the Win32 sentence). Three of the four are read back against the
28
+ * running Python — `os.strerror for every errno` covers the first two and
29
+ * `FormatMessage for every winerror` the fourth, both in
30
+ * `tools/conformance/suites/store.mjs` — and `OSERROR_SUBCLASS` is the one still
31
+ * uncovered: an errno added to `STRERROR` and not to it silently loses its exception
32
+ * class. Nothing measured is wrong today.
33
+ * - `normcase` AND `PyRuntimeError` ARE EXPORTED WITH NO CALLER OUTSIDE THIS FILE. Both
34
+ * are used inside it; the export is surface nobody asked for.
35
+ *
36
+ * FOUR PATH PARSERS became one, and N11 closed that one because it had stopped being a
37
+ * consolidation and started being wrong. `parsePath`, `lastSeparator` and `resolveWindows`
38
+ * each re-decided where a separator and a drive letter are; the Windows readings they agreed
39
+ * on were the POSIX ones. There is now a single `ntpath`/`PureWindowsPath` model —
40
+ * `ntSplitRoot`, `parseWindowsPath`, `ntJoin`, `winFormat` and the `win*` functions over
41
+ * them — and every Windows arm is a one-line dispatch to it.
42
+ *
43
+ * WHY THE WINDOWS ARMS ARE EXPORTED SEPARATELY FROM THE PLATFORM DISPATCH. They are pure
44
+ * path algebra, and CPython computes it the same way on every operating system, so exporting
45
+ * the flavour explicitly turns "measurable only on a GitHub runner" into "measurable on the
46
+ * laptop that wrote it". The `ntpath.splitroot and PureWindowsPath parsing` case in
47
+ * `tools/conformance/suites/store.mjs` drives them against a running CPython on every
48
+ * platform, and it caught a wrong answer (`///a` reported absolute) before a CI cycle was
49
+ * spent on it. What still cannot be measured off Windows is the DISPATCH — the
50
+ * `process.platform === 'win32'` line itself — and the calls that touch the real filesystem
51
+ * (`resolveWindows`, `scandirShape`).
52
+ *
53
+ * Also registered, in `store.ts` rather than here: the `ValueError` arms `_facts` catches
54
+ * and this port reaches by a different route. Owner: whichever unit consolidates `pyfs`.
55
+ */
56
+ import { appendFileSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
57
+ import { constants as osConstants, homedir, userInfo } from 'node:os';
58
+ // ------------------------------------------------------------------------------ errno
59
+ /**
60
+ * `os.strerror(errno)` for the errnos a directory scan, a `mkdir` or a file write can
61
+ * actually produce. Keyed on the errno NAME, not the number: ELOOP is 62 on macOS and 40 on
62
+ * Linux, so a table keyed on the number would be wrong on one of the two CI platforms.
63
+ *
64
+ * Every wording here is glibc's AND BSD's — verified identical for this set by the
65
+ * `strerror table` case in `tools/conformance/suites/store.mjs`, which asks the running
66
+ * Python for `os.strerror` of each entry. Adding a key without adding it to that case is
67
+ * how this quietly becomes a guess.
68
+ *
69
+ * WINDOWS IS COVERED BY A SECOND TABLE, NOT BY THIS ONE. `os.scandir` on Windows raises an
70
+ * `OSError` whose `strerror` comes from the Win32 message table by way of `winerror`, not
71
+ * from the CRT — "The directory name is invalid" where POSIX says "Not a directory". That
72
+ * table is `WINERROR` below, and which of the two a given call uses is `OSErrorOrigin`:
73
+ * anything that goes through the Win32 API reads `WINERROR`, and `open()` — which goes
74
+ * through the C runtime — reads this one.
75
+ */
76
+ const STRERROR = {
77
+ E2BIG: 'Argument list too long',
78
+ EACCES: 'Permission denied',
79
+ EBADF: 'Bad file descriptor',
80
+ EEXIST: 'File exists',
81
+ EFAULT: 'Bad address',
82
+ EINVAL: 'Invalid argument',
83
+ EIO: 'Input/output error',
84
+ EISDIR: 'Is a directory',
85
+ ELOOP: 'Too many levels of symbolic links',
86
+ EMFILE: 'Too many open files',
87
+ ENAMETOOLONG: 'File name too long',
88
+ ENFILE: 'Too many open files in system',
89
+ ENOENT: 'No such file or directory',
90
+ ENOMEM: 'Cannot allocate memory',
91
+ ENOSPC: 'No space left on device',
92
+ ENOTDIR: 'Not a directory',
93
+ ENOTEMPTY: 'Directory not empty',
94
+ EPERM: 'Operation not permitted',
95
+ EROFS: 'Read-only file system',
96
+ };
97
+ /**
98
+ * The four of those nineteen where the WINDOWS C runtime says something else.
99
+ *
100
+ * `os.strerror` is the CRT's `strerror`, and the UCRT's table is not glibc's. MEASURED
101
+ * against CPython 3.12 on windows-latest, run 32649940727, by the `os.strerror for every
102
+ * errno` case: `E2BIG` is "Arg list too long" there, `ENAMETOOLONG` is "Filename too long",
103
+ * `ENOMEM` is "Not enough space", and `ELOOP` — which the UCRT has no entry for at all —
104
+ * is the literal string "Unknown error". Everything not listed is byte-identical.
105
+ *
106
+ * None of the four is reachable from a store operation today; the table is compared in full
107
+ * because an entry nobody reaches is exactly the one that rots.
108
+ */
109
+ const STRERROR_WINDOWS = {
110
+ E2BIG: 'Arg list too long',
111
+ ELOOP: 'Unknown error',
112
+ ENAMETOOLONG: 'Filename too long',
113
+ ENOMEM: 'Not enough space',
114
+ };
115
+ /** The errno names this port claims to render in CPython's words. The suite reads it. */
116
+ export const STRERROR_NAMES = Object.keys(STRERROR);
117
+ /**
118
+ * `os.strerror` for one errno NAME — the table alone, with no Windows arm in front of it.
119
+ *
120
+ * The conformance case that checks these wordings used to read them back through
121
+ * `asPyOSError`, which now answers with the WIN32 message on Windows for exactly the codes
122
+ * the table also carries. Reading the table directly keeps that case measuring the thing its
123
+ * name says, and leaves the winerror wordings to the `winerror table` case beside it.
124
+ */
125
+ export function pyStrerror(code) {
126
+ if (process.platform === 'win32' && code in STRERROR_WINDOWS)
127
+ return STRERROR_WINDOWS[code];
128
+ return STRERROR[code];
129
+ }
130
+ /**
131
+ * CPython's `errnomap`, for the errnos this module can raise.
132
+ *
133
+ * `OSError.__new__` picks a SUBCLASS from the errno, and the class name is what a caller
134
+ * sees and what `component.Memory` puts in front of a person. A single `PyOSError` name
135
+ * would be a difference with no reason behind it, so the name follows the errno the way
136
+ * CPython's does. Anything not listed stays `OSError`, which is also CPython's default.
137
+ */
138
+ const OSERROR_SUBCLASS = {
139
+ EACCES: 'PermissionError',
140
+ EAGAIN: 'BlockingIOError',
141
+ ECHILD: 'ChildProcessError',
142
+ EEXIST: 'FileExistsError',
143
+ EINTR: 'InterruptedError',
144
+ EISDIR: 'IsADirectoryError',
145
+ ENOENT: 'FileNotFoundError',
146
+ ENOTDIR: 'NotADirectoryError',
147
+ EPERM: 'PermissionError',
148
+ EPIPE: 'BrokenPipeError',
149
+ ESRCH: 'ProcessLookupError',
150
+ };
151
+ /**
152
+ * `type(exc).__name__` for an `OSError` with this errno NAME — the table and its default.
153
+ *
154
+ * ONE reader of `OSERROR_SUBCLASS` for two callers, because the default is half the rule:
155
+ * an unlisted errno is `OSError` in CPython, and a second `?? 'OSError'` written somewhere
156
+ * else is a second place for the two halves to drift apart. `PyOSError`'s own constructor
157
+ * takes its `name` from here, and `eventlog.ts` asks the same question of an exception it
158
+ * is about to record — the port had already decided this question in the constructor and
159
+ * the event log answered it with the JavaScript constructor name instead, recording
160
+ * `PyOSError` where CPython records `NotADirectoryError`.
161
+ */
162
+ export function osErrorClassName(code) {
163
+ return OSERROR_SUBCLASS[code] ?? 'OSError';
164
+ }
165
+ /**
166
+ * An `OSError` as Python prints one.
167
+ *
168
+ * `str(OSError)` is `[Errno {errno}] {strerror}: {filename!r}`, and that text is not
169
+ * decoration: `_ensure_dirs` lets a `FileExistsError` out of `save` unconverted, and
170
+ * `component.Memory.save` turns whatever comes out into the sentence a model reads.
171
+ * `errno` is the platform number, which is what Python prints and what libuv already
172
+ * gives us as the magnitude of `err.errno`.
173
+ */
174
+ export class PyOSError extends Error {
175
+ errno;
176
+ code;
177
+ strerror;
178
+ filename;
179
+ /** `OSError.filename2` — set by the two-path syscalls, and PRINTED when it is. */
180
+ filename2;
181
+ /**
182
+ * `OSError.winerror`, which exists ONLY on Windows and CHANGES `__str__`.
183
+ *
184
+ * CPython carries both numbers for a call that went through the Win32 API: the translated
185
+ * POSIX `errno` and the raw Win32 code. `OSError.__str__` prefers the latter and prints
186
+ * `[WinError %d] <the Win32 message>`. `null` here means "this OSError has no winerror",
187
+ * which is every OSError on POSIX and every OSError CPython raises on Windows out of the
188
+ * C runtime rather than out of the Win32 API — see `asPyOSError`'s `origin`.
189
+ */
190
+ winerror;
191
+ constructor(errno, code, strerror, filename, filename2 = null, winerror = null) {
192
+ // `OSError.__str__` is `"[Errno %S] %S: %R"`, or `"[Errno %S] %S: %R -> %R"` when there
193
+ // are two (`Objects/exceptions.c`). `%R` is `repr`, NOT a hand-written pair of
194
+ // apostrophes. WAS the latter, and N8 measured the difference: a path holding a `'`, a
195
+ // newline or a tab produced a sentence CPython does not print — `pyRepr` switches to
196
+ // double quotes for the first and escapes the other two. That sentence is quoted
197
+ // verbatim into what `component.Memory` and shiftwork's clock-out put before a model.
198
+ const where = filename === null
199
+ ? ''
200
+ : filename2 === null
201
+ ? `: ${pyRepr(filename)}`
202
+ : `: ${pyRepr(filename)} -> ${pyRepr(filename2)}`;
203
+ // `Objects/exceptions.c`, `OSError_str`: the winerror arm comes FIRST, and when it fires
204
+ // the number printed is the Win32 one and the sentence is the Win32 one. There is no
205
+ // spelling of `str(OSError)` that shows both.
206
+ const head = winerror === null ? `[Errno ${errno}]` : `[WinError ${winerror}]`;
207
+ super(`${head} ${strerror}${where}`);
208
+ this.name = osErrorClassName(code);
209
+ this.errno = errno;
210
+ this.code = code;
211
+ this.strerror = strerror;
212
+ this.filename = filename;
213
+ this.filename2 = filename2;
214
+ this.winerror = winerror;
215
+ }
216
+ }
217
+ /**
218
+ * The number CPython would print, which is NOT the number libuv hands over.
219
+ *
220
+ * `err.errno` is a libuv status, and on POSIX libuv reuses the platform's own errno so the
221
+ * magnitude is right by accident. On Windows it does not: libuv has its own space starting
222
+ * at -4096, so `ENOENT` arrives as **-4058** and `EISDIR` as **-4068** where CPython prints
223
+ * `[Errno 2]` and `[Errno 21]`. MEASURED on windows-latest, run 32644269451: five test
224
+ * nodes reported `[Errno 4058] No such file or directory` for a sentence the reference
225
+ * renders `[Errno 2] ...`, and that sentence is quoted verbatim into what shiftwork's
226
+ * clock-out and `component.Memory` put in front of a model.
227
+ *
228
+ * `os.constants.errno` is the platform's own `errno.h` table — 2/21/13 on Windows exactly
229
+ * as on POSIX — so keying on the CODE NAME gives CPython's number on every platform. The
230
+ * libuv magnitude stays as the fallback for a code the table does not carry, because a
231
+ * number that is merely wrong beats no number at all in a message a human has to search
232
+ * for. Cross-checked against the reference by the `oserror` cases in
233
+ * tools/conformance/suites/store.mjs.
234
+ *
235
+ * The `winerror` half of the same question is `WINERROR` and `winerrorFor` below.
236
+ */
237
+ function pyErrno(e) {
238
+ const named = e?.code === undefined ? undefined : osConstants.errno[e.code];
239
+ return named ?? Math.abs(e?.errno ?? 0);
240
+ }
241
+ // ---------------------------------------------------------------------------- winerror
242
+ /**
243
+ * The Win32 message table, as `FormatMessageW` renders it and as CPython then TRIMS it.
244
+ *
245
+ * `PyErr_SetExcFromWindowsErrWithFilename` strips every trailing character that is `<= ' '`
246
+ * or `'.'`, so `The system cannot find the file specified.\r\n` reaches a reader without the
247
+ * period. ALL FIFTEEN ARE MEASURED, not four: the `FormatMessage for every winerror the
248
+ * port claims to render` case in `tools/conformance/suites/store.mjs` asks the running
249
+ * CPython — through `ctypes.FormatError`, with CPython's own trim — for every entry, and it
250
+ * passed on windows-latest from the first run that carried it (32649940727). Off Windows
251
+ * `ctypes.FormatError` has nothing to answer, so the case does not exist there and the
252
+ * suite's notes say which wordings are therefore unmeasured on the platform in hand.
253
+ */
254
+ const WINERROR = {
255
+ 1: 'Incorrect function',
256
+ 2: 'The system cannot find the file specified',
257
+ 3: 'The system cannot find the path specified',
258
+ 5: 'Access is denied',
259
+ 15: 'The system cannot find the drive specified',
260
+ 32: 'The process cannot access the file because it is being used by another process',
261
+ 80: 'The file exists',
262
+ 87: 'The parameter is incorrect',
263
+ 123: 'The filename, directory name, or volume label syntax is incorrect',
264
+ 145: 'The directory is not empty',
265
+ 183: 'Cannot create a file when that file already exists',
266
+ 206: 'The filename or extension is too long',
267
+ 267: 'The directory name is invalid',
268
+ 1920: 'The file cannot be accessed by the system',
269
+ 1921: 'The name of the file cannot be resolved by the system',
270
+ };
271
+ /** The winerrors this port claims to render in CPython's words. The suite reads it. */
272
+ export const WINERROR_NUMBERS = Object.keys(WINERROR).map(Number);
273
+ /** One Win32 wording, as CPython trims it. The `winerror table` case checks every entry. */
274
+ export function pyWinStrerror(winerror) {
275
+ return WINERROR[winerror];
276
+ }
277
+ /** `ERROR_CANT_RESOLVE_FILENAME` — pathlib's `_WINERROR_CANT_RESOLVE_FILENAME`. */
278
+ const WINERROR_CANT_RESOLVE_FILENAME = 1921;
279
+ /**
280
+ * libuv's code -> the Win32 error CPython would have carried, or `null` for "no winerror".
281
+ *
282
+ * THE MAPPING IS MANY-TO-ONE IN THE DIRECTION THAT LOSES INFORMATION, which is why this
283
+ * cannot be a plain table. libuv translates every Win32 error into one POSIX-shaped code, so
284
+ * `ERROR_FILE_NOT_FOUND` (2) and `ERROR_PATH_NOT_FOUND` (3) both arrive as `ENOENT` and the
285
+ * distinction Windows drew has to be RE-DERIVED. Win32 picks 3 whenever the component that
286
+ * failed was needed AS A DIRECTORY and 2 when only the final name was missing, so the probe
287
+ * is exactly that: which of the components is not there.
288
+ *
289
+ * Verified against the reference for both arms on run 32646521489 — `os.replace` onto a
290
+ * missing destination DIRECTORY is 3, `os.replace` of a missing source FILE beside an
291
+ * existing directory is 2. `exists` is injected so the derivation is a pure function with a
292
+ * node that goes red on a laptop.
293
+ */
294
+ export function winerrorFor(code, origin, path, dest, exists) {
295
+ if (origin === 'crt')
296
+ return null;
297
+ if (code === 'ENOENT')
298
+ return notFoundWinerror(origin, path, dest, exists);
299
+ // libuv reports EINVAL for two unrelated things on Windows: a name the API refuses, and a
300
+ // parameter it refuses. Only the first belongs in the path walk; the second is
301
+ // `ERROR_INVALID_PARAMETER`, and routing both through the walk would print a not-found
302
+ // sentence for an argument error.
303
+ if (code === 'EINVAL') {
304
+ return ntNameIsInvalid(path) || ntNameIsInvalid(dest)
305
+ ? notFoundWinerror(origin, path, dest, exists)
306
+ : 87;
307
+ }
308
+ switch (code) {
309
+ case 'ENOTDIR':
310
+ return 267;
311
+ case 'EEXIST':
312
+ return 183;
313
+ case 'EACCES':
314
+ case 'EPERM':
315
+ return 5;
316
+ case 'ENOTEMPTY':
317
+ return 145;
318
+ case 'ELOOP':
319
+ return WINERROR_CANT_RESOLVE_FILENAME;
320
+ case 'ENAMETOOLONG':
321
+ return 206;
322
+ case 'EBUSY':
323
+ return 32;
324
+ default:
325
+ // No claim rather than a fabricated one: an unmapped code keeps the `[Errno %d]` form,
326
+ // which is visibly a difference instead of a plausible-looking wrong sentence.
327
+ return null;
328
+ }
329
+ }
330
+ /**
331
+ * `winerror_to_errno` — CPython's translation, which is NOT the one libuv already applied.
332
+ *
333
+ * THE CLASS COMES OFF THE ERRNO, and the errno CPython carries is the one IT derived from
334
+ * the Win32 code, not the one libuv derived. MEASURED, run 32651049453: `os.unlink` of a
335
+ * name holding a newline is `ERROR_INVALID_NAME` (123), which CPython translates to EINVAL
336
+ * — and EINVAL is not in `errnomap`, so the exception is a plain `OSError`. libuv had folded
337
+ * the same failure into ENOENT, so this port raised `FileNotFoundError` and a caller's
338
+ * `except FileNotFoundError` would have swallowed an error the reference lets past.
339
+ *
340
+ * Every other arm here is an identity on the codes this port can reach (2/3 -> ENOENT,
341
+ * 183 -> EEXIST, 267 -> ENOTDIR), which is why only the 123 case was visible. The table is
342
+ * written out in full anyway: an arm that happens to be an identity today is exactly the one
343
+ * that stops being one without anybody noticing.
344
+ */
345
+ export function winerrorToCode(winerror) {
346
+ switch (winerror) {
347
+ case 2:
348
+ case 3:
349
+ case 15:
350
+ return 'ENOENT';
351
+ case 5:
352
+ case 32:
353
+ case 1920:
354
+ return 'EACCES';
355
+ case 80:
356
+ case 183:
357
+ return 'EEXIST';
358
+ case 145:
359
+ return 'ENOTEMPTY';
360
+ case 206:
361
+ return 'ENAMETOOLONG';
362
+ case 267:
363
+ return 'ENOTDIR';
364
+ default:
365
+ // `ERROR_INVALID_FUNCTION`, `ERROR_INVALID_PARAMETER`, `ERROR_INVALID_NAME` and
366
+ // `ERROR_CANT_RESOLVE_FILENAME` are all outside CPython's errmap, and an unmapped
367
+ // Win32 error becomes EINVAL. pathlib's `check_eloop` testing `winerror == 1921`
368
+ // SEPARATELY from `errno == ELOOP` is that fact written down in the reference.
369
+ return 'EINVAL';
370
+ }
371
+ }
372
+ /**
373
+ * WHICH not-found, in the ORDER Win32 arrives at one. Three answers, one path walk.
374
+ *
375
+ * Win32 resolves the directory components FIRST and only then looks at the final name, so
376
+ * the three outcomes are ordered and not alternatives:
377
+ *
378
+ * a missing DIRECTORY component -> ERROR_PATH_NOT_FOUND (3)
379
+ * a final name Win32 forbids -> ERROR_INVALID_NAME (123)
380
+ * a missing final component -> ERROR_FILE_NOT_FOUND (2)
381
+ *
382
+ * THE ORDER IS MEASURED AND IT WAS THE OTHER WAY ROUND FIRST. `unlink` of
383
+ * `<bed>\two\nlines.md` — parent present, name forbidden — is 123, and `mkdir` of
384
+ * `<bed>\missing\two\nlines.md` — the SAME forbidden name under a missing directory — is 3,
385
+ * not 123. Run 32649940727 established the first, run 32651670551 the second by refuting a
386
+ * validate-the-name-first rule that had got the first one right for the wrong reason.
387
+ *
388
+ * A two-name call walks the SOURCE to completion before it looks at the destination, which
389
+ * is why a missing source beside a missing destination DIRECTORY is 2 and not 3 (measured,
390
+ * run 32646521489, both ways round). A destination that is merely absent is not an error —
391
+ * that is the success case for `os.replace` — so only the source's absence answers.
392
+ */
393
+ function notFoundWinerror(origin, path, dest, exists) {
394
+ // A directory-listing call uses the NAMED path as a directory, so a missing leaf is
395
+ // already a missing directory component there.
396
+ if (origin === 'scandir') {
397
+ if (path === null || !exists(path))
398
+ return 3;
399
+ return ntNameIsInvalid(path) ? 123 : 2;
400
+ }
401
+ for (const candidate of dest === null ? [path] : [path, dest]) {
402
+ if (candidate === null)
403
+ continue;
404
+ if (!exists(pyParent(candidate)))
405
+ return 3;
406
+ if (ntNameIsInvalid(candidate))
407
+ return 123;
408
+ // A missing SOURCE ends the walk: `os.replace` never reaches the destination, so a
409
+ // missing destination DIRECTORY behind it is never the error reported. Deleting this
410
+ // line turns the measured 2 into a 3. (Replacing its condition with a bare
411
+ // `!exists(candidate)` is an EQUIVALENT mutation and survives a sweep — the
412
+ // destination arm would answer 2 as well, which is what the fallthrough already says.)
413
+ if (candidate === path && !exists(candidate))
414
+ return 2;
415
+ }
416
+ return 2;
417
+ }
418
+ /**
419
+ * The C runtime's answer where it is NOT libuv's, for the calls `open()` makes.
420
+ *
421
+ * One entry, and it is measured: opening a directory for writing is `EISDIR` to libuv and
422
+ * `EACCES` to the Windows CRT, so CPython prints `[Errno 13] Permission denied` for the
423
+ * `.tmp` that shiftwork's clock-out could not write. Everything else the CRT can raise here
424
+ * — ENOENT, EACCES, EEXIST, EINVAL — already agrees with libuv in both number and wording.
425
+ */
426
+ export function crtCode(code, path = null) {
427
+ if (code === 'EISDIR')
428
+ return 'EACCES';
429
+ // The same name rule, through the other door: `open()` on a name Win32 rejects is
430
+ // `[Errno 22] Invalid argument`, not a not-found. MEASURED, run 32649940727, in
431
+ // `store/every validation refusal` — a save whose name reaches the file path.
432
+ if (code === 'ENOENT' && ntNameIsInvalid(path))
433
+ return 'EINVAL';
434
+ return code;
435
+ }
436
+ /**
437
+ * Does any component of this path hold a character Win32 forbids in a name?
438
+ *
439
+ * `< > : " | ? *` and every control character below U+0020. The separators are excluded
440
+ * because they SPLIT the name rather than sitting in one, and the drive's colon is excluded
441
+ * for the same reason — it is part of the anchor, not of a component. Windows also strips a
442
+ * trailing space or dot silently rather than refusing, so neither is treated as invalid.
443
+ */
444
+ export function ntNameIsInvalid(path) {
445
+ if (path === null)
446
+ return false;
447
+ const { tail } = parseWindowsPath(path);
448
+ return tail.some((piece) => /[<>:"|?*\u0000-\u001f]/.test(piece));
449
+ }
450
+ /**
451
+ * A Node `fs` rejection, restated as the `OSError` CPython would have raised.
452
+ *
453
+ * An errno outside the table keeps libuv's own lowercase wording rather than a fabricated
454
+ * one — a wrong capital letter in a sentence a model reads is worse than a visibly
455
+ * different one, and the table is the thing the conformance suite can check.
456
+ *
457
+ * `origin` says which of CPython's two error paths the call would have taken on Windows;
458
+ * off Windows it changes nothing, because there is only one path there.
459
+ */
460
+ export function asPyOSError(error, fallbackPath, fallbackDest, origin = 'win32') {
461
+ const e = error;
462
+ const windows = process.platform === 'win32';
463
+ const rawPath = e?.path ?? fallbackPath ?? null;
464
+ const code = windows && origin === 'crt' ? crtCode(e?.code ?? 'EUNKNOWN', rawPath) : (e?.code ?? 'EUNKNOWN');
465
+ const errno = pyErrno({ code, errno: e?.errno });
466
+ const filename = rawPath;
467
+ const filename2 = e?.dest ?? fallbackDest ?? null;
468
+ const winerror = windows ? winerrorFor(code, origin, filename, filename2, pyLexists) : null;
469
+ // Once a winerror is known it is the SOURCE of both the number and the class, because it
470
+ // is CPython's source for them: `PyErr_SetExcFromWindowsErr` translates the Win32 code and
471
+ // `OSError.__new__` picks the subclass off the result.
472
+ const finalCode = winerror === null ? code : winerrorToCode(winerror);
473
+ const finalErrno = winerror === null ? errno : pyErrno({ code: finalCode, errno: e?.errno });
474
+ const strerror = (winerror === null ? pyStrerror(finalCode) : WINERROR[winerror]) ??
475
+ pyStrerror(finalCode) ??
476
+ `${finalCode}: ${e?.message ?? 'unknown error'}`;
477
+ return new PyOSError(finalErrno, finalCode, strerror, filename, filename2, winerror);
478
+ }
479
+ // -------------------------------------------------------------------------- utf-8 text
480
+ /** `UnicodeDecodeError`, which is a `ValueError` in Python and is NOT an `OSError`. */
481
+ export class PyUnicodeDecodeError extends Error {
482
+ start;
483
+ end;
484
+ reason;
485
+ constructor(bytes, start, end, reason) {
486
+ const where = end - start === 1
487
+ ? `byte 0x${bytes[start].toString(16).padStart(2, '0')} in position ${start}`
488
+ : `bytes in position ${start}-${end - 1}`;
489
+ super(`'utf-8' codec can't decode ${where}: ${reason}`);
490
+ this.name = 'UnicodeDecodeError';
491
+ this.start = start;
492
+ this.end = end;
493
+ this.reason = reason;
494
+ }
495
+ }
496
+ /** How many continuation bytes follow a lead byte, and what the second one may be. */
497
+ function leadInfo(b) {
498
+ if (b < 0x80)
499
+ return { length: 1, lo: 0, hi: 0 };
500
+ if (b >= 0xc2 && b <= 0xdf)
501
+ return { length: 2, lo: 0x80, hi: 0xbf };
502
+ if (b === 0xe0)
503
+ return { length: 3, lo: 0xa0, hi: 0xbf };
504
+ if (b >= 0xe1 && b <= 0xec)
505
+ return { length: 3, lo: 0x80, hi: 0xbf };
506
+ if (b === 0xed)
507
+ return { length: 3, lo: 0x80, hi: 0x9f }; // no surrogates
508
+ if (b >= 0xee && b <= 0xef)
509
+ return { length: 3, lo: 0x80, hi: 0xbf };
510
+ if (b === 0xf0)
511
+ return { length: 4, lo: 0x90, hi: 0xbf }; // no overlongs
512
+ if (b >= 0xf1 && b <= 0xf3)
513
+ return { length: 4, lo: 0x80, hi: 0xbf };
514
+ if (b === 0xf4)
515
+ return { length: 4, lo: 0x80, hi: 0x8f }; // no > U+10FFFF
516
+ return null;
517
+ }
518
+ /**
519
+ * `bytes.decode("utf-8")` with `errors="strict"`, including the message.
520
+ *
521
+ * `TextDecoder` without `fatal` substitutes U+FFFD, which would let a corrupt fact file
522
+ * through and have `_stamp` write the substitution back over the original bytes — a
523
+ * silent, destructive divergence rather than a loud one. `fatal: true` raises, but with a
524
+ * `TypeError` whose text says nothing about where. `_facts` reads OUTSIDE its `try`, so
525
+ * this message is what a model sees for a binary file in `facts/`, verbatim.
526
+ *
527
+ * The three reasons and the reported span are CPython's, derived by running its decoder
528
+ * over 220 sequences and matched against all of them (the `utf-8 decode` cases in the
529
+ * conformance suite re-run that comparison).
530
+ *
531
+ * `ignoreBOM: true` IS THE DEFAULT SPELLING BACKWARDS, and it is the point. `TextDecoder`
532
+ * defaults `ignoreBOM` to FALSE, which means "treat a leading EF BB BF as a byte-order mark
533
+ * and DROP it". CPython's utf-8 codec has no such rule — `b'\xef\xbb\xbf'.decode('utf-8')`
534
+ * is `'\ufeff'`, one character, and `utf-8-sig` is the codec that strips it. The validator
535
+ * above already accepted EF BB BF as an ordinary three-byte sequence, so this module was
536
+ * checking one thing and returning another: N8 measured a BOM'd checkpoint that CPython
537
+ * REFUSES (`json.loads` answers `Unexpected UTF-8 BOM (decode using utf-8-sig)`) and that
538
+ * this port parsed and then WROTE. Every text read in the package now comes through here,
539
+ * `assets.ts` included, so there is one UTF-8 decode and not two with opposite answers.
540
+ */
541
+ export function pyDecodeUtf8(bytes) {
542
+ let i = 0;
543
+ while (i < bytes.length) {
544
+ const b = bytes[i];
545
+ const info = leadInfo(b);
546
+ if (info === null)
547
+ throw new PyUnicodeDecodeError(bytes, i, i + 1, 'invalid start byte');
548
+ if (info.length === 1) {
549
+ i += 1;
550
+ continue;
551
+ }
552
+ for (let j = 1; j < info.length; j += 1) {
553
+ const at = i + j;
554
+ if (at >= bytes.length) {
555
+ throw new PyUnicodeDecodeError(bytes, i, i + j, 'unexpected end of data');
556
+ }
557
+ const c = bytes[at];
558
+ const lo = j === 1 ? info.lo : 0x80;
559
+ const hi = j === 1 ? info.hi : 0xbf;
560
+ if (c < lo || c > hi) {
561
+ throw new PyUnicodeDecodeError(bytes, i, i + j, 'invalid continuation byte');
562
+ }
563
+ }
564
+ i += info.length;
565
+ }
566
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
567
+ }
568
+ /**
569
+ * `Path.read_text(encoding="utf-8")`: strict UTF-8 AND universal-newline translation.
570
+ *
571
+ * The translation half is N2's `decodeFactBytes`, re-derived nowhere: this reads the bytes,
572
+ * decodes them strictly, and hands the string to the same fold. A CRLF fact file therefore
573
+ * parses in both runtimes, which is the property the codec suite already measures.
574
+ */
575
+ export function pyReadText(path) {
576
+ let raw;
577
+ try {
578
+ raw = readFileSync(path);
579
+ }
580
+ catch (e) {
581
+ throw asPyOSError(e, path, undefined, 'crt');
582
+ }
583
+ return pyDecodeUtf8(raw).replace(/\r\n?/g, '\n');
584
+ }
585
+ /**
586
+ * `io.TextIOWrapper`'s outgoing newline translation, which is `newline=None`'s whole effect.
587
+ *
588
+ * THIS REVERSES N2's RULING, and the reason it was reversed is worth more than the ruling
589
+ * was. N2 wrote LF on every platform, arguing that CRLF "already disagrees with the store's
590
+ * own arithmetic" and that reproducing it "would make the same Fact emit different bytes on
591
+ * two machines". Both halves describe the REFERENCE, not a defect being avoided: CPython's
592
+ * `_check_index_budget` counts the LF string it just built while `write_text` puts CRLF on
593
+ * the disk, and CPython already emits different bytes for the same Fact on Windows and on
594
+ * macOS. A port that "fixes" that is not byte-compatible with the thing it stands beside —
595
+ * and byte-compatibility is the product, because the Python server and this one read and
596
+ * write the SAME store on the SAME machine. MEASURED, run 32646521489: 83 conformance cases
597
+ * differed by exactly this, one byte per line, in files both runtimes had just written.
598
+ *
599
+ * The arithmetic does not move. Python counts the in-memory LF text and so does this port;
600
+ * only the bytes leaving through `write` are translated, which is where CPython does it too.
601
+ *
602
+ * The translation is unconditional on `\n`, INCLUDING the `\n` of a `\r\n`: CPython emits
603
+ * `\r\r\n` for a string that already held a CRLF, and so does this.
604
+ */
605
+ export function toCrlf(text) {
606
+ return text.replace(/\n/g, '\r\n');
607
+ }
608
+ /** The same translation, gated the way CPython gates it: `#ifdef MS_WINDOWS`. */
609
+ export function pyNewlineOut(text) {
610
+ return process.platform === 'win32' ? toCrlf(text) : text;
611
+ }
612
+ /** `os.linesep` as one string — the line terminator a text-mode write actually emits. */
613
+ export const PY_LINESEP = process.platform === 'win32' ? '\r\n' : '\n';
614
+ /** `Path.write_text(text, encoding="utf-8")`, newline translation included. */
615
+ export function pyWriteText(path, text) {
616
+ try {
617
+ writeFileSync(path, pyNewlineOut(text), 'utf8');
618
+ }
619
+ catch (e) {
620
+ throw asPyOSError(e, path, undefined, 'crt');
621
+ }
622
+ }
623
+ /**
624
+ * `path.open("a", encoding="utf-8").write(text)` — the accounting log's append.
625
+ *
626
+ * Same newline translation as `pyWriteText`, for the same reason. One `appendFileSync` is
627
+ * one `open(O_APPEND)/write/close`, which is what CPython's `with` block does, so a line is
628
+ * never interleaved with another process's.
629
+ */
630
+ export function pyAppendText(path, text) {
631
+ try {
632
+ appendFileSync(path, pyNewlineOut(text), 'utf8');
633
+ }
634
+ catch (e) {
635
+ throw asPyOSError(e, path, undefined, 'crt');
636
+ }
637
+ }
638
+ // ------------------------------------------------------------------------- path probes
639
+ /**
640
+ * `os.path.lexists(path)` — `lstat` without following, and FALSE for every error.
641
+ *
642
+ * Not `lstatSync(p, {throwIfNoEntry: false})`: that returns `undefined` for ENOENT but
643
+ * still throws for EACCES on the parent, and `lexists` catches `(OSError, ValueError)`
644
+ * wholesale. This is the middle arm of the `_listing` three-way, so getting the error
645
+ * behaviour wrong turns an unreadable store back into an empty one.
646
+ */
647
+ export function pyLexists(path) {
648
+ try {
649
+ lstatSync(path);
650
+ return true;
651
+ }
652
+ catch {
653
+ return false;
654
+ }
655
+ }
656
+ /**
657
+ * `Path.exists()` — swallows exactly `pathlib._IGNORED_ERRNOS` and re-raises the rest.
658
+ *
659
+ * ENOENT, ENOTDIR, EBADF and ELOOP really do mean "nothing is there"; EACCES means "I was
660
+ * not allowed to look", and `save`'s `path.exists()` before `_write_fact` must raise on it
661
+ * rather than answer `False` and overwrite. `existsSync` answers `false` for all of them.
662
+ */
663
+ const IGNORED = new Set(['ENOENT', 'ENOTDIR', 'EBADF', 'ELOOP']);
664
+ export function pyExists(path) {
665
+ try {
666
+ statSync(path);
667
+ return true;
668
+ }
669
+ catch (e) {
670
+ if (IGNORED.has(e?.code ?? ''))
671
+ return false;
672
+ throw asPyOSError(e, path);
673
+ }
674
+ }
675
+ // ------------------------------------------------------------------------- directories
676
+ /**
677
+ * `Path.mkdir(parents=True, exist_ok=True)`.
678
+ *
679
+ * `exist_ok` suppresses `FileExistsError` only when the path is a DIRECTORY: CPython
680
+ * re-raises when a regular file sits there, which is exactly how `save` fails on a store
681
+ * whose `facts/` is a file — before the listing, with a bare `FileExistsError`
682
+ * (`test_a_facts_path_that_is_not_a_directory_is_unreadable_not_empty` says so). Node's
683
+ * recursive `mkdirSync` raises EEXIST there too; this only restates the error.
684
+ */
685
+ export function pyMkdirParents(path) {
686
+ try {
687
+ mkdirSync(path, { recursive: true });
688
+ }
689
+ catch (e) {
690
+ throw asPyOSError(e, path);
691
+ }
692
+ }
693
+ /**
694
+ * `os.scandir(directory)` reduced to names, raising the way CPython does.
695
+ *
696
+ * The caller decides what a failure MEANS — that decision is `store._listing`'s and
697
+ * `layers.count_facts` makes a different one from the same syscall.
698
+ */
699
+ export function pyScandirNames(directory) {
700
+ try {
701
+ return readdirSync(directory);
702
+ }
703
+ catch (e) {
704
+ throw asPyOSError(scandirShape(e, directory), directory, undefined, 'scandir');
705
+ }
706
+ }
707
+ /**
708
+ * The one place libuv and CPython make DIFFERENT SYSCALLS, not just different sentences.
709
+ *
710
+ * `os.scandir(p)` on Windows is `FindFirstFileW(p + "\\*")`, so `p` is a DIRECTORY COMPONENT
711
+ * of the name being opened; a dangling FILE symlink there is `ERROR_DIRECTORY` (267) and
712
+ * CPython raises `NotADirectoryError`. libuv opens a handle on `p` itself instead, follows
713
+ * the reparse point, finds nothing and reports `ENOENT`. MEASURED, run 32646521489: for the
714
+ * same dangling `facts` symlink the reference says "The directory name is invalid" and this
715
+ * port said "No such file or directory (a path exists there)", and `count_facts` answered
716
+ * `0` where the reference raised. That is not a wording difference — an answer of 0 sends a
717
+ * caller down the success branch — so the SHAPE is restated here and the sentence follows.
718
+ *
719
+ * The probe is `lexists`, not `exists`: the whole point is a name that IS there while what
720
+ * it names is not. `reference-windows-dangling-symlink-two-shapes` is the same finding from
721
+ * the other side, and this is the second shape it predicted.
722
+ */
723
+ function scandirShape(error, directory) {
724
+ const e = error;
725
+ if (process.platform !== 'win32')
726
+ return error;
727
+ if (!scandirIsNotADirectory(e?.code ?? '', () => pyLexists(directory)))
728
+ return error;
729
+ return { ...e, code: 'ENOTDIR', errno: undefined };
730
+ }
731
+ /** The decision inside `scandirShape`, without the platform gate, so a laptop can fail it. */
732
+ export function scandirIsNotADirectory(code, lexists) {
733
+ return code === 'ENOENT' && lexists();
734
+ }
735
+ /**
736
+ * `fnmatch.fnmatch(name, "*.md")`, which is the match `pathlib.glob` performs.
737
+ *
738
+ * Two things it is not: it is not "ends with .md" on Windows, where `os.path.normcase`
739
+ * lowercases first and `FACT.MD` is therefore a fact; and it has no dotfile or
740
+ * directory-entry special case, so `.hidden.md` counts and a DIRECTORY called `x.md`
741
+ * counts. `_listing`'s docstring makes not changing the counted set an explicit
742
+ * requirement, so this deliberately keeps every oddity.
743
+ */
744
+ export function matchesMd(name) {
745
+ return normcase(name).endsWith('.md');
746
+ }
747
+ /** `os.path.normcase` — identity off Windows, `s.replace('/', '\\').lower()` on it. */
748
+ export function normcase(name) {
749
+ return process.platform === 'win32' ? name.replace(/\//g, '\\').toLowerCase() : name;
750
+ }
751
+ // -------------------------------------------------------------------------- comparison
752
+ /**
753
+ * Python's `<` on `str`, which is CODEPOINT order. JS compares UTF-16 code units.
754
+ *
755
+ * They disagree for every astral character: `['\u{1F414}.md', '!.md'].sort()` puts the
756
+ * chicken first because its lead surrogate is 0xD83D, and Python puts it last because
757
+ * 0x1F414 > 0xFF01. `_fact_paths` sorts, `recall` breaks score ties on the name, and the
758
+ * index is written in that order — so a single astral fact name is enough to make two
759
+ * `index.md` files that differ.
760
+ */
761
+ export function cmpCodepoint(a, b) {
762
+ const x = [...a];
763
+ const y = [...b];
764
+ const n = Math.min(x.length, y.length);
765
+ for (let i = 0; i < n; i += 1) {
766
+ const p = x[i].codePointAt(0);
767
+ const q = y[i].codePointAt(0);
768
+ if (p !== q)
769
+ return p < q ? -1 : 1;
770
+ }
771
+ return x.length - y.length;
772
+ }
773
+ /**
774
+ * `sorted(directory / name for name in names)` — sorting `Path`s, not strings.
775
+ *
776
+ * `PurePath` compares `_parts_normcase` tuples. Every path here shares a parent, so the
777
+ * comparison reduces to the last component; what survives is the CASE FOLD, which
778
+ * `_parts_normcase` applies on Windows and nowhere else. Ties under that fold keep listing
779
+ * order, because both runtimes sort stably.
780
+ */
781
+ export function sortedPathNames(names) {
782
+ return [...names].sort((a, b) => cmpCodepoint(normcase(a), normcase(b)));
783
+ }
784
+ /**
785
+ * `sorted(paths)` when the paths are NESTED — the case `sortedPathNames` above cannot cover.
786
+ *
787
+ * `PurePath.__lt__` compares the `_parts_normcase` TUPLE, and a tuple comparison stops at
788
+ * the first component that differs; it never sees the separator. Sorting the same paths as
789
+ * STRINGS does see it, and `-` (0x2D) sorts before `/` (0x2F), so the two orders are
790
+ * opposite for a pair like `a/b` and `a-b`:
791
+ *
792
+ * sorted([Path("assets/a/b"), Path("assets/a-b")]) -> ['assets/a/b', 'assets/a-b']
793
+ * sorted(["assets/a/b", "assets/a-b"]) -> ['assets/a-b', 'assets/a/b']
794
+ *
795
+ * N3 measured that this does not bite `_fact_paths`, whose paths are all siblings of one
796
+ * directory. It DOES bite `mcpserver._tree_digest`, which walks a nested tree and folds the
797
+ * order straight into `assets_digest` — a digest computed in string order over the shipped
798
+ * pack is a different digest, silently, and `build_identity` exists precisely to be
799
+ * trustworthy about that. A shorter tuple sorts first when it is a prefix of the longer one,
800
+ * which is `[] < ['b']` and needs no special case.
801
+ */
802
+ export function sortedPathParts(paths) {
803
+ return paths
804
+ .map((p) => [...p])
805
+ .sort((a, b) => {
806
+ const n = Math.min(a.length, b.length);
807
+ for (let i = 0; i < n; i += 1) {
808
+ const order = cmpCodepoint(normcase(a[i]), normcase(b[i]));
809
+ if (order !== 0)
810
+ return order;
811
+ }
812
+ return a.length - b.length;
813
+ });
814
+ }
815
+ // ----------------------------------------------------------------------- path building
816
+ /**
817
+ * `PurePath.__truediv__` and `str(Path)`, for the shapes a store root can take.
818
+ *
819
+ * Not `path.join`: `join('/a/b/..', 'facts')` collapses to `/a/facts` while
820
+ * `str(Path('/a/b/..') / 'facts')` keeps `/a/b/../facts`. Both open the same file, but the
821
+ * store PRINTS this path in the sentence a model reads for an unreadable store, and a port
822
+ * whose error names a different path than the reference's is a difference nobody put in
823
+ * writing. Empty and `.` components are dropped, `..` is kept, and the anchor survives —
824
+ * which is `PurePath`'s parsing, not a normalization.
825
+ *
826
+ * On Windows the separator is `\` and `/` is also accepted as one; drive-relative paths
827
+ * (`C:foo`) and UNC roots are outside what a store root has ever been and are not modelled.
828
+ */
829
+ export function pyJoin(...parts) {
830
+ if (process.platform === 'win32')
831
+ return winStr(ntJoin(...parts));
832
+ let anchor = '';
833
+ const out = [];
834
+ for (const part of parts) {
835
+ // A leading separator, or a drive: `parsePath` is that reading, shared with `pyParents`
836
+ // and `pyExpanduser` so the four cannot disagree about what a path is made of.
837
+ const parsed = parsePath(part);
838
+ // An ANCHORED later part replaces everything before it — `PurePath('/a', '/b')` is
839
+ // `/b`. `load_grants` is where it fires: `config.parent / entry` with an absolute
840
+ // `entry` is the entry, and joining them instead builds a path under the config that
841
+ // never existed. The store never passes an absolute second part, so this arm was
842
+ // unreachable until the binding layer arrived.
843
+ if (parsed.anchor !== '') {
844
+ anchor = parsed.anchor;
845
+ out.length = 0;
846
+ }
847
+ out.push(...parsed.parts);
848
+ }
849
+ return renderPath(anchor, out);
850
+ }
851
+ function parsePath(path) {
852
+ if (process.platform === 'win32') {
853
+ const { drive, root, tail } = parseWindowsPath(path);
854
+ return { anchor: drive + root, parts: tail };
855
+ }
856
+ const pieces = path.split('/');
857
+ let anchor = '';
858
+ // `''.split('/')` is `['']`, which is not a leading separator: `str(PurePath(''))` is `.`
859
+ // and has no anchor. Measured against CPython through `Path('').expanduser()`.
860
+ if (path !== '' && pieces[0] === '') {
861
+ let leading = 0;
862
+ while (pieces[leading] === '')
863
+ leading += 1;
864
+ anchor = leading === 2 && pieces.length > 2 ? '//' : '/';
865
+ pieces.splice(0, leading);
866
+ }
867
+ return { anchor, parts: pieces.filter((piece) => piece !== '' && piece !== '.') };
868
+ }
869
+ // --------------------------------------------------------------------- ntpath, on Windows
870
+ /**
871
+ * `ntpath.splitroot` — drive, root, and the rest, ported branch for branch.
872
+ *
873
+ * The old spelling of `parsePath` counted leading separators the way `PurePosixPath` does,
874
+ * and that reading is WRONG ON WINDOWS in the direction that changes an answer: `//a/b` is
875
+ * not "a root and two components", it is a UNC share whose whole text is the drive, with no
876
+ * parents at all and nothing for `_walk_to_store` to iterate. MEASURED against CPython 3.12
877
+ * on run 32646521489 — `PureWindowsPath('//a/b').parents` is `[]` and this port answered
878
+ * `['\\\\a', '\\\\']`, which is a walk over two directories that do not exist.
879
+ *
880
+ * The result is spelled with the CALLER's separators, not normalised ones, exactly as
881
+ * `ntpath.splitroot` returns slices of its argument.
882
+ */
883
+ export function ntSplitRoot(path) {
884
+ const norm = path.replace(/\//g, '\\');
885
+ if (norm.startsWith('\\')) {
886
+ if (norm.startsWith('\\\\')) {
887
+ // `\\?\UNC\` is eight characters of prefix before the server name starts.
888
+ const start = norm.slice(0, 8).toUpperCase() === '\\\\?\\UNC\\' ? 8 : 2;
889
+ const index = norm.indexOf('\\', start);
890
+ if (index === -1)
891
+ return [path, '', ''];
892
+ const index2 = norm.indexOf('\\', index + 1);
893
+ if (index2 === -1)
894
+ return [path, '', ''];
895
+ return [path.slice(0, index2), path.slice(index2, index2 + 1), path.slice(index2 + 1)];
896
+ }
897
+ return ['', path.slice(0, 1), path.slice(1)];
898
+ }
899
+ if (norm.slice(1, 2) === ':') {
900
+ if (norm.slice(2, 3) === '\\')
901
+ return [path.slice(0, 2), path.slice(2, 3), path.slice(3)];
902
+ return [path.slice(0, 2), '', path.slice(2)];
903
+ }
904
+ return ['', '', path];
905
+ }
906
+ /**
907
+ * `PurePath._parse_path` under the Windows flavour: drive, root and the `_tail` components.
908
+ *
909
+ * The `drv_parts` arm is not decoration. `splitroot` hands back a bare `\\server\share` with
910
+ * an EMPTY root, and pathlib puts the root back on when the drive names a real share (four
911
+ * pieces) or a `\\?\UNC\server\share` (six) — which is why `//a/b` prints with a trailing
912
+ * separator and `//a` does not.
913
+ */
914
+ export function parseWindowsPath(path) {
915
+ if (path === '')
916
+ return { drive: '', root: '', tail: [] };
917
+ const normalised = path.replace(/\//g, '\\');
918
+ const [drive, splitRoot, rest] = ntSplitRoot(normalised);
919
+ let root = splitRoot;
920
+ if (root === '' && drive.startsWith('\\') && !drive.endsWith('\\')) {
921
+ const pieces = drive.split('\\');
922
+ // `drv_parts[2] not in '?.'` is a SUBSTRING test in Python, and the empty string is a
923
+ // substring of everything: `///a` splits to a third piece of `''`, the test is therefore
924
+ // False, and the path keeps its empty root and stays RELATIVE. Spelling it as two
925
+ // character comparisons made `///a` absolute — caught on a laptop by the case below,
926
+ // which is the entire reason that case exists.
927
+ if (pieces.length === 4 && !'?.'.includes(pieces[2]))
928
+ root = '\\';
929
+ else if (pieces.length === 6)
930
+ root = '\\';
931
+ }
932
+ return { drive, root, tail: rest.split('\\').filter((piece) => piece !== '' && piece !== '.') };
933
+ }
934
+ /**
935
+ * `ntpath.join` — what `PurePath.__truediv__` actually feeds the parser.
936
+ *
937
+ * `PurePath('//a') / 'facts'` is `\\a\facts\`, not `\\a\facts`, because the join produces
938
+ * `//a\facts` and the RE-PARSE reads all of it as a share. Nothing else reproduces that.
939
+ */
940
+ export function ntJoin(...parts) {
941
+ const separators = '\\/';
942
+ let [drive, root, tail] = ntSplitRoot(parts[0] ?? '');
943
+ for (const part of parts.slice(1)) {
944
+ const [partDrive, partRoot, partTail] = ntSplitRoot(part);
945
+ if (partRoot !== '') {
946
+ if (partDrive !== '' || drive === '')
947
+ drive = partDrive;
948
+ root = partRoot;
949
+ tail = partTail;
950
+ continue;
951
+ }
952
+ if (partDrive !== '' && partDrive !== drive) {
953
+ if (partDrive.toLowerCase() !== drive.toLowerCase()) {
954
+ drive = partDrive;
955
+ root = partRoot;
956
+ tail = partTail;
957
+ continue;
958
+ }
959
+ drive = partDrive;
960
+ }
961
+ if (tail !== '' && !separators.includes(tail[tail.length - 1]))
962
+ tail += '\\';
963
+ tail += partTail;
964
+ }
965
+ // A separator has to appear between a bare UNC share and a relative remainder.
966
+ if (tail !== '' && root === '' && drive !== '' && !`:${separators}`.includes(drive[drive.length - 1])) {
967
+ return `${drive}\\${tail}`;
968
+ }
969
+ return drive + root + tail;
970
+ }
971
+ /**
972
+ * `PurePath._format_parsed_parts` under the Windows flavour — `str(PureWindowsPath)`.
973
+ *
974
+ * Exported, and every Windows arm below goes through it, so the flavour has exactly ONE
975
+ * implementation and the conformance suite can drive it against `PureWindowsPath` from a
976
+ * laptop instead of from a runner.
977
+ */
978
+ export function winFormat(drive, root, tail) {
979
+ if (drive !== '' || root !== '')
980
+ return drive + root + tail.join('\\');
981
+ // An anchorless path whose FIRST component would itself parse as a drive gets a `.` in
982
+ // front, so `PureWindowsPath('a', 'C:x')` does not print as a drive-relative path.
983
+ if (tail.length > 0 && ntSplitRoot(tail[0])[0] !== '')
984
+ return ['.', ...tail].join('\\');
985
+ return tail.join('\\') || '.';
986
+ }
987
+ /** `str(PureWindowsPath(path))`. */
988
+ export function winStr(path) {
989
+ const { drive, root, tail } = parseWindowsPath(path);
990
+ return winFormat(drive, root, tail);
991
+ }
992
+ /** `PureWindowsPath.parents`. `//a/b` is a share and has NONE. */
993
+ export function winParents(path) {
994
+ const { drive, root, tail } = parseWindowsPath(path);
995
+ const out = [];
996
+ for (let n = tail.length - 1; n >= 0; n -= 1)
997
+ out.push(winFormat(drive, root, tail.slice(0, n)));
998
+ return out;
999
+ }
1000
+ /** `PureWindowsPath.name`. */
1001
+ export function winName(path) {
1002
+ const { tail } = parseWindowsPath(path);
1003
+ return tail.length === 0 ? '' : tail[tail.length - 1];
1004
+ }
1005
+ /** `PureWindowsPath.is_absolute()` — `bool(drive and root)`, so a bare `//a` is NOT one. */
1006
+ export function winIsAbsolute(path) {
1007
+ const { drive, root } = parseWindowsPath(path);
1008
+ return drive !== '' && root !== '';
1009
+ }
1010
+ /** `PureWindowsPath.with_suffix`, which rebuilds the path in the flavour's own spelling. */
1011
+ export function winWithSuffix(path, suffix) {
1012
+ const { drive, root, tail } = parseWindowsPath(path);
1013
+ if (tail.length === 0)
1014
+ return winFormat(drive, root, tail);
1015
+ const name = tail[tail.length - 1];
1016
+ const dot = suffixDot(name);
1017
+ const stem = dot === -1 ? name : name.slice(0, dot);
1018
+ return winFormat(drive, root, [...tail.slice(0, -1), `${stem}${suffix}`]);
1019
+ }
1020
+ /**
1021
+ * `str(PurePosixPath)` for an already-parsed path: the empty path prints as `.`.
1022
+ *
1023
+ * POSIX ONLY. Every Windows arm renders through `winFormat`; this one had a
1024
+ * `process.platform` separator once and it hid the fact that `pyExpanduser` was calling it
1025
+ * on both platforms with a Windows anchor and a POSIX join.
1026
+ */
1027
+ function renderPath(anchor, parts) {
1028
+ if (parts.length === 0)
1029
+ return anchor === '' ? '.' : anchor;
1030
+ return anchor + parts.join('/');
1031
+ }
1032
+ /**
1033
+ * `PurePath.parents` — `_walk_to_store` iterates `(base, *base.parents)` and stops at `/`.
1034
+ *
1035
+ * The tail is not the anchor: `/a/b` yields `/a` then `/` and stops, while `a/b` yields `a`
1036
+ * then `.` — a relative start walks to the CWD-relative dot and no further, which is why
1037
+ * `_resolved_base` resolves before walking. `/` and `.` have no parents at all.
1038
+ */
1039
+ export function pyParents(path) {
1040
+ if (process.platform === 'win32')
1041
+ return winParents(path);
1042
+ const { anchor, parts } = parsePath(path);
1043
+ const out = [];
1044
+ for (let n = parts.length - 1; n >= 0; n -= 1)
1045
+ out.push(renderPath(anchor, parts.slice(0, n)));
1046
+ return out;
1047
+ }
1048
+ /** `PurePath.parent`. `/`'s parent is `/` and `.`'s is `.`, as in Python. */
1049
+ export function pyParent(path) {
1050
+ const parents = pyParents(path);
1051
+ if (parents.length > 0)
1052
+ return parents[0];
1053
+ if (process.platform === 'win32') {
1054
+ const { drive, root } = parseWindowsPath(path);
1055
+ return winFormat(drive, root, []);
1056
+ }
1057
+ return renderPath(parsePath(path).anchor, []);
1058
+ }
1059
+ /** `PurePath.name` — the last component, or `''` for a bare anchor. */
1060
+ export function pyName(path) {
1061
+ if (process.platform === 'win32')
1062
+ return winName(path);
1063
+ const { parts } = parsePath(path);
1064
+ return parts.length === 0 ? '' : parts[parts.length - 1];
1065
+ }
1066
+ /**
1067
+ * `PurePath.is_absolute()`.
1068
+ *
1069
+ * On Windows a root without a drive is NOT absolute (`PureWindowsPath('/a')` is relative to
1070
+ * the current drive), which is why this is not "starts with a separator". `_pinned_store`
1071
+ * raises on a relative pin, so getting this wrong there would accept a pin the reference
1072
+ * refuses, or refuse one it accepts.
1073
+ */
1074
+ export function pyIsAbsolute(path) {
1075
+ if (process.platform !== 'win32')
1076
+ return parsePath(path).anchor !== '';
1077
+ return winIsAbsolute(path);
1078
+ }
1079
+ // -------------------------------------------------------------------------- home and ~
1080
+ /**
1081
+ * `RuntimeError`, which `Path.expanduser()` and `Path.resolve()` raise. NOT an `OSError`, so
1082
+ * nothing that catches `OSError` catches it — `_pinned_store`'s `except OSError` included.
1083
+ */
1084
+ export class PyRuntimeError extends Error {
1085
+ constructor(message) {
1086
+ super(message);
1087
+ this.name = 'RuntimeError';
1088
+ }
1089
+ }
1090
+ /**
1091
+ * `Path.home()`, which is `os.path.expanduser('~')`.
1092
+ *
1093
+ * `$HOME` FIRST, and only then the passwd database — verified by overriding it. That order
1094
+ * is what lets the tests and the conformance harness point the profile layer somewhere that
1095
+ * is not the operator's own facts; a port that read the uid's home directly would recall
1096
+ * against, and STAMP, the real store on every run.
1097
+ *
1098
+ * An empty `HOME` is still a set `HOME` in Python (the test is `'HOME' not in os.environ`),
1099
+ * and `''.rstrip('/') or '/'` makes it `/`. `os.homedir()` falls back to the passwd entry
1100
+ * there instead, so the rule is spelled out rather than delegated.
1101
+ *
1102
+ * On Windows Python reads `USERPROFILE`, then `HOMEDRIVE`+`HOMEPATH`; `os.homedir()` reads
1103
+ * `USERPROFILE` and then the API. The first arm is the same, and it is the one a host sets.
1104
+ */
1105
+ export function pyHome() {
1106
+ if (process.platform === 'win32') {
1107
+ const profile = process.env['USERPROFILE'];
1108
+ const drive = process.env['HOMEDRIVE'];
1109
+ const tail = process.env['HOMEPATH'];
1110
+ const raw = profile ?? (drive !== undefined && tail !== undefined ? drive + tail : homedir());
1111
+ return raw.replace(/[\\/]+$/, '') || '\\';
1112
+ }
1113
+ const home = process.env['HOME'];
1114
+ const raw = home !== undefined ? home : homedir();
1115
+ return raw.replace(/\/+$/, '') || '/';
1116
+ }
1117
+ /**
1118
+ * `Path(raw).expanduser()`.
1119
+ *
1120
+ * Only a FIRST component beginning with `~` expands, and only when the path has no anchor;
1121
+ * `/~/x` and `a/~` are left alone. An MCP host passes `env` verbatim with no shell, so the
1122
+ * tilde arrives literal and this is the only thing that expands it — see `_pinned_store`.
1123
+ *
1124
+ * RULING — `~someone-else`. Python asks the passwd database and resolves any account on the
1125
+ * machine (measured: `~root` -> `/var/root`). Node has no `getpwnam`, so this resolves `~`
1126
+ * and `~<the current user>` and raises Python's own `RuntimeError` for anything else, where
1127
+ * Python would have answered a path. Reproducing it means shelling out to `dscl`/`getent` at
1128
+ * runtime — a subprocess, in a pure-Node package, for a spelling of the pin nothing has ever
1129
+ * used. Carried as a must-differ case in `tools/conformance/suites/recall-strings.mjs`, so
1130
+ * it is re-measured on every run rather than remembered.
1131
+ */
1132
+ export function pyExpanduser(path) {
1133
+ const { anchor, parts } = parsePath(path);
1134
+ const first = parts[0];
1135
+ if (anchor !== '' || first === undefined || !first.startsWith('~')) {
1136
+ // `str(PurePath(raw))` in the FLAVOUR, which is what `pyJoin` of a single path is.
1137
+ // `renderPath` is the POSIX renderer and joining a Windows tail with it produced
1138
+ // `C:\Users/runneradmin/...` — one backslash from the anchor and forward slashes for
1139
+ // everything after it. MEASURED on windows-latest, run 32649940727: three conformance
1140
+ // cases and two test nodes, all naming a path in that mixed spelling.
1141
+ return pyJoin(path);
1142
+ }
1143
+ let home = null;
1144
+ const user = first.slice(1);
1145
+ if (user === '')
1146
+ home = pyHome();
1147
+ else {
1148
+ try {
1149
+ const info = userInfo();
1150
+ if (info.username === user)
1151
+ home = info.homedir.replace(/\/+$/, '') || '/';
1152
+ }
1153
+ catch {
1154
+ home = null; // no passwd entry at all; Python raises here too
1155
+ }
1156
+ }
1157
+ if (home === null)
1158
+ throw new PyRuntimeError('Could not determine home directory.');
1159
+ return pyJoin(home, ...parts.slice(1));
1160
+ }
1161
+ /** `Path.cwd()`. */
1162
+ export function pyCwd() {
1163
+ return process.cwd();
1164
+ }
1165
+ // --------------------------------------------------------------------------- resolution
1166
+ /**
1167
+ * `Path.resolve()` with `strict=False`: `posixpath.realpath`, then pathlib's ELOOP check.
1168
+ *
1169
+ * WHY NOT `fs.realpathSync`: it is strict. `_resolved_base` resolves a START DIRECTORY that
1170
+ * may not exist, and `load_grants` resolves a granted path precisely in order to then ask
1171
+ * whether it exists. Python resolves as far as the filesystem goes and appends the rest
1172
+ * verbatim — measured, `<bed>/nope/x` resolves to itself, and so does `<0o000-dir>/x/y`.
1173
+ *
1174
+ * `..` is applied to the path resolved SO FAR, not lexically to the input, so through a
1175
+ * symlinked directory it lands in the target's parent. That is what makes a symlinked
1176
+ * worktree bind the store beside its real checkout.
1177
+ *
1178
+ * A symlink LOOP is the one failure that is not swallowed: non-strict `realpath` returns the
1179
+ * unresolved path, `Path.resolve` then stats it, gets ELOOP, and raises
1180
+ * `RuntimeError("Symlink loop from %r" % e.filename)`. The `%r` is `repr`, not a pair of
1181
+ * apostrophes — the second spelling of the `OSError.__str__` bug N8 found, and fixed the
1182
+ * same way. Measured against CPython here.
1183
+ *
1184
+ * WINDOWS IS AN APPROXIMATION AND SAYS SO. `ntpath.realpath` goes through
1185
+ * `GetFinalPathNameByHandle` and returns forms this cannot reproduce; there this resolves
1186
+ * what it can natively and falls back to the lexical form. NOT MEASURED — no Windows runner
1187
+ * has run this port.
1188
+ */
1189
+ export function pyResolve(path) {
1190
+ if (process.platform === 'win32')
1191
+ return resolveWindows(path);
1192
+ const [resolved] = joinRealpath('', pyJoin(path), new Map());
1193
+ const absolute = posixAbspath(resolved);
1194
+ try {
1195
+ statSync(absolute);
1196
+ }
1197
+ catch (e) {
1198
+ // pathlib's `check_eloop`: only a symlink loop is promoted; every other error is ignored.
1199
+ if (e?.code === 'ELOOP') {
1200
+ throw new PyRuntimeError(`Symlink loop from ${pyRepr(e.path ?? absolute)}`);
1201
+ }
1202
+ }
1203
+ return absolute;
1204
+ }
1205
+ /** `posixpath._joinrealpath`, ported statement for statement including the `seen` protocol. */
1206
+ function joinRealpath(path, rest, seen) {
1207
+ let current = path;
1208
+ let remaining = rest;
1209
+ if (remaining.startsWith('/')) {
1210
+ remaining = remaining.slice(1);
1211
+ current = '/';
1212
+ }
1213
+ while (remaining !== '') {
1214
+ const at = remaining.indexOf('/');
1215
+ const name = at === -1 ? remaining : remaining.slice(0, at);
1216
+ remaining = at === -1 ? '' : remaining.slice(at + 1);
1217
+ if (name === '' || name === '.')
1218
+ continue;
1219
+ if (name === '..') {
1220
+ if (current !== '') {
1221
+ const [head, tail] = posixSplit(current);
1222
+ current = head;
1223
+ if (tail === '..')
1224
+ current = posixJoin(posixJoin(current, '..'), '..');
1225
+ }
1226
+ else {
1227
+ current = '..';
1228
+ }
1229
+ continue;
1230
+ }
1231
+ const newpath = posixJoin(current, name);
1232
+ let isLink = false;
1233
+ try {
1234
+ isLink = lstatSync(newpath).isSymbolicLink();
1235
+ }
1236
+ catch {
1237
+ isLink = false; // non-strict: every OSError means "not a link", and the walk goes on
1238
+ }
1239
+ if (!isLink) {
1240
+ current = newpath;
1241
+ continue;
1242
+ }
1243
+ if (seen.has(newpath)) {
1244
+ const cached = seen.get(newpath);
1245
+ if (cached !== null) {
1246
+ current = cached;
1247
+ continue;
1248
+ }
1249
+ return [posixJoin(newpath, remaining), false]; // a loop; non-strict leaves it alone
1250
+ }
1251
+ seen.set(newpath, null);
1252
+ const [resolved, ok] = joinRealpath(current, readlinkSync(newpath), seen);
1253
+ if (!ok)
1254
+ return [posixJoin(resolved, remaining), false];
1255
+ current = resolved;
1256
+ seen.set(newpath, current);
1257
+ }
1258
+ return [current, true];
1259
+ }
1260
+ /** `posixpath.split`: the head keeps its trailing separator only when it is the root. */
1261
+ function posixSplit(path) {
1262
+ const cut = path.lastIndexOf('/');
1263
+ if (cut === -1)
1264
+ return ['', path];
1265
+ const head = path.slice(0, cut + 1);
1266
+ return [head === '/' ? head : head.replace(/\/+$/, ''), path.slice(cut + 1)];
1267
+ }
1268
+ /** `posixpath.join` for two components. */
1269
+ function posixJoin(a, b) {
1270
+ if (b.startsWith('/'))
1271
+ return b;
1272
+ if (a === '' || a.endsWith('/'))
1273
+ return a + b;
1274
+ return `${a}/${b}`;
1275
+ }
1276
+ /** `posixpath.abspath` = `normpath(join(getcwd(), path))`. Two leading slashes survive. */
1277
+ function posixAbspath(path) {
1278
+ const joined = path.startsWith('/') ? path : posixJoin(process.cwd(), path);
1279
+ const leading = /^\/\/(?!\/)/.test(joined) ? '//' : joined.startsWith('/') ? '/' : '';
1280
+ const out = [];
1281
+ for (const piece of joined.split('/')) {
1282
+ if (piece === '' || piece === '.')
1283
+ continue;
1284
+ if (piece === '..' && leading !== '') {
1285
+ if (out.length > 0 && out[out.length - 1] !== '..')
1286
+ out.pop();
1287
+ continue;
1288
+ }
1289
+ if (piece === '..' && out.length > 0 && out[out.length - 1] !== '..') {
1290
+ out.pop();
1291
+ continue;
1292
+ }
1293
+ out.push(piece);
1294
+ }
1295
+ return leading + out.join('/') || '.';
1296
+ }
1297
+ /**
1298
+ * The Windows arm of `pyResolve` — `ntpath.realpath(strict=False)` and pathlib's ELOOP check.
1299
+ *
1300
+ * SWALLOWING THE FAILURE WAS THE DEFECT. This used to answer `realpathSync.native` or, on any
1301
+ * error at all, the lexical path — which is neither of the two things CPython does. MEASURED
1302
+ * on run 32646521489, both halves: a DANGLING symlink resolves to its TARGET on the reference
1303
+ * (`<bed>\dang` -> `<bed>\nowhere`) and this port answered `<bed>\dang`; and a self-referential
1304
+ * symlink raises `RuntimeError("Symlink loop from %r")` on the reference while this port
1305
+ * raised nothing and returned a path. A missing raise is worse than a wrong sentence: the
1306
+ * caller takes the success branch.
1307
+ *
1308
+ * Both come out of `ntpath._getfinalpathname_nonstrict`, which is ported below: when the OS
1309
+ * cannot resolve a name it FOLLOWS THE LINK ITSELF with `readlink` before giving up, and only
1310
+ * then walks up a component. The loop survives that walk — `_readlink_deep` stops when it
1311
+ * revisits a name — and is caught afterwards by the `stat` that `Path.resolve` runs precisely
1312
+ * so that non-strict resolution still raises on a cycle.
1313
+ */
1314
+ function resolveWindows(path) {
1315
+ const absolute = ntIsAbs(path) ? path : ntJoin(process.cwd(), path);
1316
+ let resolved;
1317
+ try {
1318
+ resolved = realpathSync.native(absolute);
1319
+ }
1320
+ catch {
1321
+ resolved = getFinalPathNameNonStrict(absolute);
1322
+ }
1323
+ resolved = pyJoin(resolved);
1324
+ try {
1325
+ statSync(resolved);
1326
+ }
1327
+ catch (e) {
1328
+ // `check_eloop`: `errno == ELOOP or winerror == 1921`. libuv folds
1329
+ // `ERROR_CANT_RESOLVE_FILENAME` into `UV_ELOOP`, so the one code covers both tests.
1330
+ if (e?.code === 'ELOOP') {
1331
+ throw new PyRuntimeError(`Symlink loop from ${pyRepr(e.path ?? resolved)}`);
1332
+ }
1333
+ }
1334
+ return resolved;
1335
+ }
1336
+ /** `ntpath._readlink_deep` — follow the chain by hand, stopping the first time a name repeats. */
1337
+ function readlinkDeep(start) {
1338
+ const seen = new Set();
1339
+ let path = start;
1340
+ while (!seen.has(normcase(path))) {
1341
+ seen.add(normcase(path));
1342
+ let target;
1343
+ try {
1344
+ target = readlinkSync(path);
1345
+ }
1346
+ catch {
1347
+ break; // not a link, or unreadable: what we have is the answer
1348
+ }
1349
+ path = ntIsAbs(target) ? target : ntJoin(ntDirname(path), target);
1350
+ }
1351
+ return path;
1352
+ }
1353
+ /** `ntpath._getfinalpathname_nonstrict` — as much of the target as the OS will resolve. */
1354
+ function getFinalPathNameNonStrict(start) {
1355
+ let path = start;
1356
+ let tail = '';
1357
+ while (path !== '') {
1358
+ try {
1359
+ const got = realpathSync.native(path);
1360
+ return tail === '' ? got : ntJoin(got, tail);
1361
+ }
1362
+ catch {
1363
+ /* every allowed winerror; the walk goes on */
1364
+ }
1365
+ const followed = readlinkDeep(path);
1366
+ if (followed !== path)
1367
+ return tail === '' ? followed : ntJoin(followed, tail);
1368
+ const [head, name] = ntSplit(path);
1369
+ if (head !== '' && name === '')
1370
+ return head + tail;
1371
+ tail = tail === '' ? name : ntJoin(name, tail);
1372
+ path = head;
1373
+ }
1374
+ return tail;
1375
+ }
1376
+ /**
1377
+ * `ntpath.split` — the head keeps its trailing separators only where they are the root.
1378
+ *
1379
+ * Read off `Lib/ntpath.py` at 3.12, which is the interpreter the reference side runs:
1380
+ * `return d + r + head.rstrip(seps), tail`, with no fallback to the unstripped head.
1381
+ */
1382
+ export function ntSplit(path) {
1383
+ const [drive, root, rest] = ntSplitRoot(path);
1384
+ let cut = rest.length;
1385
+ while (cut > 0 && !'\\/'.includes(rest[cut - 1]))
1386
+ cut -= 1;
1387
+ const name = rest.slice(cut);
1388
+ const head = rest.slice(0, cut);
1389
+ return [drive + root + head.replace(/[\\/]+$/, ''), name];
1390
+ }
1391
+ /**
1392
+ * `ntpath.isabs`, which is NOT `PureWindowsPath.is_absolute()`.
1393
+ *
1394
+ * `ntpath` calls `\a` absolute — a leading separator is enough — while pathlib calls it
1395
+ * relative, because a rooted path with no drive is relative to the CURRENT drive. CPython's
1396
+ * own source labels the difference a LEGACY BUG and keeps it. Both readings are right for
1397
+ * their own caller, and
1398
+ * `realpath` is `ntpath`'s caller: it prepends the cwd on `not isabs(path)`, so using
1399
+ * pathlib's stricter test there would join the cwd onto a path Windows already resolves
1400
+ * from the drive root.
1401
+ */
1402
+ export function ntIsAbs(path) {
1403
+ // `Lib/ntpath.py`, verbatim: only the first THREE characters are looked at.
1404
+ const head = path.slice(0, 3).replace(/\//g, '\\');
1405
+ return head.startsWith('\\') || head.slice(1).startsWith(':\\');
1406
+ }
1407
+ /** `ntpath.dirname`. */
1408
+ function ntDirname(path) {
1409
+ return ntSplit(path)[0];
1410
+ }
1411
+ // -------------------------------------------------------------------------- stat probes
1412
+ /**
1413
+ * `Path.is_dir()` — and it RE-RAISES, which is the half that matters.
1414
+ *
1415
+ * `pathlib` swallows only `_IGNORED_ERRNOS`; `EACCES` is not one of them, so a candidate
1416
+ * inside a directory the process cannot traverse raises `PermissionError` out of the WALK
1417
+ * itself. MEASURED against CPython 3.12 here: `_walk_to_store` through a `0o000` ancestor
1418
+ * raises rather than skipping the candidate, which refutes the claim in `_pinned_store`'s
1419
+ * docstring that the walk "lives with" an unreadable candidate. `existsSync` and
1420
+ * `statSync(p, {throwIfNoEntry: false})` both answer `false` there and would bind a
1421
+ * different store in silence.
1422
+ */
1423
+ export function pyIsDir(path) {
1424
+ try {
1425
+ return statSync(path).isDirectory();
1426
+ }
1427
+ catch (e) {
1428
+ if (IGNORED.has(e?.code ?? ''))
1429
+ return false;
1430
+ throw asPyOSError(e, path);
1431
+ }
1432
+ }
1433
+ /** `Path.is_file()`, with the same swallow set as `pyIsDir`. */
1434
+ export function pyIsFile(path) {
1435
+ try {
1436
+ return statSync(path).isFile();
1437
+ }
1438
+ catch (e) {
1439
+ if (IGNORED.has(e?.code ?? ''))
1440
+ return false;
1441
+ throw asPyOSError(e, path);
1442
+ }
1443
+ }
1444
+ /**
1445
+ * `os.stat(p)` reduced to "is it a directory", unguarded.
1446
+ *
1447
+ * `_pinned_store` uses `os.stat` rather than `is_dir()` on purpose: a pin is a single path
1448
+ * the operator named out loud, so an unreadable one gets the accurate reason instead of
1449
+ * being reported as a typo.
1450
+ */
1451
+ export function pyStatIsDir(path) {
1452
+ try {
1453
+ return statSync(path).isDirectory();
1454
+ }
1455
+ catch (e) {
1456
+ throw asPyOSError(e, path);
1457
+ }
1458
+ }
1459
+ // --------------------------------------------------------------------------------- repr
1460
+ /**
1461
+ * `repr(str)`, which `_pinned_store` interpolates with `{raw!r}` for a relative pin.
1462
+ *
1463
+ * Python quotes with `'` unless the value contains a `'` and no `"`; it escapes `\\`, the
1464
+ * quote, `\t`, `\n`, `\r`, and every codepoint that is not `str.isprintable()` — that is,
1465
+ * everything in a `C*` category and every separator except the space itself. JS spells the
1466
+ * same predicate `\p{C}` / `\p{Z}` under the `u` flag, so this is the rule rather than a
1467
+ * table. The escapes are `\xNN` below U+0100, `\uNNNN` below U+10000, `\UNNNNNNNN` above.
1468
+ *
1469
+ * The one place it can drift is a codepoint whose category changed between CPython 3.12's
1470
+ * Unicode 15.0 and the ICU this Node was built against: a newly assigned character is `Cn`
1471
+ * (not printable, escaped) for Python and assigned (printable, raw) here. A pin path made of
1472
+ * brand-new codepoints is the only input that reaches it.
1473
+ */
1474
+ export function pyRepr(value) {
1475
+ const quote = value.includes("'") && !value.includes('"') ? '"' : "'";
1476
+ let out = quote;
1477
+ for (const ch of value) {
1478
+ if (ch === '\\')
1479
+ out += '\\\\';
1480
+ else if (ch === quote)
1481
+ out += `\\${ch}`;
1482
+ else if (ch === '\t')
1483
+ out += '\\t';
1484
+ else if (ch === '\n')
1485
+ out += '\\n';
1486
+ else if (ch === '\r')
1487
+ out += '\\r';
1488
+ else if (ch === ' ' || !/^(?:\p{C}|\p{Z})$/u.test(ch))
1489
+ out += ch;
1490
+ else {
1491
+ const cp = ch.codePointAt(0);
1492
+ if (cp < 0x100)
1493
+ out += `\\x${cp.toString(16).padStart(2, '0')}`;
1494
+ else if (cp < 0x10000)
1495
+ out += `\\u${cp.toString(16).padStart(4, '0')}`;
1496
+ else
1497
+ out += `\\U${cp.toString(16).padStart(8, '0')}`;
1498
+ }
1499
+ }
1500
+ return out + quote;
1501
+ }
1502
+ /**
1503
+ * `repr(float)`.
1504
+ *
1505
+ * CPython formats with `PyOS_double_to_string(v, 'r', 0, Py_DTSF_ADD_DOT_0)`: the shortest
1506
+ * decimal that round-trips, rendered in scientific notation when the decimal point falls at
1507
+ * or left of position -4 or right of position 16, and with a forced `.0` otherwise. JS
1508
+ * agrees on the DIGITS (`toExponential()` with no argument is also shortest-round-trip) and
1509
+ * on nothing else: it switches to scientific at 1e21 and 1e-7, writes `10000000000000000`
1510
+ * where Python writes `1e+16`, and never pads the exponent to two digits.
1511
+ */
1512
+ export function pyFloatRepr(x) {
1513
+ if (Number.isNaN(x))
1514
+ return 'nan';
1515
+ if (x === Infinity)
1516
+ return 'inf';
1517
+ if (x === -Infinity)
1518
+ return '-inf';
1519
+ const negative = x < 0 || Object.is(x, -0);
1520
+ const [mantissa, exponent] = Math.abs(x).toExponential().split('e');
1521
+ const digits = mantissa.replace('.', '');
1522
+ const decpt = Number(exponent) + 1; // digits[0] sits just left of position `decpt`
1523
+ const sign = negative ? '-' : '';
1524
+ if (decpt <= -4 || decpt > 16) {
1525
+ const head = digits.slice(0, 1);
1526
+ const tail = digits.slice(1).replace(/0+$/, '');
1527
+ const e = decpt - 1;
1528
+ const eSign = e < 0 ? '-' : '+';
1529
+ return `${sign}${head}${tail ? `.${tail}` : ''}e${eSign}${String(Math.abs(e)).padStart(2, '0')}`;
1530
+ }
1531
+ if (decpt <= 0)
1532
+ return `${sign}0.${'0'.repeat(-decpt)}${digits}`;
1533
+ if (decpt >= digits.length)
1534
+ return `${sign}${digits}${'0'.repeat(decpt - digits.length)}.0`;
1535
+ return `${sign}${digits.slice(0, decpt)}.${digits.slice(decpt)}`;
1536
+ }
1537
+ /**
1538
+ * `PurePath.with_suffix(suffix)` on the LAST component.
1539
+ *
1540
+ * `_write_fact` writes `path.with_suffix(".md.tmp")`, and the rule is not "append": a name
1541
+ * whose last component already has a suffix has that suffix REPLACED, while a component
1542
+ * that is all leading dots has one appended. `facts/a.b.md` -> `facts/a.b.md.tmp`, and the
1543
+ * degenerate `facts/.md` (a fact whose frontmatter name is empty) -> `facts/.md.md.tmp`.
1544
+ */
1545
+ export function pyWithSuffix(path, suffix) {
1546
+ // `with_suffix` rebuilds a `PurePath`, so the answer comes back in the flavour's own
1547
+ // spelling: `PureWindowsPath('/f/a.md').with_suffix('.md.tmp')` is `\f\a.md.tmp`, with the
1548
+ // separators the CALLER wrote replaced. MEASURED against CPython 3.12; slicing the input
1549
+ // string kept `/f/a.md.tmp`, which is a path the reference never prints.
1550
+ if (process.platform === 'win32')
1551
+ return winWithSuffix(path, suffix);
1552
+ const { anchor, parts } = parsePath(path);
1553
+ if (parts.length === 0)
1554
+ return renderPath(anchor, parts);
1555
+ const name = parts[parts.length - 1];
1556
+ const dot = suffixDot(name);
1557
+ const stem = dot === -1 ? name : name.slice(0, dot);
1558
+ return renderPath(anchor, [...parts.slice(0, -1), `${stem}${suffix}`]);
1559
+ }
1560
+ /**
1561
+ * `PurePath.suffix`: the last dot counts only when it is neither the first character of the
1562
+ * name nor the last one, so `.md` has no suffix and `a.` has none either.
1563
+ */
1564
+ function suffixDot(name) {
1565
+ const dot = name.lastIndexOf('.');
1566
+ return dot > 0 && dot < name.length - 1 ? dot : -1;
1567
+ }
1568
+ /**
1569
+ * `PurePath.suffix` as a value, which `shiftwork.clock_out` needs on its own:
1570
+ * `path.with_suffix(path.suffix + ".tmp")` turns `cp.json` into `cp.json.tmp` and a
1571
+ * suffix-less `cp` into `cp.tmp`, and the two arms are only distinguishable by reading the
1572
+ * suffix first.
1573
+ */
1574
+ export function pySuffix(path) {
1575
+ const dot = suffixDot(pyName(path));
1576
+ return dot === -1 ? '' : pyName(path).slice(dot);
1577
+ }
1578
+ /** `Path.unlink()` and `Path.replace(target)`, with CPython's error text. */
1579
+ export function pyUnlink(path) {
1580
+ try {
1581
+ unlinkSync(path);
1582
+ }
1583
+ catch (e) {
1584
+ throw asPyOSError(e, path);
1585
+ }
1586
+ }
1587
+ export function pyReplace(source, target) {
1588
+ try {
1589
+ renameSync(source, target);
1590
+ }
1591
+ catch (e) {
1592
+ // TWO filenames: `os.replace` sets `filename2`, so `str(e)` is `... : 'src' -> 'dst'`.
1593
+ throw asPyOSError(e, source, target);
1594
+ }
1595
+ }
1596
+ /** `date.fromtimestamp(path.stat().st_mtime).isoformat()` — LOCAL, like `date.today()`. */
1597
+ export function pyMtimeDate(path) {
1598
+ let ms;
1599
+ try {
1600
+ ms = statSync(path).mtimeMs;
1601
+ }
1602
+ catch (e) {
1603
+ throw asPyOSError(e, path);
1604
+ }
1605
+ const when = new Date(ms);
1606
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
1607
+ return `${pad(when.getFullYear(), 4)}-${pad(when.getMonth() + 1)}-${pad(when.getDate())}`;
1608
+ }