@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.
Files changed (150) hide show
  1. package/README.md +26 -15
  2. package/dist/compare-code-units.d.ts +28 -0
  3. package/dist/compare-code-units.d.ts.map +1 -0
  4. package/dist/compare-code-units.js +33 -0
  5. package/dist/compare-code-units.js.map +1 -0
  6. package/dist/crawl-timing.d.ts +657 -0
  7. package/dist/crawl-timing.d.ts.map +1 -0
  8. package/dist/crawl-timing.js +765 -0
  9. package/dist/crawl-timing.js.map +1 -0
  10. package/dist/crawl.d.ts +2 -4
  11. package/dist/crawl.d.ts.map +1 -1
  12. package/dist/crawl.js +2 -4
  13. package/dist/crawl.js.map +1 -1
  14. package/dist/file-crawler.d.ts +22 -0
  15. package/dist/file-crawler.d.ts.map +1 -1
  16. package/dist/file-crawler.js +31 -7
  17. package/dist/file-crawler.js.map +1 -1
  18. package/dist/fs-utils.d.ts +17 -0
  19. package/dist/fs-utils.d.ts.map +1 -1
  20. package/dist/fs-utils.js +69 -0
  21. package/dist/fs-utils.js.map +1 -1
  22. package/dist/fs.d.ts +3 -1
  23. package/dist/fs.d.ts.map +1 -1
  24. package/dist/fs.js +5 -1
  25. package/dist/fs.js.map +1 -1
  26. package/dist/git-run.d.ts +167 -0
  27. package/dist/git-run.d.ts.map +1 -0
  28. package/dist/git-run.js +154 -0
  29. package/dist/git-run.js.map +1 -0
  30. package/dist/git-snapshot.d.ts +181 -0
  31. package/dist/git-snapshot.d.ts.map +1 -0
  32. package/dist/git-snapshot.js +232 -0
  33. package/dist/git-snapshot.js.map +1 -0
  34. package/dist/git-tracker.d.ts +94 -1
  35. package/dist/git-tracker.d.ts.map +1 -1
  36. package/dist/git-tracker.js +135 -3
  37. package/dist/git-tracker.js.map +1 -1
  38. package/dist/git-utils.d.ts +38 -0
  39. package/dist/git-utils.d.ts.map +1 -1
  40. package/dist/git-utils.js +103 -80
  41. package/dist/git-utils.js.map +1 -1
  42. package/dist/git.d.ts +12 -4
  43. package/dist/git.d.ts.map +1 -1
  44. package/dist/git.js +12 -4
  45. package/dist/git.js.map +1 -1
  46. package/dist/gitignore-checker.d.ts.map +1 -1
  47. package/dist/gitignore-checker.js +6 -3
  48. package/dist/gitignore-checker.js.map +1 -1
  49. package/dist/index.d.ts +24 -18
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +49 -30
  52. package/dist/index.js.map +1 -1
  53. package/dist/path-core.d.ts +1 -1
  54. package/dist/path-core.js +1 -1
  55. package/dist/project-utils.d.ts.map +1 -1
  56. package/dist/project-utils.js +6 -3
  57. package/dist/project-utils.js.map +1 -1
  58. package/dist/project.d.ts +7 -5
  59. package/dist/project.d.ts.map +1 -1
  60. package/dist/project.js +7 -5
  61. package/dist/project.js.map +1 -1
  62. package/dist/safe-exec.d.ts +38 -10
  63. package/dist/safe-exec.d.ts.map +1 -1
  64. package/dist/safe-exec.js +88 -19
  65. package/dist/safe-exec.js.map +1 -1
  66. package/dist/skill-test/index.d.ts +1 -1
  67. package/dist/skill-test/index.d.ts.map +1 -1
  68. package/dist/skill-test/index.js +1 -1
  69. package/dist/skill-test/index.js.map +1 -1
  70. package/dist/skill-test/spawn-claude.d.ts +28 -0
  71. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  72. package/dist/skill-test/spawn-claude.js +51 -2
  73. package/dist/skill-test/spawn-claude.js.map +1 -1
  74. package/dist/skill-test/transcript.d.ts +41 -2
  75. package/dist/skill-test/transcript.d.ts.map +1 -1
  76. package/dist/skill-test/transcript.js +47 -3
  77. package/dist/skill-test/transcript.js.map +1 -1
  78. package/dist/test-helpers.d.ts +185 -13
  79. package/dist/test-helpers.d.ts.map +1 -1
  80. package/dist/test-helpers.js +274 -27
  81. package/dist/test-helpers.js.map +1 -1
  82. package/dist/testing.d.ts +73 -3
  83. package/dist/testing.d.ts.map +1 -1
  84. package/dist/testing.js +93 -3
  85. package/dist/testing.js.map +1 -1
  86. package/dist/text-content.d.ts +197 -0
  87. package/dist/text-content.d.ts.map +1 -0
  88. package/dist/text-content.js +348 -0
  89. package/dist/text-content.js.map +1 -0
  90. package/dist/text-file.d.ts +45 -0
  91. package/dist/text-file.d.ts.map +1 -0
  92. package/dist/text-file.js +53 -0
  93. package/dist/text-file.js.map +1 -0
  94. package/dist/text.d.ts +16 -0
  95. package/dist/text.d.ts.map +1 -0
  96. package/dist/text.js +16 -0
  97. package/dist/text.js.map +1 -0
  98. package/dist/timing-dump.d.ts +136 -0
  99. package/dist/timing-dump.d.ts.map +1 -0
  100. package/dist/timing-dump.js +230 -0
  101. package/dist/timing-dump.js.map +1 -0
  102. package/eslint/README.md +105 -3
  103. package/eslint/index.cjs +50 -0
  104. package/eslint/index.d.cts +14 -4
  105. package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
  106. package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
  107. package/eslint/rules/no-raw-text-decode.cjs +296 -0
  108. package/eslint/rules/no-self-package-import.cjs +157 -0
  109. package/package.json +13 -8
  110. package/dist/link-auth/build-headers.d.ts +0 -34
  111. package/dist/link-auth/build-headers.d.ts.map +0 -1
  112. package/dist/link-auth/build-headers.js +0 -58
  113. package/dist/link-auth/build-headers.js.map +0 -1
  114. package/dist/link-auth/expand-macro.d.ts +0 -38
  115. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  116. package/dist/link-auth/expand-macro.js +0 -133
  117. package/dist/link-auth/expand-macro.js.map +0 -1
  118. package/dist/link-auth/macros.yaml +0 -50
  119. package/dist/link-auth/resolve-token.d.ts +0 -83
  120. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  121. package/dist/link-auth/resolve-token.js +0 -101
  122. package/dist/link-auth/resolve-token.js.map +0 -1
  123. package/dist/link-auth/resolve.d.ts +0 -102
  124. package/dist/link-auth/resolve.d.ts.map +0 -1
  125. package/dist/link-auth/resolve.js +0 -66
  126. package/dist/link-auth/resolve.js.map +0 -1
  127. package/dist/link-auth/rewrite.d.ts +0 -52
  128. package/dist/link-auth/rewrite.d.ts.map +0 -1
  129. package/dist/link-auth/rewrite.js +0 -102
  130. package/dist/link-auth/rewrite.js.map +0 -1
  131. package/dist/link-auth/select-provider.d.ts +0 -30
  132. package/dist/link-auth/select-provider.d.ts.map +0 -1
  133. package/dist/link-auth/select-provider.js +0 -55
  134. package/dist/link-auth/select-provider.js.map +0 -1
  135. package/dist/link-auth/template.d.ts +0 -40
  136. package/dist/link-auth/template.d.ts.map +0 -1
  137. package/dist/link-auth/template.js +0 -89
  138. package/dist/link-auth/template.js.map +0 -1
  139. package/dist/link-auth/transforms.d.ts +0 -46
  140. package/dist/link-auth/transforms.d.ts.map +0 -1
  141. package/dist/link-auth/transforms.js +0 -52
  142. package/dist/link-auth/transforms.js.map +0 -1
  143. package/dist/template-entry.d.ts +0 -10
  144. package/dist/template-entry.d.ts.map +0 -1
  145. package/dist/template-entry.js +0 -10
  146. package/dist/template-entry.js.map +0 -1
  147. package/dist/template.d.ts +0 -7
  148. package/dist/template.d.ts.map +0 -1
  149. package/dist/template.js +0 -18
  150. package/dist/template.js.map +0 -1
