@orkestrel/scaffold 0.0.108 → 0.0.109

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 (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -0,0 +1,425 @@
1
+ // Count one lint rule's hits before it is enabled. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-harden/scripts/measure.ts --rule ID [--options JSON] PATH...
3
+ // The script reads the workspace's `.oxlintrc.json` for its `plugins` and `jsPlugins` (a relative
4
+ // plugin specifier becomes absolute, so the policy plugin loads from the checkout), writes a
5
+ // variant config that enables only `ID` as an error under `tmp/probes/lint/<ID>/.oxlintrc.json`,
6
+ // runs the installed oxlint with `--format json` over the named paths from the checkout root, and
7
+ // prints `rule=ID hits=N files=N` then one `N<TAB>path` line per file with a hit, most hits first.
8
+ // `--options` is a JSON value given to the rule: an array spreads into the rule's arguments after
9
+ // `error`, any other value is the one argument. The workspace's `ignorePatterns` and its override
10
+ // blocks are not carried over, so a rule measured here reads exactly the named paths with no
11
+ // per-file override. A rule that reads `context.cwd` must run from the checkout root, so run the
12
+ // script there. Enable only the rule under measure to count a rule the workspace might drop. A hit
13
+ // is a diagnostic whose `code` names the rule (`<plugin>(<name>)`, with `eslint` for an unprefixed
14
+ // id and the plugin's own scope for a built-in plugin such as `typescript-eslint`); any other
15
+ // diagnostic, a parse error included, is not a hit and refuses the measurement, because a file the
16
+ // linter could not read has no count. A linter that exits other than 0 or 1, or exits 1 with no
17
+ // diagnostic, refuses it too. The script refuses a flag it does not read, the `--flag=value` form
18
+ // included, and names an unknown flag before a flag given with no value.
19
+ // Exit 0 when the rule has no hit, 3 when it has, 2 when the linter cannot run or reports another
20
+ // diagnostic, 64 on usage.
21
+ import { spawnSync } from 'node:child_process'
22
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
23
+ import { isAbsolute, join, resolve } from 'node:path'
24
+
25
+ /** Lists the flags that take a value. */
26
+ export const VALUE_FLAGS: readonly string[] = ['--rule', '--options']
27
+
28
+ /** Lists every flag the script reads, value flags and switches alike. */
29
+ export const FLAGS: readonly string[] = VALUE_FLAGS
30
+
31
+ /** Holds the usage line printed when a call names no rule or no path. */
32
+ export const USAGE = 'usage: measure.ts --rule ID [--options JSON] PATH...'
33
+
34
+ /** Holds the directory, under the working directory, that keeps the variant configs. */
35
+ export const PROBE_ROOT = 'tmp/probes/lint'
36
+
37
+ /** Holds the linter entry, under the working directory, that the script runs. */
38
+ export const LINTER_ENTRY = 'node_modules/oxlint/bin/oxlint'
39
+
40
+ /** Maps a plugin name to the code scope oxlint reports for it when that is neither the name nor `eslint-plugin-` and the name. */
41
+ export const CODE_SCOPES: Readonly<Record<string, string>> = Object.freeze({
42
+ typescript: 'typescript-eslint',
43
+ node: 'eslint-plugin-n',
44
+ nextjs: 'eslint-plugin-next',
45
+ })
46
+
47
+ /** Describes the hits per file a lint run found, and the diagnostics that were not hits. */
48
+ export interface Hits {
49
+ readonly total: number
50
+ readonly files: ReadonlyArray<readonly [string, number]>
51
+ readonly foreign: readonly string[]
52
+ }
53
+
54
+ /**
55
+ * Reads the value that follows a flag.
56
+ *
57
+ * @remarks
58
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
59
+ * and the `--flag=value` form is not read.
60
+ *
61
+ * @param argv - The arguments after the script path.
62
+ * @param name - The flag, such as `--rule`.
63
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
64
+ * another flag.
65
+ */
66
+ export function readOption(argv: readonly string[], name: string): string | undefined {
67
+ const index = argv.indexOf(name)
68
+ const value = index === -1 ? undefined : argv[index + 1]
69
+ return value === undefined || value.startsWith('--') ? undefined : value
70
+ }
71
+
72
+ /**
73
+ * Names the value flags a caller gave with no value.
74
+ *
75
+ * @param argv - The arguments after the script path.
76
+ * @param names - The flags that take a value.
77
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
78
+ */
79
+ export function readMissingFlags(
80
+ argv: readonly string[],
81
+ names: readonly string[],
82
+ ): readonly string[] {
83
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
84
+ }
85
+
86
+ /**
87
+ * Names the flags a caller gave that the script does not read.
88
+ *
89
+ * @remarks
90
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
91
+ * scan.
92
+ *
93
+ * @param argv - The arguments after the script path.
94
+ * @param flags - Every flag the script reads, value flags and switches alike.
95
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
96
+ */
97
+ export function readUnknownFlags(
98
+ argv: readonly string[],
99
+ flags: readonly string[],
100
+ ): readonly string[] {
101
+ const end = argv.indexOf('--')
102
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
103
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
104
+ )
105
+ }
106
+
107
+ /**
108
+ * Reads the arguments that are neither a flag nor the value of a value flag.
109
+ *
110
+ * @param argv - The arguments after the script path.
111
+ * @param valueFlags - The flags that take a value.
112
+ * @returns The positional arguments in order.
113
+ */
114
+ export function readPositionals(
115
+ argv: readonly string[],
116
+ valueFlags: readonly string[],
117
+ ): readonly string[] {
118
+ return argv.filter(
119
+ (argument, index) => !argument.startsWith('--') && !valueFlags.includes(argv[index - 1] ?? ''),
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Checks whether a value is a non-null, non-array object.
125
+ *
126
+ * @param value - The value to inspect.
127
+ * @returns True if the value is an object other than null or an array; false otherwise.
128
+ */
129
+ export function isRecord(value: unknown): value is Record<string, unknown> {
130
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
131
+ }
132
+
133
+ /**
134
+ * Normalizes a host path to forward slashes.
135
+ *
136
+ * @param path - A host path.
137
+ * @returns The same path with every backslash replaced by a slash.
138
+ */
139
+ export function normalizeSlashes(path: string): string {
140
+ return path.split('\\').join('/')
141
+ }
142
+
143
+ /**
144
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
145
+ *
146
+ * @param text - The text to quote.
147
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
148
+ */
149
+ export function quote(text: string): string {
150
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text)
151
+ }
152
+
153
+ /**
154
+ * Parses a JSON text without throwing.
155
+ *
156
+ * @param text - The text to parse.
157
+ * @returns The parsed value, or undefined when the text is not JSON.
158
+ */
159
+ export function parseJson(text: string): unknown {
160
+ try {
161
+ return JSON.parse(text)
162
+ } catch {
163
+ return undefined
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Names the directory that holds one rule's variant config.
169
+ *
170
+ * @param rule - The rule id, such as `policy/no-let`.
171
+ * @returns The id with every character outside letters, digits, dots, underscores, and hyphens
172
+ * replaced by a hyphen.
173
+ */
174
+ export function nameProbe(rule: string): string {
175
+ return rule.replace(/[^A-Za-z0-9._-]/gu, '-')
176
+ }
177
+
178
+ /**
179
+ * Turns a plugin specifier of the workspace config into one that loads from any directory.
180
+ *
181
+ * @param root - The checkout root the config's relative specifiers resolve against.
182
+ * @param entry - A `jsPlugins` entry: a specifier, or an object with a `specifier`.
183
+ * @returns The entry with a relative specifier made absolute with forward slashes; any other entry
184
+ * unchanged.
185
+ */
186
+ export function anchorPlugin(root: string, entry: unknown): unknown {
187
+ if (typeof entry === 'string') return anchorSpecifier(root, entry)
188
+ if (isRecord(entry) && typeof entry.specifier === 'string') {
189
+ return { ...entry, specifier: anchorSpecifier(root, entry.specifier) }
190
+ }
191
+ return entry
192
+ }
193
+
194
+ /**
195
+ * Makes a relative plugin specifier absolute.
196
+ *
197
+ * @param root - The checkout root.
198
+ * @param specifier - A plugin specifier.
199
+ * @returns The absolute forward-slash path when the specifier opens with a dot; otherwise the
200
+ * specifier unchanged.
201
+ */
202
+ export function anchorSpecifier(root: string, specifier: string): string {
203
+ return specifier.startsWith('.') && !isAbsolute(specifier)
204
+ ? normalizeSlashes(resolve(root, specifier))
205
+ : specifier
206
+ }
207
+
208
+ /**
209
+ * Builds the variant config that enables one rule beside the workspace's plugins.
210
+ *
211
+ * @param root - The checkout root.
212
+ * @param workspace - The parsed workspace `.oxlintrc.json`.
213
+ * @param rule - The rule id.
214
+ * @param options - The rule's arguments after `error`, empty for none.
215
+ * @returns The config object to write as JSON.
216
+ */
217
+ export function buildVariant(
218
+ root: string,
219
+ workspace: Record<string, unknown>,
220
+ rule: string,
221
+ options: readonly unknown[],
222
+ ): Record<string, unknown> {
223
+ const plugins = Array.isArray(workspace.plugins) ? { plugins: workspace.plugins } : {}
224
+ const jsPlugins = Array.isArray(workspace.jsPlugins)
225
+ ? { jsPlugins: workspace.jsPlugins.map((entry) => anchorPlugin(root, entry)) }
226
+ : {}
227
+ return {
228
+ ...plugins,
229
+ ...jsPlugins,
230
+ categories: { correctness: 'off', suspicious: 'off' },
231
+ rules: { [rule]: options.length === 0 ? 'error' : ['error', ...options] },
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Reads the arguments the `--options` value gives the rule.
237
+ *
238
+ * @param text - The JSON text of the option, or undefined when the flag is absent.
239
+ * @returns The arguments after `error`: the array's members, or the one value; undefined when the
240
+ * text is not JSON.
241
+ */
242
+ export function readRuleOptions(text: string | undefined): readonly unknown[] | undefined {
243
+ if (text === undefined) return []
244
+ const parsed = parseJson(text)
245
+ if (parsed === undefined) return undefined
246
+ return Array.isArray(parsed) ? parsed : [parsed]
247
+ }
248
+
249
+ /**
250
+ * Lists the code scopes oxlint may print for a rule's diagnostics.
251
+ *
252
+ * @param rule - The rule id, such as `policy/no-let` or `no-debugger`.
253
+ * @returns `eslint` for an unprefixed id; for a prefixed id, the prefix, the prefix after
254
+ * `eslint-plugin-`, and the scope {@link CODE_SCOPES} gives the plugin.
255
+ */
256
+ export function readCodeScopes(rule: string): readonly string[] {
257
+ const slash = rule.lastIndexOf('/')
258
+ if (slash === -1) return ['eslint']
259
+ const plugin = rule.slice(0, slash)
260
+ const mapped = CODE_SCOPES[plugin]
261
+ return [plugin, `eslint-plugin-${plugin}`, ...(mapped === undefined ? [] : [mapped])]
262
+ }
263
+
264
+ /**
265
+ * Checks whether a diagnostic's code names a rule.
266
+ *
267
+ * @param code - The diagnostic's `code` value.
268
+ * @param rule - The rule id.
269
+ * @returns True when the code reads `<scope>(<name>)` with the rule's name and one of its scopes;
270
+ * false for any other code, a missing one included.
271
+ */
272
+ export function namesRule(code: unknown, rule: string): boolean {
273
+ const match = typeof code === 'string' ? /^(.+?)\((.+)\)$/u.exec(code) : null
274
+ if (match === null) return false
275
+ return (
276
+ match[2] === rule.slice(rule.lastIndexOf('/') + 1) &&
277
+ readCodeScopes(rule).includes(match[1] ?? '')
278
+ )
279
+ }
280
+
281
+ /**
282
+ * Checks whether a diagnostic is a hit of a rule.
283
+ *
284
+ * @param diagnostic - One diagnostic record.
285
+ * @param rule - The rule id.
286
+ * @returns True when the diagnostic names a file and its code names the rule; false otherwise.
287
+ */
288
+ export function isHit(diagnostic: Record<string, unknown>, rule: string): boolean {
289
+ return typeof diagnostic.filename === 'string' && namesRule(diagnostic.code, rule)
290
+ }
291
+
292
+ /**
293
+ * Describes a diagnostic for a refusal.
294
+ *
295
+ * @param diagnostic - One diagnostic record.
296
+ * @returns Its code when it has one, otherwise its message, otherwise a fixed phrase.
297
+ */
298
+ export function describeDiagnostic(diagnostic: Record<string, unknown>): string {
299
+ if (typeof diagnostic.code === 'string' && diagnostic.code !== '') return diagnostic.code
300
+ return typeof diagnostic.message === 'string' ? diagnostic.message : 'a diagnostic with no code'
301
+ }
302
+
303
+ /**
304
+ * Reads the hits per file from oxlint's JSON output.
305
+ *
306
+ * @param output - The JSON text oxlint printed.
307
+ * @param rule - The rule id the hits are counted for.
308
+ * @returns The total and each file with its hits, most hits first and ties by path, and each other
309
+ * diagnostic described once; undefined when the output is not a report with a `diagnostics` array.
310
+ */
311
+ export function readHits(output: string, rule: string): Hits | undefined {
312
+ const report = parseJson(output)
313
+ if (!isRecord(report) || !Array.isArray(report.diagnostics)) return undefined
314
+ const records = report.diagnostics.filter(isRecord)
315
+ const names = records.flatMap((diagnostic): readonly string[] =>
316
+ isHit(diagnostic, rule) && typeof diagnostic.filename === 'string'
317
+ ? [normalizeSlashes(diagnostic.filename)]
318
+ : [],
319
+ )
320
+ const foreign = records.filter((diagnostic) => !isHit(diagnostic, rule)).map(describeDiagnostic)
321
+ const files = [...new Set(names)]
322
+ .map((name): readonly [string, number] => [
323
+ name,
324
+ names.filter((other) => other === name).length,
325
+ ])
326
+ .sort(
327
+ ([leftName, left], [rightName, right]) => right - left || leftName.localeCompare(rightName),
328
+ )
329
+ return { total: names.length, files, foreign: [...new Set(foreign)] }
330
+ }
331
+
332
+ /**
333
+ * Formats the report the script prints.
334
+ *
335
+ * @param rule - The rule id.
336
+ * @param hits - The hits per file.
337
+ * @returns The summary line and one `N<TAB>path` line per file, separated by line breaks.
338
+ */
339
+ export function formatHits(rule: string, hits: Hits): string {
340
+ return [
341
+ `rule=${rule} hits=${String(hits.total)} files=${String(hits.files.length)}`,
342
+ ...hits.files.map(([name, count]) => `${String(count)}\t${name}`),
343
+ ].join('\n')
344
+ }
345
+
346
+ /**
347
+ * Runs the script on its arguments.
348
+ *
349
+ * @param argv - The arguments after the script path.
350
+ * @returns The exit code: 0 when the rule has no hit, 3 when it has, 2 when the linter cannot run,
351
+ * 64 on usage.
352
+ */
353
+ export function main(argv: readonly string[]): number {
354
+ const unknown = readUnknownFlags(argv, FLAGS)
355
+ if (unknown.length > 0) {
356
+ console.error(`measure: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`)
357
+ return 64
358
+ }
359
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
360
+ if (missing.length > 0) {
361
+ console.error(`measure: ${missing.join(', ')} given with no value`)
362
+ return 64
363
+ }
364
+ const rule = readOption(argv, '--rule')
365
+ const paths = readPositionals(argv, VALUE_FLAGS)
366
+ if (rule === undefined || paths.length === 0) {
367
+ console.error(USAGE)
368
+ return 64
369
+ }
370
+ const options = readRuleOptions(readOption(argv, '--options'))
371
+ if (options === undefined) {
372
+ console.error(`measure: --options ${quote(readOption(argv, '--options') ?? '')} is not JSON`)
373
+ return 64
374
+ }
375
+ const root = process.cwd()
376
+ const configPath = join(root, '.oxlintrc.json')
377
+ const linter = join(root, LINTER_ENTRY)
378
+ const workspace = existsSync(configPath) ? parseJson(readFileSync(configPath, 'utf8')) : undefined
379
+ if (!isRecord(workspace) || !existsSync(linter)) {
380
+ console.error(
381
+ `measure: needs ${quote('.oxlintrc.json')} as JSON and ${quote(LINTER_ENTRY)} under ${quote(root)}; run from the checkout root after npm ci`,
382
+ )
383
+ return 2
384
+ }
385
+ const directory = join(root, PROBE_ROOT, nameProbe(rule))
386
+ mkdirSync(directory, { recursive: true })
387
+ const variant = join(directory, '.oxlintrc.json')
388
+ writeFileSync(variant, JSON.stringify(buildVariant(root, workspace, rule, options), null, '\t'))
389
+ const run = spawnSync(
390
+ process.execPath,
391
+ [linter, '--config', variant, '--format', 'json', ...paths],
392
+ { cwd: root, encoding: 'utf8', windowsHide: true, maxBuffer: 256 * 1024 * 1024 },
393
+ )
394
+ const hits = run.error === undefined ? readHits(run.stdout, rule) : undefined
395
+ if (hits === undefined) {
396
+ console.error(
397
+ `measure: the linter did not report: ${run.error?.message ?? quote(run.stderr.slice(0, 400))}`,
398
+ )
399
+ return 2
400
+ }
401
+ if (run.status !== 0 && run.status !== 1) {
402
+ console.error(
403
+ `measure: the linter exited ${String(run.status ?? 'by a signal')}, not 0 or 1: ${quote(run.stderr.slice(0, 400))}`,
404
+ )
405
+ return 2
406
+ }
407
+ if (run.status === 1 && hits.total + hits.foreign.length === 0) {
408
+ console.error(
409
+ `measure: the linter exited 1 with no diagnostic: ${quote(run.stderr.slice(0, 400))}`,
410
+ )
411
+ return 2
412
+ }
413
+ if (hits.foreign.length > 0) {
414
+ console.error(
415
+ `measure: the linter reported a diagnostic beside the rule, so the count is not the rule's: ${hits.foreign.slice(0, 5).map(quote).join(', ')}`,
416
+ )
417
+ return 2
418
+ }
419
+ console.log(formatHits(rule, hits))
420
+ return hits.total === 0 ? 0 : 3
421
+ }
422
+
423
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
424
+ process.exitCode = main(process.argv.slice(2))
425
+ }