@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,361 @@
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
+ /** Lists the flags that take a value. */
25
+ export const VALUE_FLAGS = ['--rule', '--options'];
26
+ /** Lists every flag the script reads, value flags and switches alike. */
27
+ export const FLAGS = VALUE_FLAGS;
28
+ /** Holds the usage line printed when a call names no rule or no path. */
29
+ export const USAGE = 'usage: measure.ts --rule ID [--options JSON] PATH...';
30
+ /** Holds the directory, under the working directory, that keeps the variant configs. */
31
+ export const PROBE_ROOT = 'tmp/probes/lint';
32
+ /** Holds the linter entry, under the working directory, that the script runs. */
33
+ export const LINTER_ENTRY = 'node_modules/oxlint/bin/oxlint';
34
+ /** Maps a plugin name to the code scope oxlint reports for it when that is neither the name nor `eslint-plugin-` and the name. */
35
+ export const CODE_SCOPES = Object.freeze({
36
+ typescript: 'typescript-eslint',
37
+ node: 'eslint-plugin-n',
38
+ nextjs: 'eslint-plugin-next',
39
+ });
40
+ /**
41
+ * Reads the value that follows a flag.
42
+ *
43
+ * @remarks
44
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
45
+ * and the `--flag=value` form is not read.
46
+ *
47
+ * @param argv - The arguments after the script path.
48
+ * @param name - The flag, such as `--rule`.
49
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
50
+ * another flag.
51
+ */
52
+ export function readOption(argv, name) {
53
+ const index = argv.indexOf(name);
54
+ const value = index === -1 ? undefined : argv[index + 1];
55
+ return value === undefined || value.startsWith('--') ? undefined : value;
56
+ }
57
+ /**
58
+ * Names the value flags a caller gave with no value.
59
+ *
60
+ * @param argv - The arguments after the script path.
61
+ * @param names - The flags that take a value.
62
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
63
+ */
64
+ export function readMissingFlags(argv, names) {
65
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
66
+ }
67
+ /**
68
+ * Names the flags a caller gave that the script does not read.
69
+ *
70
+ * @remarks
71
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
72
+ * scan.
73
+ *
74
+ * @param argv - The arguments after the script path.
75
+ * @param flags - Every flag the script reads, value flags and switches alike.
76
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
77
+ */
78
+ export function readUnknownFlags(argv, flags) {
79
+ const end = argv.indexOf('--');
80
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
81
+ }
82
+ /**
83
+ * Reads the arguments that are neither a flag nor the value of a value flag.
84
+ *
85
+ * @param argv - The arguments after the script path.
86
+ * @param valueFlags - The flags that take a value.
87
+ * @returns The positional arguments in order.
88
+ */
89
+ export function readPositionals(argv, valueFlags) {
90
+ return argv.filter((argument, index) => !argument.startsWith('--') && !valueFlags.includes(argv[index - 1] ?? ''));
91
+ }
92
+ /**
93
+ * Checks whether a value is a non-null, non-array object.
94
+ *
95
+ * @param value - The value to inspect.
96
+ * @returns True if the value is an object other than null or an array; false otherwise.
97
+ */
98
+ export function isRecord(value) {
99
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
100
+ }
101
+ /**
102
+ * Normalizes a host path to forward slashes.
103
+ *
104
+ * @param path - A host path.
105
+ * @returns The same path with every backslash replaced by a slash.
106
+ */
107
+ export function normalizeSlashes(path) {
108
+ return path.split('\\').join('/');
109
+ }
110
+ /**
111
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
112
+ *
113
+ * @param text - The text to quote.
114
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
115
+ */
116
+ export function quote(text) {
117
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text);
118
+ }
119
+ /**
120
+ * Parses a JSON text without throwing.
121
+ *
122
+ * @param text - The text to parse.
123
+ * @returns The parsed value, or undefined when the text is not JSON.
124
+ */
125
+ export function parseJson(text) {
126
+ try {
127
+ return JSON.parse(text);
128
+ }
129
+ catch {
130
+ return undefined;
131
+ }
132
+ }
133
+ /**
134
+ * Names the directory that holds one rule's variant config.
135
+ *
136
+ * @param rule - The rule id, such as `policy/no-let`.
137
+ * @returns The id with every character outside letters, digits, dots, underscores, and hyphens
138
+ * replaced by a hyphen.
139
+ */
140
+ export function nameProbe(rule) {
141
+ return rule.replace(/[^A-Za-z0-9._-]/gu, '-');
142
+ }
143
+ /**
144
+ * Turns a plugin specifier of the workspace config into one that loads from any directory.
145
+ *
146
+ * @param root - The checkout root the config's relative specifiers resolve against.
147
+ * @param entry - A `jsPlugins` entry: a specifier, or an object with a `specifier`.
148
+ * @returns The entry with a relative specifier made absolute with forward slashes; any other entry
149
+ * unchanged.
150
+ */
151
+ export function anchorPlugin(root, entry) {
152
+ if (typeof entry === 'string')
153
+ return anchorSpecifier(root, entry);
154
+ if (isRecord(entry) && typeof entry.specifier === 'string') {
155
+ return { ...entry, specifier: anchorSpecifier(root, entry.specifier) };
156
+ }
157
+ return entry;
158
+ }
159
+ /**
160
+ * Makes a relative plugin specifier absolute.
161
+ *
162
+ * @param root - The checkout root.
163
+ * @param specifier - A plugin specifier.
164
+ * @returns The absolute forward-slash path when the specifier opens with a dot; otherwise the
165
+ * specifier unchanged.
166
+ */
167
+ export function anchorSpecifier(root, specifier) {
168
+ return specifier.startsWith('.') && !isAbsolute(specifier)
169
+ ? normalizeSlashes(resolve(root, specifier))
170
+ : specifier;
171
+ }
172
+ /**
173
+ * Builds the variant config that enables one rule beside the workspace's plugins.
174
+ *
175
+ * @param root - The checkout root.
176
+ * @param workspace - The parsed workspace `.oxlintrc.json`.
177
+ * @param rule - The rule id.
178
+ * @param options - The rule's arguments after `error`, empty for none.
179
+ * @returns The config object to write as JSON.
180
+ */
181
+ export function buildVariant(root, workspace, rule, options) {
182
+ const plugins = Array.isArray(workspace.plugins) ? { plugins: workspace.plugins } : {};
183
+ const jsPlugins = Array.isArray(workspace.jsPlugins)
184
+ ? { jsPlugins: workspace.jsPlugins.map((entry) => anchorPlugin(root, entry)) }
185
+ : {};
186
+ return {
187
+ ...plugins,
188
+ ...jsPlugins,
189
+ categories: { correctness: 'off', suspicious: 'off' },
190
+ rules: { [rule]: options.length === 0 ? 'error' : ['error', ...options] },
191
+ };
192
+ }
193
+ /**
194
+ * Reads the arguments the `--options` value gives the rule.
195
+ *
196
+ * @param text - The JSON text of the option, or undefined when the flag is absent.
197
+ * @returns The arguments after `error`: the array's members, or the one value; undefined when the
198
+ * text is not JSON.
199
+ */
200
+ export function readRuleOptions(text) {
201
+ if (text === undefined)
202
+ return [];
203
+ const parsed = parseJson(text);
204
+ if (parsed === undefined)
205
+ return undefined;
206
+ return Array.isArray(parsed) ? parsed : [parsed];
207
+ }
208
+ /**
209
+ * Lists the code scopes oxlint may print for a rule's diagnostics.
210
+ *
211
+ * @param rule - The rule id, such as `policy/no-let` or `no-debugger`.
212
+ * @returns `eslint` for an unprefixed id; for a prefixed id, the prefix, the prefix after
213
+ * `eslint-plugin-`, and the scope {@link CODE_SCOPES} gives the plugin.
214
+ */
215
+ export function readCodeScopes(rule) {
216
+ const slash = rule.lastIndexOf('/');
217
+ if (slash === -1)
218
+ return ['eslint'];
219
+ const plugin = rule.slice(0, slash);
220
+ const mapped = CODE_SCOPES[plugin];
221
+ return [plugin, `eslint-plugin-${plugin}`, ...(mapped === undefined ? [] : [mapped])];
222
+ }
223
+ /**
224
+ * Checks whether a diagnostic's code names a rule.
225
+ *
226
+ * @param code - The diagnostic's `code` value.
227
+ * @param rule - The rule id.
228
+ * @returns True when the code reads `<scope>(<name>)` with the rule's name and one of its scopes;
229
+ * false for any other code, a missing one included.
230
+ */
231
+ export function namesRule(code, rule) {
232
+ const match = typeof code === 'string' ? /^(.+?)\((.+)\)$/u.exec(code) : null;
233
+ if (match === null)
234
+ return false;
235
+ return (match[2] === rule.slice(rule.lastIndexOf('/') + 1) &&
236
+ readCodeScopes(rule).includes(match[1] ?? ''));
237
+ }
238
+ /**
239
+ * Checks whether a diagnostic is a hit of a rule.
240
+ *
241
+ * @param diagnostic - One diagnostic record.
242
+ * @param rule - The rule id.
243
+ * @returns True when the diagnostic names a file and its code names the rule; false otherwise.
244
+ */
245
+ export function isHit(diagnostic, rule) {
246
+ return typeof diagnostic.filename === 'string' && namesRule(diagnostic.code, rule);
247
+ }
248
+ /**
249
+ * Describes a diagnostic for a refusal.
250
+ *
251
+ * @param diagnostic - One diagnostic record.
252
+ * @returns Its code when it has one, otherwise its message, otherwise a fixed phrase.
253
+ */
254
+ export function describeDiagnostic(diagnostic) {
255
+ if (typeof diagnostic.code === 'string' && diagnostic.code !== '')
256
+ return diagnostic.code;
257
+ return typeof diagnostic.message === 'string' ? diagnostic.message : 'a diagnostic with no code';
258
+ }
259
+ /**
260
+ * Reads the hits per file from oxlint's JSON output.
261
+ *
262
+ * @param output - The JSON text oxlint printed.
263
+ * @param rule - The rule id the hits are counted for.
264
+ * @returns The total and each file with its hits, most hits first and ties by path, and each other
265
+ * diagnostic described once; undefined when the output is not a report with a `diagnostics` array.
266
+ */
267
+ export function readHits(output, rule) {
268
+ const report = parseJson(output);
269
+ if (!isRecord(report) || !Array.isArray(report.diagnostics))
270
+ return undefined;
271
+ const records = report.diagnostics.filter(isRecord);
272
+ const names = records.flatMap((diagnostic) => isHit(diagnostic, rule) && typeof diagnostic.filename === 'string'
273
+ ? [normalizeSlashes(diagnostic.filename)]
274
+ : []);
275
+ const foreign = records.filter((diagnostic) => !isHit(diagnostic, rule)).map(describeDiagnostic);
276
+ const files = [...new Set(names)]
277
+ .map((name) => [
278
+ name,
279
+ names.filter((other) => other === name).length,
280
+ ])
281
+ .sort(([leftName, left], [rightName, right]) => right - left || leftName.localeCompare(rightName));
282
+ return { total: names.length, files, foreign: [...new Set(foreign)] };
283
+ }
284
+ /**
285
+ * Formats the report the script prints.
286
+ *
287
+ * @param rule - The rule id.
288
+ * @param hits - The hits per file.
289
+ * @returns The summary line and one `N<TAB>path` line per file, separated by line breaks.
290
+ */
291
+ export function formatHits(rule, hits) {
292
+ return [
293
+ `rule=${rule} hits=${String(hits.total)} files=${String(hits.files.length)}`,
294
+ ...hits.files.map(([name, count]) => `${String(count)}\t${name}`),
295
+ ].join('\n');
296
+ }
297
+ /**
298
+ * Runs the script on its arguments.
299
+ *
300
+ * @param argv - The arguments after the script path.
301
+ * @returns The exit code: 0 when the rule has no hit, 3 when it has, 2 when the linter cannot run,
302
+ * 64 on usage.
303
+ */
304
+ export function main(argv) {
305
+ const unknown = readUnknownFlags(argv, FLAGS);
306
+ if (unknown.length > 0) {
307
+ console.error(`measure: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
308
+ return 64;
309
+ }
310
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
311
+ if (missing.length > 0) {
312
+ console.error(`measure: ${missing.join(', ')} given with no value`);
313
+ return 64;
314
+ }
315
+ const rule = readOption(argv, '--rule');
316
+ const paths = readPositionals(argv, VALUE_FLAGS);
317
+ if (rule === undefined || paths.length === 0) {
318
+ console.error(USAGE);
319
+ return 64;
320
+ }
321
+ const options = readRuleOptions(readOption(argv, '--options'));
322
+ if (options === undefined) {
323
+ console.error(`measure: --options ${quote(readOption(argv, '--options') ?? '')} is not JSON`);
324
+ return 64;
325
+ }
326
+ const root = process.cwd();
327
+ const configPath = join(root, '.oxlintrc.json');
328
+ const linter = join(root, LINTER_ENTRY);
329
+ const workspace = existsSync(configPath) ? parseJson(readFileSync(configPath, 'utf8')) : undefined;
330
+ if (!isRecord(workspace) || !existsSync(linter)) {
331
+ console.error(`measure: needs ${quote('.oxlintrc.json')} as JSON and ${quote(LINTER_ENTRY)} under ${quote(root)}; run from the checkout root after npm ci`);
332
+ return 2;
333
+ }
334
+ const directory = join(root, PROBE_ROOT, nameProbe(rule));
335
+ mkdirSync(directory, { recursive: true });
336
+ const variant = join(directory, '.oxlintrc.json');
337
+ writeFileSync(variant, JSON.stringify(buildVariant(root, workspace, rule, options), null, '\t'));
338
+ const run = spawnSync(process.execPath, [linter, '--config', variant, '--format', 'json', ...paths], { cwd: root, encoding: 'utf8', windowsHide: true, maxBuffer: 256 * 1024 * 1024 });
339
+ const hits = run.error === undefined ? readHits(run.stdout, rule) : undefined;
340
+ if (hits === undefined) {
341
+ console.error(`measure: the linter did not report: ${run.error?.message ?? quote(run.stderr.slice(0, 400))}`);
342
+ return 2;
343
+ }
344
+ if (run.status !== 0 && run.status !== 1) {
345
+ console.error(`measure: the linter exited ${String(run.status ?? 'by a signal')}, not 0 or 1: ${quote(run.stderr.slice(0, 400))}`);
346
+ return 2;
347
+ }
348
+ if (run.status === 1 && hits.total + hits.foreign.length === 0) {
349
+ console.error(`measure: the linter exited 1 with no diagnostic: ${quote(run.stderr.slice(0, 400))}`);
350
+ return 2;
351
+ }
352
+ if (hits.foreign.length > 0) {
353
+ console.error(`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(', ')}`);
354
+ return 2;
355
+ }
356
+ console.log(formatHits(rule, hits));
357
+ return hits.total === 0 ? 0 : 3;
358
+ }
359
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
360
+ process.exitCode = main(process.argv.slice(2));
361
+ }