package/README.md CHANGED
@@ -16,38 +16,47 @@ bun add @vibe-agent-toolkit/utils
16
16
 
17
17
  Every area has its own subpath. Import the one you need.
18
18
 
19
- The package sets `"sideEffects": false`, so a modern bundler will tree-shake unused code out of the `.` barrel — importing `safePath` from `.` and from `./path` produce near-identical bundles. **Subpaths are not primarily a size optimization.** What they control is what your build has to *resolve* and what your module graph *reaches*: the `.` barrel reaches `yaml`, `handlebars`, and `node:fs` no matter what you destructure from it, so it cannot be bundled for a browser target and requires every dependency to be installed. A narrow entry reaches only what it needs.
19
+ The package sets `"sideEffects": false`, so a modern bundler will tree-shake unused code out of the `.` barrel — importing `safePath` from `.` and from `./path` produce near-identical bundles. **Subpaths are not primarily a size optimization.** What they control is what your build has to *resolve* and what your module graph *reaches*. A narrow entry reaches only what it needs.
20
+
21
+ **The `.` barrel reaches no third-party package at all.** Every domain that carries a dependency — directly or transitively — is a subpath: `./crawl`, `./git`, `./process`, `./skill-test`, `./yaml`. That is asserted by equality in `test/subpath-purity.test.ts`, so the barrel cannot silently regain one. It still reaches `node:fs` and friends, so it is Node-only; what it does not do is make a consumer of `safePath` install a template engine.
20
22
 
