@vibe-agent-toolkit/utils 0.2.0-rc.2 → 0.2.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -15
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +657 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +765 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +31 -7
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +17 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +69 -0
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +181 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +232 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +94 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +135 -3
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +38 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +103 -80
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +12 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +12 -4
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +6 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +24 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -30
- package/dist/index.js.map +1 -1
- package/dist/path-core.d.ts +1 -1
- package/dist/path-core.js +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +6 -3
- package/dist/project-utils.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.d.ts +38 -10
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +88 -19
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +28 -0
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +51 -2
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +47 -3
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/test-helpers.d.ts +185 -13
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +274 -27
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing.d.ts +73 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +93 -3
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +348 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +53 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +136 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +230 -0
- package/dist/timing-dump.js.map +1 -0
- package/eslint/README.md +105 -3
- package/eslint/index.cjs +50 -0
- package/eslint/index.d.cts +14 -4
- package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
- package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
- package/eslint/rules/no-raw-text-decode.cjs +296 -0
- package/eslint/rules/no-self-package-import.cjs +157 -0
- package/package.json +13 -8
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -133
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -101
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The one way VAT turns file bytes into text.**
|
|
3
|
+
*
|
|
4
|
+
* `local/no-raw-text-decode` enforces that — but only where it is registered,
|
|
5
|
+
* which today is `packages/utils/src` and `packages/resources/src`. Those two
|
|
6
|
+
* own the seam and every corpus-document read; the rest of the repo is a
|
|
7
|
+
* migration ledger, not a covered claim, and `eslint.config.js` carries it in
|
|
8
|
+
* cost order. Do not read this docstring as "nothing else in the repo decodes".
|
|
9
|
+
*
|
|
10
|
+
* ## Where this lives, and why it is a `utils` primitive
|
|
11
|
+
*
|
|
12
|
+
* Bytes-to-text is a pure function of its argument. It knows nothing about
|
|
13
|
+
* content keys, parse caches or the projection — `readContentWithKey` in
|
|
14
|
+
* `@vibe-agent-toolkit/resources` *composes* this with a raw-bytes content key,
|
|
15
|
+
* and the key is the projection concept, not the decode.
|
|
16
|
+
*
|
|
17
|
+
* It has to live here for a harder reason than tidiness. `resources` depends on
|
|
18
|
+
* `utils` and `utils` must never depend on `resources`, so a seam in `resources`
|
|
19
|
+
* with a lint rule shipped from `utils` would flag `utils`' own reads
|
|
20
|
+
* (`gitignore-checker.ts` reads an adopter's `.gitignore`; `project-utils.ts`
|
|
21
|
+
* reads an adopter's `package.json`) while giving them no legal way to comply.
|
|
22
|
+
* The rule would then be widened with exemptions until it meant nothing. Placing
|
|
23
|
+
* the primitive at the bottom of the arrow is what makes the guardrail
|
|
24
|
+
* enforceable.
|
|
25
|
+
*
|
|
26
|
+
* `./text` is also deliberately **pure** — no `node:*` import at all — so bytes
|
|
27
|
+
* from a git blob, an HTTP body or a zip entry decode through the same function
|
|
28
|
+
* as bytes from disk. The file-reading half is in `./fs` (`readTextContent`),
|
|
29
|
+
* where everything that touches `node:fs` lives.
|
|
30
|
+
*
|
|
31
|
+
* ## The defect this exists for
|
|
32
|
+
*
|
|
33
|
+
* `readContentWithKey` (in `@vibe-agent-toolkit/resources`) used to call
|
|
34
|
+
* `bytes.toString('utf-8')` unconditionally. Measured end to end, on a real
|
|
35
|
+
* `working-tree-encoding=UTF-16` checkout (see `resources`'
|
|
36
|
+
* `test/system/git-hostile-config.system.test.ts`):
|
|
37
|
+
*
|
|
38
|
+
* ```text
|
|
39
|
+
* bytes on disk 40 B, BOM fe ff, UTF-16BE
|
|
40
|
+
* decoded "��# \0D\0o\0c\0…" NUL-interleaved mojibake
|
|
41
|
+
* looksBinary true — the decoded string carries NULs
|
|
42
|
+
* projection BLOB_NOT_TEXT, no blob row, 0 sections, 0 references
|
|
43
|
+
* the same doc 1 heading, 1 link, from its UTF-8 bytes
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* So VAT could not read a UTF-16 document *at all*, and the reason it matters is
|
|
47
|
+
* not exotic: **PowerShell 5.1's `Out-File` and `>` write UTF-16LE by default**,
|
|
48
|
+
* so a Windows-authored document lands squarely in that hole.
|
|
49
|
+
*
|
|
50
|
+
* ## What is a fact here, and what is an assumption
|
|
51
|
+
*
|
|
52
|
+
* The distinction is carried in the result ({@link TextProvenance.encodingSource})
|
|
53
|
+
* rather than left to prose, because the two are not the same kind of claim:
|
|
54
|
+
*
|
|
55
|
+
* | input | encoding | encodingSource | why |
|
|
56
|
+
* |---|---|---|---|
|
|
57
|
+
* | leading `ef bb bf` | `utf-8` | `bom` | the bytes say so |
|
|
58
|
+
* | leading `ff fe` (not `ff fe 00 00`) | `utf-16le` | `bom` | the bytes say so |
|
|
59
|
+
* | leading `fe ff` | `utf-16be` | `bom` | the bytes say so |
|
|
60
|
+
* | leading `ff fe 00 00` | `utf-32le` | `bom` | the bytes say so |
|
|
61
|
+
* | leading `00 00 fe ff` | `utf-32be` | `bom` | the bytes say so |
|
|
62
|
+
* | anything else | `utf-8` | `assumed` | the defensible default |
|
|
63
|
+
*
|
|
64
|
+
* ## And what proves the assumption WRONG
|
|
65
|
+
*
|
|
66
|
+
* `encoding` says what was guessed; {@link TextProvenance.replacementCharacters}
|
|
67
|
+
* is what the guess cost. A malformed sequence decodes to U+FFFD instead of
|
|
68
|
+
* throwing, so a mis-decoded document arrives as a well-formed JS string full of
|
|
69
|
+
* garbage — and a byte-level BPE tokenizer has no out-of-vocabulary concept, so
|
|
70
|
+
* it embeds and indexes that garbage without erroring anywhere. Counting the
|
|
71
|
+
* substitutions is what turns "we assumed UTF-8" into "we assumed UTF-8 and were
|
|
72
|
+
* demonstrably wrong 3,200 times in this file".
|
|
73
|
+
*
|
|
74
|
+
* It is counted **without paying for it on the clean path**. Every decode runs
|
|
75
|
+
* first through a `fatal: true` decoder, which throws on the first malformed
|
|
76
|
+
* sequence rather than substituting; a file that decodes cleanly — nearly every
|
|
77
|
+
* file — costs exactly one decode and no scan at all. Only a file that actually
|
|
78
|
+
* threw is decoded a second time in substituting mode and scanned for U+FFFD, so
|
|
79
|
+
* the O(n) scan is charged entirely to broken input.
|
|
80
|
+
*
|
|
81
|
+
* That ordering also buys a correctness property a scan alone cannot have: a
|
|
82
|
+
* document that *legitimately contains* U+FFFD is valid input, so the fatal
|
|
83
|
+
* decoder does not throw and it is reported as **0** replacements rather than
|
|
84
|
+
* accused of a bad decode. A bare scan would count its own content against it.
|
|
85
|
+
*
|
|
86
|
+
* 🪤 **The UTF-32LE BOM starts with the UTF-16LE BOM.** `ff fe 00 00` matches
|
|
87
|
+
* `ff fe`, so a table tested shortest-first decodes every UTF-32LE document as
|
|
88
|
+
* NUL-interleaved UTF-16 — the same bug this module exists to fix, one encoding
|
|
89
|
+
* further down. {@link BOMS} is therefore ordered longest-first and the test
|
|
90
|
+
* suite pins that ordering directly.
|
|
91
|
+
*
|
|
92
|
+
* ## The two limitations, recorded rather than guessed around
|
|
93
|
+
*
|
|
94
|
+
* - **BOM-less UTF-16 is not detected.** It is undecidable from bytes alone: the
|
|
95
|
+
* same byte string is a legal, different UTF-8 document. A NUL-density
|
|
96
|
+
* heuristic would decide it *usually* correctly and silently wrongly the rest
|
|
97
|
+
* of the time, and "silently wrongly" is the failure class this whole module
|
|
98
|
+
* is a reaction to. BOM-less input is UTF-8.
|
|
99
|
+
* - **Latin charsets are not detected either, and there is no windows-1252
|
|
100
|
+
* fallback.** "These bytes are not valid UTF-8" is a fact; "therefore they are
|
|
101
|
+
* latin-1" is a guess, and it is equally consistent with a UTF-8 document
|
|
102
|
+
* carrying one corrupt byte. Malformed input gets U+FFFD. In practice the
|
|
103
|
+
* Latin family costs little: every ASCII byte string is valid UTF-8 and
|
|
104
|
+
* decodes correctly, so only high bytes are affected.
|
|
105
|
+
*
|
|
106
|
+
* Both are pinned as tests that state what is given up, so adding a heuristic
|
|
107
|
+
* later has to edit an assertion rather than quietly widen a claim.
|
|
108
|
+
*
|
|
109
|
+
* ## Why `TextDecoder` for three encodings and hand-rolled code for two
|
|
110
|
+
*
|
|
111
|
+
* `TextDecoder` implements the WHATWG Encoding Standard, which **deliberately
|
|
112
|
+
* omits UTF-32** — no engine offers it. Node's `Buffer` is narrower still:
|
|
113
|
+
* `utf8`, `utf16le`/`ucs2`, `latin1`, `ascii` and the binary-to-text codecs, with
|
|
114
|
+
* **no UTF-16BE at all** (the encoding a round trip through git's
|
|
115
|
+
* `working-tree-encoding=UTF-16` actually produces). So `TextDecoder` carries
|
|
116
|
+
* utf-8/utf-16le/utf-16be, and UTF-32 is converted here.
|
|
117
|
+
*
|
|
118
|
+
* Refusing UTF-32 loudly was the alternative, and it was rejected for one
|
|
119
|
+
* reason: the BOM has to be *recognised* regardless (see the trap above), so the
|
|
120
|
+
* choice was never "detect it or not" — only "having detected it, decode it or
|
|
121
|
+
* throw". Decoding is ~20 lines and leaves no hole.
|
|
122
|
+
*
|
|
123
|
+
* ## The BOM is stripped, and that is load-bearing in both directions
|
|
124
|
+
*
|
|
125
|
+
* A surviving leading U+FEFF stops `# Heading` from parsing as a heading. And
|
|
126
|
+
* every offset downstream is a **character** offset over this decoded string —
|
|
127
|
+
* `lineStartOffsets` in `resources`' `projection/blob-sections.ts` derives them
|
|
128
|
+
* from `content.split('\n')`, and `parseMarkdownContent` takes remark's own
|
|
129
|
+
* character positions — so stripping shifts all of them consistently. What must
|
|
130
|
+
* NOT shift is the content key, which is computed over the raw bytes; see
|
|
131
|
+
* `resources`' `content-key.ts`.
|
|
132
|
+
*/
|
|
133
|
+
/**
|
|
134
|
+
* Byte-order marks, **longest first**.
|
|
135
|
+
*
|
|
136
|
+
* The ordering is the correctness property, not a formatting choice — see the
|
|
137
|
+
* module docstring's trap. Frozen so a caller cannot reorder it in place.
|
|
138
|
+
*/
|
|
139
|
+
const BOMS = Object.freeze([
|
|
140
|
+
{ bytes: [0xff, 0xfe, 0x00, 0x00], encoding: 'utf-32le' },
|
|
141
|
+
{ bytes: [0x00, 0x00, 0xfe, 0xff], encoding: 'utf-32be' },
|
|
142
|
+
{ bytes: [0xef, 0xbb, 0xbf], encoding: 'utf-8' },
|
|
143
|
+
{ bytes: [0xff, 0xfe], encoding: 'utf-16le' },
|
|
144
|
+
{ bytes: [0xfe, 0xff], encoding: 'utf-16be' },
|
|
145
|
+
]);
|
|
146
|
+
/**
|
|
147
|
+
* One `TextDecoder` per encoding, built once.
|
|
148
|
+
*
|
|
149
|
+
* Constructing a decoder per call is measurable on a corpus of thousands of
|
|
150
|
+
* files, and these are stateless when `stream: false` (the default for
|
|
151
|
+
* `decode()`), so one instance is safe to share.
|
|
152
|
+
*
|
|
153
|
+
* `ignoreBOM: true` on purpose: this module has already removed the BOM by the
|
|
154
|
+
* time a decoder sees the bytes, and leaving the option at its default would put
|
|
155
|
+
* a second, silent BOM-stripping step behind the deliberate one — so a document
|
|
156
|
+
* whose *content* legitimately begins with U+FEFF would lose a character that
|
|
157
|
+
* nothing here decided to remove.
|
|
158
|
+
*/
|
|
159
|
+
const DECODERS = new Map([
|
|
160
|
+
// No `eslint-disable` needed: this file is the `exemptFiles` entry for
|
|
161
|
+
// `local/no-raw-text-decode` in the repo's `eslint.config.js`. If the rule
|
|
162
|
+
// starts firing here, that entry has drifted from this path.
|
|
163
|
+
['utf-8', new TextDecoder('utf-8', { ignoreBOM: true })],
|
|
164
|
+
['utf-16le', new TextDecoder('utf-16le', { ignoreBOM: true })],
|
|
165
|
+
['utf-16be', new TextDecoder('utf-16be', { ignoreBOM: true })],
|
|
166
|
+
]);
|
|
167
|
+
/**
|
|
168
|
+
* The same three encodings in **fatal** mode — throw rather than substitute.
|
|
169
|
+
*
|
|
170
|
+
* This is the whole cost model for {@link TextProvenance.replacementCharacters}.
|
|
171
|
+
* A substituting decoder cannot tell a caller whether it substituted, so the only
|
|
172
|
+
* other way to know is to scan every decoded string for U+FFFD — an O(n) pass
|
|
173
|
+
* over every file in a corpus to learn that almost none of them needed it. A
|
|
174
|
+
* fatal decoder answers the same question by *not throwing*, at no extra cost on
|
|
175
|
+
* the clean path, and the expensive route is taken only where there is genuinely
|
|
176
|
+
* something to count.
|
|
177
|
+
*
|
|
178
|
+
* Same `ignoreBOM: true` as their substituting twins, for the same reason: the
|
|
179
|
+
* BOM is already gone by the time either sees the bytes.
|
|
180
|
+
*/
|
|
181
|
+
const FATAL_DECODERS = new Map([
|
|
182
|
+
['utf-8', new TextDecoder('utf-8', { ignoreBOM: true, fatal: true })],
|
|
183
|
+
['utf-16le', new TextDecoder('utf-16le', { ignoreBOM: true, fatal: true })],
|
|
184
|
+
['utf-16be', new TextDecoder('utf-16be', { ignoreBOM: true, fatal: true })],
|
|
185
|
+
]);
|
|
186
|
+
/** How many code points to spread into one `String.fromCodePoint` call. */
|
|
187
|
+
const CODE_POINT_CHUNK = 4096;
|
|
188
|
+
/** The last Unicode code point. */
|
|
189
|
+
const MAX_CODE_POINT = 0x10_ff_ff;
|
|
190
|
+
/** First and last UTF-16 surrogate — never a scalar value on their own. */
|
|
191
|
+
const SURROGATE_FIRST = 0xd8_00;
|
|
192
|
+
const SURROGATE_LAST = 0xdf_ff;
|
|
193
|
+
/** U+FFFD REPLACEMENT CHARACTER, what a malformed unit becomes. */
|
|
194
|
+
const REPLACEMENT_CODE_POINT = 0xff_fd;
|
|
195
|
+
/** Does `bytes` begin with `prefix`? */
|
|
196
|
+
function startsWith(bytes, prefix) {
|
|
197
|
+
if (bytes.length < prefix.length)
|
|
198
|
+
return false;
|
|
199
|
+
return prefix.every((byte, index) => bytes[index] === byte);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The encoding a BOM announces, and how many bytes it occupies.
|
|
203
|
+
*
|
|
204
|
+
* @param bytes - Raw bytes, from the start of the file
|
|
205
|
+
* @returns The BOM's encoding and length, or `null` when there is no BOM
|
|
206
|
+
*/
|
|
207
|
+
function bomAt(bytes) {
|
|
208
|
+
for (const bom of BOMS) {
|
|
209
|
+
if (startsWith(bytes, bom.bytes)) {
|
|
210
|
+
return { encoding: bom.encoding, length: bom.bytes.length };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Is `codePoint` something `String.fromCodePoint` will accept?
|
|
217
|
+
*
|
|
218
|
+
* A lone surrogate and anything past U+10FFFF both throw, and a corrupt document
|
|
219
|
+
* must not surface as an exception out of a decoder.
|
|
220
|
+
*/
|
|
221
|
+
function isScalarValue(codePoint) {
|
|
222
|
+
if (codePoint > MAX_CODE_POINT)
|
|
223
|
+
return false;
|
|
224
|
+
return codePoint < SURROGATE_FIRST || codePoint > SURROGATE_LAST;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Build a string from code points, in chunks.
|
|
228
|
+
*
|
|
229
|
+
* `String.fromCodePoint(...points)` blows the engine's argument limit somewhere
|
|
230
|
+
* in the tens of thousands, which a real document reaches — so the spread is
|
|
231
|
+
* bounded rather than whole-array.
|
|
232
|
+
*/
|
|
233
|
+
function fromCodePoints(points) {
|
|
234
|
+
if (points.length <= CODE_POINT_CHUNK)
|
|
235
|
+
return String.fromCodePoint(...points);
|
|
236
|
+
const parts = [];
|
|
237
|
+
for (let start = 0; start < points.length; start += CODE_POINT_CHUNK) {
|
|
238
|
+
parts.push(String.fromCodePoint(...points.slice(start, start + CODE_POINT_CHUNK)));
|
|
239
|
+
}
|
|
240
|
+
return parts.join('');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Decode UTF-32, by hand, because no engine does.
|
|
244
|
+
*
|
|
245
|
+
* Every 4-byte unit that is not a Unicode scalar value becomes U+FFFD, as does a
|
|
246
|
+
* trailing run of 1–3 bytes that cannot form a unit. That mirrors what
|
|
247
|
+
* `TextDecoder` does with malformed input, so the two paths fail the same way.
|
|
248
|
+
*
|
|
249
|
+
* There is no fatal-first pass here and none is needed: this loop *decides* each
|
|
250
|
+
* substitution, so it can count them as it makes them — free, and exact. The
|
|
251
|
+
* fatal-decoder trick exists only because `TextDecoder` refuses to say.
|
|
252
|
+
*
|
|
253
|
+
* @param bytes - The content bytes, BOM already removed
|
|
254
|
+
* @param littleEndian - Byte order the BOM announced
|
|
255
|
+
* @returns The decoded string and how many units were replaced
|
|
256
|
+
*/
|
|
257
|
+
function decodeUtf32(bytes, littleEndian) {
|
|
258
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
259
|
+
const whole = bytes.byteLength - (bytes.byteLength % 4);
|
|
260
|
+
const points = [];
|
|
261
|
+
let replacementCharacters = 0;
|
|
262
|
+
for (let offset = 0; offset < whole; offset += 4) {
|
|
263
|
+
const codePoint = view.getUint32(offset, littleEndian);
|
|
264
|
+
if (isScalarValue(codePoint)) {
|
|
265
|
+
points.push(codePoint);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
points.push(REPLACEMENT_CODE_POINT);
|
|
269
|
+
replacementCharacters += 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (whole !== bytes.byteLength) {
|
|
273
|
+
points.push(REPLACEMENT_CODE_POINT);
|
|
274
|
+
replacementCharacters += 1;
|
|
275
|
+
}
|
|
276
|
+
return { text: fromCodePoints(points), replacementCharacters };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Count the U+FFFD in a string that a fatal decode already refused.
|
|
280
|
+
*
|
|
281
|
+
* Only ever called on input known to be malformed, which is what keeps the O(n)
|
|
282
|
+
* scan off the common path. A scan rather than a regex or a split: it allocates
|
|
283
|
+
* nothing on a string that may be megabytes long. U+FFFD is a BMP character, so
|
|
284
|
+
* the comparison is exact whether the index lands on it or on a surrogate half.
|
|
285
|
+
*
|
|
286
|
+
* @param text - The substituting decoder's output
|
|
287
|
+
* @returns How many replacement characters it contains
|
|
288
|
+
*/
|
|
289
|
+
function countReplacementCharacters(text) {
|
|
290
|
+
let count = 0;
|
|
291
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
292
|
+
if (text.codePointAt(index) === REPLACEMENT_CODE_POINT)
|
|
293
|
+
count += 1;
|
|
294
|
+
}
|
|
295
|
+
return count;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* **The canonical content-decoding seam.** Turn file bytes into the text a
|
|
299
|
+
* parser should see.
|
|
300
|
+
*
|
|
301
|
+
* Takes bytes rather than a path so that a caller which must also key, hash or
|
|
302
|
+
* measure the raw bytes reads the file exactly once — `readContentWithKey` in
|
|
303
|
+
* `@vibe-agent-toolkit/resources` is that caller, and its key must stay over the
|
|
304
|
+
* raw byte preimage whatever this function decides the characters are. For the
|
|
305
|
+
* ordinary "read a file, give me its text" case use `readTextContent` from
|
|
306
|
+
* `@vibe-agent-toolkit/utils/fs`.
|
|
307
|
+
*
|
|
308
|
+
* @param bytes - The exact bytes read from disk
|
|
309
|
+
* @returns The decoded text, the encoding used, whether that was a fact, and how
|
|
310
|
+
* many characters the decode had to replace
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```typescript
|
|
314
|
+
* const bytes = await readFile(path);
|
|
315
|
+
* const { text, encoding, encodingSource, replacementCharacters } = decodeTextContent(bytes);
|
|
316
|
+
* // UTF-16BE file: encoding 'utf-16be', encodingSource 'bom', 0 replacements,
|
|
317
|
+
* // text with no BOM and no NULs
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
export function decodeTextContent(bytes) {
|
|
321
|
+
const bom = bomAt(bytes);
|
|
322
|
+
const encoding = bom?.encoding ?? 'utf-8';
|
|
323
|
+
const body = bom === null ? bytes : bytes.subarray(bom.length);
|
|
324
|
+
const encodingSource = bom === null ? 'assumed' : 'bom';
|
|
325
|
+
if (encoding === 'utf-32le' || encoding === 'utf-32be') {
|
|
326
|
+
return { ...decodeUtf32(body, encoding === 'utf-32le'), encoding, encodingSource };
|
|
327
|
+
}
|
|
328
|
+
const decoder = DECODERS.get(encoding);
|
|
329
|
+
const fatalDecoder = FATAL_DECODERS.get(encoding);
|
|
330
|
+
if (decoder === undefined || fatalDecoder === undefined) {
|
|
331
|
+
// Unreachable: BOMS, DECODERS and FATAL_DECODERS cover the same five
|
|
332
|
+
// encodings between them. Thrown rather than defaulted, because a silent fall
|
|
333
|
+
// back to UTF-8 here would reproduce the exact defect this module was written
|
|
334
|
+
// to remove.
|
|
335
|
+
throw new Error(`no decoder for encoding "${encoding}"`);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
// The clean path, and the only one nearly every file takes: one decode, no
|
|
339
|
+
// scan. A throw here is the ONLY evidence that a substitution happened —
|
|
340
|
+
// see FATAL_DECODERS.
|
|
341
|
+
return { text: fatalDecoder.decode(body), encoding, encodingSource, replacementCharacters: 0 };
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
const text = decoder.decode(body);
|
|
345
|
+
return { text, encoding, encodingSource, replacementCharacters: countReplacementCharacters(text) };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=text-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-content.js","sourceRoot":"","sources":["../src/text-content.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a file and decode it through the one content-decoding seam.
|
|
3
|
+
*
|
|
4
|
+
* The replacement for `readFile(path, 'utf-8')`, which decodes inside `fs` with
|
|
5
|
+
* no byte-order-mark handling and no way to express UTF-16BE at all. The
|
|
6
|
+
* *decision* about what the bytes say lives in `text-content.ts`, which is pure
|
|
7
|
+
* and reaches no `node:*` module; this file is only the two lines that get the
|
|
8
|
+
* bytes off disk, and it lives on the `./fs` entry with everything else here
|
|
9
|
+
* that touches the filesystem.
|
|
10
|
+
*
|
|
11
|
+
* Two functions rather than one, because the callers genuinely differ: an
|
|
12
|
+
* enumeration or parse lane is asynchronous throughout, while a manifest or
|
|
13
|
+
* config probe on a startup path is not, and handing the latter a Promise makes
|
|
14
|
+
* it worse rather than more consistent.
|
|
15
|
+
*
|
|
16
|
+
* A caller that needs the RAW bytes as well as the text — to hash them, key
|
|
17
|
+
* them, or report `stat().size` — must not use these: read the bytes once itself
|
|
18
|
+
* and call `decodeTextContent` on them, so the digest and the characters come
|
|
19
|
+
* from the same read. `readContentWithKey` in `@vibe-agent-toolkit/resources` is
|
|
20
|
+
* that caller and is shaped exactly that way.
|
|
21
|
+
*/
|
|
22
|
+
import { type DecodedText } from './text-content.js';
|
|
23
|
+
/**
|
|
24
|
+
* Read a file and decode it through {@link decodeTextContent}.
|
|
25
|
+
*
|
|
26
|
+
* @param filePath - Path to read
|
|
27
|
+
* @returns The decoded text, the encoding used, whether that was a fact, and the replacement-character count
|
|
28
|
+
* @throws Whatever `readFile` throws — callers decide whether that is fatal
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const { text, encoding } = await readTextContent(docPath);
|
|
33
|
+
* // A PowerShell-written document: encoding 'utf-16le', text with no BOM
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function readTextContent(filePath: string): Promise<DecodedText>;
|
|
37
|
+
/**
|
|
38
|
+
* {@link readTextContent}, synchronously.
|
|
39
|
+
*
|
|
40
|
+
* @param filePath - Path to read
|
|
41
|
+
* @returns The decoded text, the encoding used, whether that was a fact, and the replacement-character count
|
|
42
|
+
* @throws Whatever `readFileSync` throws
|
|
43
|
+
*/
|
|
44
|
+
export declare function readTextContentSync(filePath: string): DecodedText;
|
|
45
|
+
//# sourceMappingURL=text-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-file.d.ts","sourceRoot":"","sources":["../src/text-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAG5E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAGjE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a file and decode it through the one content-decoding seam.
|
|
3
|
+
*
|
|
4
|
+
* The replacement for `readFile(path, 'utf-8')`, which decodes inside `fs` with
|
|
5
|
+
* no byte-order-mark handling and no way to express UTF-16BE at all. The
|
|
6
|
+
* *decision* about what the bytes say lives in `text-content.ts`, which is pure
|
|
7
|
+
* and reaches no `node:*` module; this file is only the two lines that get the
|
|
8
|
+
* bytes off disk, and it lives on the `./fs` entry with everything else here
|
|
9
|
+
* that touches the filesystem.
|
|
10
|
+
*
|
|
11
|
+
* Two functions rather than one, because the callers genuinely differ: an
|
|
12
|
+
* enumeration or parse lane is asynchronous throughout, while a manifest or
|
|
13
|
+
* config probe on a startup path is not, and handing the latter a Promise makes
|
|
14
|
+
* it worse rather than more consistent.
|
|
15
|
+
*
|
|
16
|
+
* A caller that needs the RAW bytes as well as the text — to hash them, key
|
|
17
|
+
* them, or report `stat().size` — must not use these: read the bytes once itself
|
|
18
|
+
* and call `decodeTextContent` on them, so the digest and the characters come
|
|
19
|
+
* from the same read. `readContentWithKey` in `@vibe-agent-toolkit/resources` is
|
|
20
|
+
* that caller and is shaped exactly that way.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { decodeTextContent } from './text-content.js';
|
|
25
|
+
/**
|
|
26
|
+
* Read a file and decode it through {@link decodeTextContent}.
|
|
27
|
+
*
|
|
28
|
+
* @param filePath - Path to read
|
|
29
|
+
* @returns The decoded text, the encoding used, whether that was a fact, and the replacement-character count
|
|
30
|
+
* @throws Whatever `readFile` throws — callers decide whether that is fatal
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const { text, encoding } = await readTextContent(docPath);
|
|
35
|
+
* // A PowerShell-written document: encoding 'utf-16le', text with no BOM
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function readTextContent(filePath) {
|
|
39
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path, same trust level as the parsers this feeds
|
|
40
|
+
return decodeTextContent(await readFile(filePath));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* {@link readTextContent}, synchronously.
|
|
44
|
+
*
|
|
45
|
+
* @param filePath - Path to read
|
|
46
|
+
* @returns The decoded text, the encoding used, whether that was a fact, and the replacement-character count
|
|
47
|
+
* @throws Whatever `readFileSync` throws
|
|
48
|
+
*/
|
|
49
|
+
export function readTextContentSync(filePath) {
|
|
50
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path, same trust level as the parsers this feeds
|
|
51
|
+
return decodeTextContent(readFileSync(filePath));
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=text-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-file.js","sourceRoot":"","sources":["../src/text-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAExE;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,uIAAuI;IACvI,OAAO,iBAAiB,CAAC,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,uIAAuI;IACvI,OAAO,iBAAiB,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnD,CAAC"}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibe-agent-toolkit/utils/text
|
|
3
|
+
*
|
|
4
|
+
* **The one way bytes become text.** Encoding detection from a byte-order mark,
|
|
5
|
+
* BOM stripping, and a stated default when there is no BOM — see
|
|
6
|
+
* `text-content.ts` for what is a fact, what is an assumption, and the two
|
|
7
|
+
* limitations that are deliberately not guessed around.
|
|
8
|
+
*
|
|
9
|
+
* Pure: this entry reaches no `node:*` builtin and no third-party package, so
|
|
10
|
+
* bytes from a git blob, an HTTP response or a zip entry decode through exactly
|
|
11
|
+
* the same function as bytes from disk. For the read-a-file case, import
|
|
12
|
+
* `readTextContent` from `@vibe-agent-toolkit/utils/fs`, which is this plus a
|
|
13
|
+
* `readFile`.
|
|
14
|
+
*/
|
|
15
|
+
export { decodeTextContent, type DecodedText, type EncodingSource, type TextEncoding, type TextProvenance, } from './text-content.js';
|
|
16
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,mBAAmB,CAAC"}
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vibe-agent-toolkit/utils/text
|
|
3
|
+
*
|
|
4
|
+
* **The one way bytes become text.** Encoding detection from a byte-order mark,
|
|
5
|
+
* BOM stripping, and a stated default when there is no BOM — see
|
|
6
|
+
* `text-content.ts` for what is a fact, what is an assumption, and the two
|
|
7
|
+
* limitations that are deliberately not guessed around.
|
|
8
|
+
*
|
|
9
|
+
* Pure: this entry reaches no `node:*` builtin and no third-party package, so
|
|
10
|
+
* bytes from a git blob, an HTTP response or a zip entry decode through exactly
|
|
11
|
+
* the same function as bytes from disk. For the read-a-file case, import
|
|
12
|
+
* `readTextContent` from `@vibe-agent-toolkit/utils/fs`, which is this plus a
|
|
13
|
+
* `readFile`.
|
|
14
|
+
*/
|
|
15
|
+
export { decodeTextContent, } from './text-content.js';
|
|
16
|
+
//# sourceMappingURL=text.js.map
|
package/dist/text.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,iBAAiB,GAKlB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The on-disk plumbing every VAT timing seam shares.
|
|
3
|
+
*
|
|
4
|
+
* Two seams write per-process JSON dumps to an operator-named directory:
|
|
5
|
+
* `@vibe-agent-toolkit/resources`' `parse-timing.ts` (which pass inside a parser
|
|
6
|
+
* owns the time) and this package's `crawl-timing.ts` (which contributor or
|
|
7
|
+
* crawler owns the time). They sit in different packages because `crawl-timing`
|
|
8
|
+
* has to bracket `GitTracker`, which is here — so this module lives at the lower
|
|
9
|
+
* of the two and is exported for the higher one. What they
|
|
10
|
+
* MEASURE could not be less alike — one axis is a closed enum of parser passes,
|
|
11
|
+
* the other is an open set of contributor ids — but every property that makes
|
|
12
|
+
* the *file* trustworthy is identical between them, and the lab's
|
|
13
|
+
* `harness/dumps.ts` already says so from the reading end:
|
|
14
|
+
*
|
|
15
|
+
* 1. A directory that cannot be created is reported the moment the seam turns
|
|
16
|
+
* on, while there is still a run to abandon — never at exit, where a failure
|
|
17
|
+
* costs the whole measurement.
|
|
18
|
+
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
|
+
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
|
+
* they must never touch stdout, which carries vat's report.
|
|
21
|
+
* 3. A pid can file more than one dump, and the name is CLAIMED rather than
|
|
22
|
+
* checked. `vat validate` spawns the vat binary once per phase and pids are
|
|
23
|
+
* reused, so `<stem>-<pid>.json` genuinely collides; worse, worker threads
|
|
24
|
+
* SHARE their parent's pid, so a thread pool's whole cohort competes for one
|
|
25
|
+
* `<stem>-<pid>` sequence at the same instant. The name gains a counter
|
|
26
|
+
* rather than overwriting, and the counter is settled by
|
|
27
|
+
* {@link EXCLUSIVE_CREATE} so the OS — not a check-then-write gap — decides
|
|
28
|
+
* who won it.
|
|
29
|
+
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
30
|
+
* lifetime figure for the process and never a duration of the measured work;
|
|
31
|
+
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
32
|
+
* brackets above it were competing with a loaded machine.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ **A reader must never SUM these across dumps.** Point 3 means one
|
|
35
|
+
* command routinely files several, and a vat command's phase processes
|
|
36
|
+
* overlap in time — so summing their lifetimes counts the same wall clock
|
|
37
|
+
* more than once and produces a "total" longer than the command took. The
|
|
38
|
+
* figure is per process, and the only honest aggregate over several is the
|
|
39
|
+
* per-process ratio read one dump at a time. The `crawl` facet keeps one
|
|
40
|
+
* record per dump and publishes no total for exactly this reason; `parse`
|
|
41
|
+
* still sums, which is review finding F2 (2026-08-14) and is annotated at
|
|
42
|
+
* `facets/parse/dump.ts`. This list is where both seams learn what makes a
|
|
43
|
+
* dump trustworthy, so the hazard belongs here rather than only beside the
|
|
44
|
+
* consumer that already fixed it.
|
|
45
|
+
*
|
|
46
|
+
* Writing that twice would give two seams two chances to diverge on the one
|
|
47
|
+
* thing a reader has to be able to trust identically. What each seam keeps for
|
|
48
|
+
* itself is its accumulator shape, its dump body and the noun it is called by.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Process-level wall and CPU time, read ONCE when a dump is written.
|
|
52
|
+
*
|
|
53
|
+
* All three are lifetime figures for the whole process, not for the measured
|
|
54
|
+
* work: the point of carrying them is the *ratio*. CPU well below wall means the
|
|
55
|
+
* process was waiting rather than computing, and every wall-timed bracket in the
|
|
56
|
+
* dump carries that waiting inside it. CPU above wall is normal and not an
|
|
57
|
+
* error — `process.cpuUsage()` sums every thread, including libuv's pool.
|
|
58
|
+
*/
|
|
59
|
+
export interface TimingProcess {
|
|
60
|
+
/** Wall clock since this process started. */
|
|
61
|
+
wallMs: number;
|
|
62
|
+
/** User CPU consumed by the process, across all its threads. */
|
|
63
|
+
cpuUserMs: number;
|
|
64
|
+
/** System CPU consumed by the process, across all its threads. */
|
|
65
|
+
cpuSystemMs: number;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Ceiling on the pid-collision search. A directory holding this many dumps for
|
|
69
|
+
* one pid is a runaway, not a collision, and giving up on a reported line is a
|
|
70
|
+
* better outcome than spinning.
|
|
71
|
+
*
|
|
72
|
+
* Exhausting it does NOT fall back to overwriting the last slot. That was the
|
|
73
|
+
* old behaviour, and it destroyed a dump for the same reason the check-then-
|
|
74
|
+
* write gap did — silently, and precisely when the directory was busiest.
|
|
75
|
+
*/
|
|
76
|
+
export declare const MAX_DUMP_COLLISIONS = 1000;
|
|
77
|
+
/**
|
|
78
|
+
* Reduce a raw env value to a directory or `null`.
|
|
79
|
+
*
|
|
80
|
+
* An empty-string value counts as absent: `VAT_PARSE_TIMING=` in a shell profile
|
|
81
|
+
* is a variable somebody meant to unset, not a request to dump into the process's
|
|
82
|
+
* working directory.
|
|
83
|
+
*
|
|
84
|
+
* @param raw - The env var's value, if set
|
|
85
|
+
* @returns The dump directory, or `null` when the seam is off
|
|
86
|
+
*/
|
|
87
|
+
export declare function normalizeTimingDirectory(raw: string | undefined): string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Report a dump problem on stderr.
|
|
90
|
+
*
|
|
91
|
+
* Never throws and never touches stdout: vat's stdout carries a YAML report, and
|
|
92
|
+
* an exit handler that threw would change the process's exit behaviour.
|
|
93
|
+
*
|
|
94
|
+
* @param noun - What the seam is called, so a reader knows which instrument failed
|
|
95
|
+
* @param target - Path the failure concerns
|
|
96
|
+
* @param error - Whatever was caught
|
|
97
|
+
*/
|
|
98
|
+
export declare function reportTimingDumpFailure(noun: string, target: string, error: unknown): void;
|
|
99
|
+
/**
|
|
100
|
+
* Create the dump directory, swallowing failure.
|
|
101
|
+
*
|
|
102
|
+
* Done once when the seam turns on rather than at exit, so a bad path is
|
|
103
|
+
* reported while there is still a run to abandon — and so the exit handler does
|
|
104
|
+
* the minimum possible work.
|
|
105
|
+
*
|
|
106
|
+
* @param noun - What the seam is called, for the failure line
|
|
107
|
+
* @param directory - Directory dumps will be written to
|
|
108
|
+
*/
|
|
109
|
+
export declare function ensureTimingDirectory(noun: string, directory: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* Read this process's lifetime wall and CPU time.
|
|
112
|
+
*
|
|
113
|
+
* Called ONCE per dump — two syscalls for a whole run, which is why the process
|
|
114
|
+
* level can afford a CPU reading that a per-bracket level cannot. Deliberately
|
|
115
|
+
* not an accumulator and deliberately not reset: it describes the process, not
|
|
116
|
+
* the measurement window.
|
|
117
|
+
*
|
|
118
|
+
* @returns Wall clock and CPU since process start, in milliseconds
|
|
119
|
+
*/
|
|
120
|
+
export declare function readTimingProcess(): TimingProcess;
|
|
121
|
+
/**
|
|
122
|
+
* Write one seam's dump, if the seam is on.
|
|
123
|
+
*
|
|
124
|
+
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
125
|
+
* snapshot accumulators nobody will read — and a `build` that throws is reported
|
|
126
|
+
* like any other failure, because this runs from an `exit` listener where a
|
|
127
|
+
* throw would change the process's exit behaviour.
|
|
128
|
+
*
|
|
129
|
+
* @param noun - What the seam is called, for any failure line
|
|
130
|
+
* @param directory - Where to write, or `null` when the seam is off
|
|
131
|
+
* @param basename - Basename stem for the file
|
|
132
|
+
* @param build - Produces the dump body
|
|
133
|
+
* @returns The path written, or `null` when the seam is off or nothing was written
|
|
134
|
+
*/
|
|
135
|
+
export declare function writeTimingDump(noun: string, directory: string | null, basename: string, build: () => unknown): string | null;
|
|
136
|
+
//# sourceMappingURL=timing-dump.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing-dump.d.ts","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAOH;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AA6BxC;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAM3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,CAOjD;AA0DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,OAAO,GACnB,MAAM,GAAG,IAAI,CAoBf"}
|