21
23
  The last two columns are the ones that matter when choosing. **"Resolves with zero deps installed?"** is the sharper of the two: it separates an entry that is merely *heavy* from one that is *unbuildable* in an environment where the package's third-party dependencies are absent or unresolvable.
22
24
 
23
25
  | Subpath | Contents | Node builtins reached | Third-party | Resolves with zero deps installed? |
24
26
  |---|---|---|---|---|
25
27
  | `./path` | `safePath`, `toForwardSlash`, `toNfc`, `isAbsolutePath`, `isAbsoluteAnyPlatform`, `hasParentTraversalSegment`, `toAbsolutePath`, `getRelativePath`, `issueLocation` | `path` only | — | **yes** |
28
+ | `./text` | `decodeTextContent` — the one bytes-to-text seam: BOM-announced UTF-8/UTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE, BOM stripped, UTF-8 assumed otherwise; reports the encoding, whether it was a BOM fact or an assumption, and how many U+FFFD the decode substituted | **none** | — | **yes** |
26
29
  | `./zod` | `ZodTypeNames`, `getZodTypeName`, `isZodType`, `unwrapZodType`, `isZodOptional`, `isZodNullable` | **none** | — | **yes** |
27
30
  | `./glob` | `isGlob`, static base extraction, magic remainder | `path` only | — | **yes** |
28
- | `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `copyDirectory`, `fillSiblingNames`, `classifyFilenameCaseFrom`, `FsLookupCache` | `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
29
- | `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite` | `crypto`, `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
31
+ | `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `copyDirectory`, `fillSiblingNames`, `classifyFilenameCaseFrom`, `FsLookupCache`, `readTextContent`, `readTextContentSync` | `fs`, `fs/promises`, `os`, `path`, `url`, `util` | — | **yes** |
32
+ | `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite`, `removeScratchDir`, `symlinkCapability`, `createSymlink`, `createSymlinkAsync` | `crypto`, `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
30
33
  | `./asset` | `resolveAssetReference` — paths and npm bare specifiers | `fs`, `module`, `os`, `path`, `url` | — | **yes** |
31
34
  | `./yaml` | `updateYamlIn`, `verifyConfinedYamlEdit` — byte-surgical YAML edits | **none** | `yaml` | no — needs `yaml` |
32
- | `./template` | `renderTemplate` cached Handlebars | **none** | `handlebars` | no — needs `handlebars` |
33
- | `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking` | `child_process`, `path` | `which` | no — needs `which` |
34
- | `./git` | `gitFindRoot`, `gitLsFiles`, `isGitIgnored`, `loadGitignoreRules`, `GitTracker`, `parseGitUrl`, `isGitUrl`, `nonInteractiveGitOverrides` | `child_process`, `fs`, `os`, `path`, `url` | `ignore`, `which` | no — needs `which`, `ignore` |
35
- | `./crawl` | `crawlDirectory`, `crawlDirectorySync`, `NEVER_CRAWL_GLOBS`, `BUILD_OUTPUT_GLOBS` | `child_process`, `fs`, `os`, `path`, `url` | `picomatch`, `which` | no — needs `picomatch`, `which` |
36
- | `./project` | `findProjectRoot`, `findConfigFile`, `findNodeWorkspaceRoot`, `resetProjectRootCaches` | `fs`, `path` | — | **yes** |
37
- | `./eslint` | the 21 ESLint rules that enforce everything above — see [ESLint rules](#eslint-rules--vibe-agent-toolkitutilseslint) | **none** | — | **yes** |
38
- | `.` | every runtime entry above (not `./eslint`) | all of the above, plus `stream` | `handlebars`, `ignore`, `picomatch`, `which`, `yaml` | no needs all of them |
35
+ | `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking` | `child_process`, `path` | `@vibe-validate/git`, `which` | no — needs both |
36
+ | `./git` | `runGit`, `runGitOrThrow`, `gitFindRoot`, `gitLsFiles`, `gitLsOthers`, `isGitIgnored`, `loadGitignoreRules`, `GitTracker`, `gitTreeSnapshot`, `peekGitTreeSnapshot`, `withGitSnapshotCache`, `parseGitUrl`, `isGitUrl`, `nonInteractiveGitOverrides` | `async_hooks`, `fs`, `fs/promises`, `os`, `path`, `url` | `@vibe-validate/git`, `ignore` | no — needs both |
37
+ | `./crawl` | `crawlDirectory`, `crawlDirectorySync`, `crawlPathFilter`, `NEVER_CRAWL_GLOBS`, `BUILD_OUTPUT_GLOBS` | `fs`, `os`, `path`, `url` | `@vibe-validate/git`, `picomatch` | no — needs both |
38
+ | `./skill-test` | `spawnHeadlessClaude`, `assembleClaudeArgs`, `killAllActiveClaudeChildren`, `resolveAuth`, `probeAuthStatus`, `AuthPreflightError`, `applyDeclaredEnv`, `buildForwardedEnv`, `formatForwardedEnvLine`, `isProtectedName`, `protectedEnvNames`, `parseStreamJsonTranscript`, `detectInvocationFromTranscript` | `child_process`, `path`, `stream` | `@vibe-validate/git`, `which` | no — needs both |
39
+ | `./project` | `findProjectRoot`, `findConfigFile`, `findNodeWorkspaceRoot`, `resetProjectRootCaches` | `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
40
+ | `./eslint` | the 22 ESLint rules that enforce everything above — see [ESLint rules](#eslint-rules--vibe-agent-toolkitutilseslint) | **none** | — | **yes** |
41
+ | `.` | the dependency-free entries above path, text, fs, asset, project, testing, zod, glob, plus the crawl-timing seam | `async_hooks`, `crypto`, `fs`, `fs/promises`, `module`, `os`, `path`, `url`, `util` | — | **yes** |
39
42
  | `./package.json` | the manifest itself, for version reporting and resolution assertions | — | — | **yes** |
40
43
 
41
44
  Note `./zod` reaches nothing at all: it detects Zod types by duck-typing `_def.typeName` rather than importing Zod, which is exactly why it works across Zod v3 and v4.
42
45
 
43
- `./crawl` is the only *subpath* that reaches `picomatch` (the `.` barrel also reaches it, via linkAuth's host-pattern matching), and it is deliberately *not* folded into `./glob` — `./glob` is guarded as portable (`node:path`, no third-party), and directory crawling would break both halves of that guarantee.
46
+ `./crawl` is the only entry that reaches `picomatch`, and it is deliberately *not* folded into `./glob` — `./glob` is guarded as portable (`node:path`, no third-party), and directory crawling would break both halves of that guarantee.
47
+
48
+ `./skill-test` declares no dependency of its own; it is a separate entry because of what it *reaches*. Spawning a headless agent goes through `./process`, which costs `which` and — because `safeExecSync` refuses the `git` binary and delegates — `@vibe-validate/git`. Reachability is the criterion, not the import a module happens to write.
49
+
50
+ `./git` is the only published route to `runGit`. `safeExecSync` and `safeExecResult` on `./process` refuse `git` outright and point here, so a caller that wants git gets the scrubbed environment by construction rather than by remembering to ask.
44
51
 
45
52
  ```typescript
46
53
  // Reaches node:path and nothing else
47
54
  import { safePath, toForwardSlash } from '@vibe-agent-toolkit/utils/path';
55
+
56
+ // Reaches `which` and @vibe-validate/git — a real install cost, so it is its own entry
48
57
  import { safeExecSync, spawnHardened } from '@vibe-agent-toolkit/utils/process';
49
58
 
50
- // Reaches yaml, handlebars, and node:fs regardless of what you destructure
59
+ // Reaches node builtins only no third-party package, whatever you destructure
51
60
  import { safePath } from '@vibe-agent-toolkit/utils';
52
61
  ```
53
62
 
@@ -61,8 +70,8 @@ This package targets **Node >= 22** and is not published for browsers. Most entr
61
70
 
62
71
  A guard test in `test/subpath-purity.test.ts` walks each entry's transitive source graph and enforces **both** of the table's last two columns:
63
72
 
64
- - **Third-party reach** — the "Resolves with zero deps installed?" column. Every entry's expected third-party set is asserted exactly, so every **yes** row above is a tested claim rather than a documented intention, and adding a dependency to any entry is a deliberate, reviewed edit.
65
- - **Builtin reach** — five entries are held to a stricter contract still: `./zod`, `./yaml`, `./template` reach **no Node builtin at all**, and `./path`, `./glob` reach **`node:path` and nothing else** — the one builtin every bundler shims.
73
+ - **Third-party reach** — the "Resolves with zero deps installed?" column. Every entry's expected third-party set is asserted exactly, so every **yes** row above is a tested claim rather than a documented intention, and adding a dependency to any entry is a deliberate, reviewed edit. The `.` row asserts `[]`, which is the load-bearing one: a dependency arriving on the barrel — through any module it exports, at any depth — reddens that assertion, and the fix is a new subpath rather than a longer expected list.
74
+ - **Builtin reach** — five entries are held to a stricter contract still: `./zod`, `./yaml`, `./text` reach **no Node builtin at all**, and `./path`, `./glob` reach **`node:path` and nothing else** — the one builtin every bundler shims.
66
75
 
67
76
  That is an enforced invariant, not a browser-support commitment: there are no browser export conditions and no browser test lane. The guard exists so the property can't regress silently — it fails loudly if it cannot resolve a module, so it can't pass vacuously; `test/fixtures/dangling-import/` exercises that failure so the guarantee is demonstrated, not just claimed. If you add a new entry, add it to that test or nothing protects it.
68
77
 
@@ -183,6 +192,8 @@ These return **OS-native** separators, because they resolve real filesystem iden
183
192
 
184
193
  - `setupAsyncTempDirSuite()` / `setupSyncTempDirSuite()` - per-suite temp directories with cleanup
185
194
  - `getTestOutputDir()` / `getTestOutputBase()` - isolated test output paths
195
+ - `symlinkCapability()` - probes once (memoized per process) whether this host can create symlinks (Windows needs Developer Mode or `SeCreateSymbolicLinkPrivilege`), returning a `SymlinkCapability` token or `null`
196
+ - `createSymlink()` / `createSymlinkAsync()` - the sanctioned way to create a symlink in test code; both require a `SymlinkCapability` from `symlinkCapability()`, so a test cannot reach the raw syscall without first proving the host supports it (or explicitly skipping via vitest's `skip()`)
186
197
 
187
198
  ### Project roots — `@vibe-agent-toolkit/utils` (barrel only)
188
199
 
@@ -194,7 +205,7 @@ These are CLI-boundary functions: inner libraries should take a root as a parame
194
205
 
195
206
  **These four are VAT-shaped — read this before reaching for them.** `findProjectRoot()` looks for `vibe-agent-toolkit.config.yaml` and then `.git/`; if your repo's notion of "root" is a `pnpm-workspace.yaml`, a `turbo.json`, or a lockfile, that ladder is not your ladder — and for a *published* package, keying anything on `.git/` is a bug, since it will not be there at install time. `findNodeWorkspaceRoot()` is narrower still: it needs a `package.json` carrying a `"workspaces"` key, which pnpm and Bun workspaces do not have. `findConfigFile()` hardcodes VAT's config filename. If you want a git root, take `gitFindRoot()` from [`./git`](#git--vibe-agent-toolkitutilsgit); if you want your own marker, a six-line walk-up is more honest than a helper whose ladder you have to work around.
196
207
 
197
- They are nonetheless on their own [`./project`](#import-narrowly) entry rather than the barrel alone. The entry was briefly withdrawn on the grounds that the functions fit few repos — which is true, and is what the paragraph above says — but that answered the wrong question. What decides whether an *entry* exists is how heavy the only remaining door is: barrel-only, these four cost `handlebars`, `yaml`, `picomatch`, `ignore` and `which` to reach, while their own code imports nothing but `node:fs` and `node:path`. Publishing the entry is not a claim that the ladder fits you — only that finding out shouldn't cost five dependencies.
208
+ They are nonetheless on their own [`./project`](#import-narrowly) entry rather than the barrel alone. The entry was briefly withdrawn on the grounds that the functions fit few repos — which is true, and is what the paragraph above says — but that answered the wrong question. What decides whether an *entry* exists is how heavy the only remaining door is, and barrel-only these four once cost five third-party packages to reach while their own code imports nothing but `node:fs` and `node:path`. Publishing the entry is not a claim that the ladder fits you — only that finding out shouldn't cost a dependency graph. The barrel is dependency-free now, which is that same rule applied everywhere rather than a reason to fold `./project` back in.
198
209
 
199
210
  ### Directory crawling — `@vibe-agent-toolkit/utils/crawl`
200
211
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Host-independent string ordering, for sorts whose output is hashed or compared
3
+ * across machines.
4
+ *
5
+ * ## Why this is not `localeCompare`
6
+ *
7
+ * `sonarjs/no-alphabetical-sort` fires on a bare `strings.sort()` and its suggested fix is a
8
+ * `localeCompare` comparator. **That fix is wrong for anything whose order feeds a digest, a
9
+ * content key, a serialized document or a golden file.** Collation is locale-dependent, so two
10
+ * machines running the same code over the same corpus can order the same strings differently —
11
+ * destroying exactly the cross-run comparability those artifacts exist to provide. The rule only
12
+ * requires *a* comparator; this is the one to give it.
13
+ *
14
+ * Code-unit order is total, stable and machine-independent. It is not alphabetical in any human
15
+ * sense (uppercase sorts before lowercase, `10` before `2`), which is fine — callers using it are
16
+ * ordering bytes for a machine, not a list for a reader. Sort numbers numerically instead.
17
+ *
18
+ * This lives in `utils` because it had independently grown three identical private copies (in
19
+ * `resources`' projection digest and blob population, and in `claude-marketplace`' plugin extent)
20
+ * and a fourth was nearly written. Three copies of a "never use `localeCompare`" rule is three
21
+ * chances for someone to helpfully replace one of them.
22
+ *
23
+ * @param left - First string
24
+ * @param right - Second string
25
+ * @returns Negative, zero or positive per the `Array.prototype.sort` contract
26
+ */
27
+ export declare function compareCodeUnits(left: string, right: string): number;
28
+ //# sourceMappingURL=compare-code-units.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-code-units.d.ts","sourceRoot":"","sources":["../src/compare-code-units.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpE"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Host-independent string ordering, for sorts whose output is hashed or compared
3
+ * across machines.
4
+ *
5
+ * ## Why this is not `localeCompare`
6
+ *
7
+ * `sonarjs/no-alphabetical-sort` fires on a bare `strings.sort()` and its suggested fix is a
8
+ * `localeCompare` comparator. **That fix is wrong for anything whose order feeds a digest, a
9
+ * content key, a serialized document or a golden file.** Collation is locale-dependent, so two
10
+ * machines running the same code over the same corpus can order the same strings differently —
11
+ * destroying exactly the cross-run comparability those artifacts exist to provide. The rule only
12
+ * requires *a* comparator; this is the one to give it.
13
+ *
14
+ * Code-unit order is total, stable and machine-independent. It is not alphabetical in any human
15
+ * sense (uppercase sorts before lowercase, `10` before `2`), which is fine — callers using it are
16
+ * ordering bytes for a machine, not a list for a reader. Sort numbers numerically instead.
17
+ *
18
+ * This lives in `utils` because it had independently grown three identical private copies (in
19
+ * `resources`' projection digest and blob population, and in `claude-marketplace`' plugin extent)
20
+ * and a fourth was nearly written. Three copies of a "never use `localeCompare`" rule is three
21
+ * chances for someone to helpfully replace one of them.
22
+ *
23
+ * @param left - First string
24
+ * @param right - Second string
25
+ * @returns Negative, zero or positive per the `Array.prototype.sort` contract
26
+ */
27
+ export function compareCodeUnits(left, right) {
28
+ if (left === right) {
29
+ return 0;
30
+ }
31
+ return left < right ? -1 : 1;
32
+ }
33
+ //# sourceMappingURL=compare-code-units.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-code-units.js","sourceRoot":"","sources":["../src/compare-code-units.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC"}