@orkestrel/scaffold 0.0.107 → 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 (164) 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 +368 -27
  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 +26 -20
  25. package/dist/host/agents/orchestration.md +98 -58
  26. package/dist/host/agents/platforms/posix.md +15 -0
  27. package/dist/host/agents/platforms/windows.md +37 -0
  28. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  29. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  30. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  31. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  32. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  33. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  34. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  35. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  36. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  37. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +12 -14
  38. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
  39. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  48. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +428 -27
  49. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  50. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  51. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  52. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  53. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  54. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  55. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  56. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  58. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  59. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  60. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  63. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  64. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  65. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  66. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  67. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  68. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  69. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +91 -30
  70. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
  71. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
  72. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
  73. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
  74. package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
  75. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  76. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  77. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  78. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  79. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  80. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  81. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  82. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -10
  83. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  86. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  87. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  88. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  89. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  90. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  91. package/dist/host/agents/templates/brief.md +1 -1
  92. package/dist/host/agents/transports/claude-cli.md +3 -3
  93. package/dist/host/agents/transports/codex.md +3 -8
  94. package/dist/host/agents/transports/cursor.md +2 -3
  95. package/dist/host/claude/AGENTS.md +7 -10
  96. package/dist/host/claude/agents/analyst.md +3 -3
  97. package/dist/host/claude/agents/astra.md +3 -3
  98. package/dist/host/claude/agents/builder.md +1 -1
  99. package/dist/host/claude/agents/checker.md +1 -1
  100. package/dist/host/claude/agents/distiller.md +1 -1
  101. package/dist/host/claude/agents/grok.md +1 -1
  102. package/dist/host/claude/agents/opus.md +2 -2
  103. package/dist/host/claude/agents/orkestrel.md +57 -90
  104. package/dist/host/claude/agents/researcher.md +1 -1
  105. package/dist/host/claude/agents/reviewer.md +4 -0
  106. package/dist/host/claude/agents/scout.md +1 -1
  107. package/dist/host/claude/agents/verifier.md +1 -1
  108. package/dist/host/claude/rules/application.md +14 -21
  109. package/dist/host/claude/rules/architecture.md +34 -44
  110. package/dist/host/claude/rules/browser.md +3 -4
  111. package/dist/host/claude/rules/campaign.md +55 -0
  112. package/dist/host/claude/rules/documentation.md +26 -11
  113. package/dist/host/claude/rules/names.md +9 -4
  114. package/dist/host/claude/rules/patterns.md +6 -7
  115. package/dist/host/claude/rules/portability.md +2 -3
  116. package/dist/host/claude/rules/quality.md +4 -5
  117. package/dist/host/claude/rules/styles.md +2 -2
  118. package/dist/host/claude/rules/tests.md +57 -114
  119. package/dist/host/claude/rules/typescript.md +8 -10
  120. package/dist/host/claude/rules/workspace.md +21 -19
  121. package/dist/host/claude/rules/writing.md +3 -3
  122. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  123. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  124. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +1 -1
  125. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  126. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  127. package/dist/host/codex/agents/analyst.toml +9 -3
  128. package/dist/host/codex/agents/astra.toml +3 -4
  129. package/dist/host/codex/agents/builder.toml +1 -1
  130. package/dist/host/codex/agents/opus.toml +3 -4
  131. package/dist/host/codex/agents/orkestrel.toml +1 -1
  132. package/dist/host/codex/agents/researcher.toml +2 -2
  133. package/dist/host/codex/agents/verifier.toml +1 -1
  134. package/dist/host/codex/config.toml +3 -3
  135. package/dist/host/configs/helpers.ts +21 -1
  136. package/dist/host/configs/policy.ts +405 -16
  137. package/dist/host/cursor/rules/orchestration.mdc +1 -0
  138. package/dist/host/dotfiles/oxlintrc.json +141 -5
  139. package/dist/host/guides/agent.md +111 -134
  140. package/dist/host/guides/capture.md +801 -0
  141. package/dist/host/guides/probe.md +27 -8
  142. package/dist/host/guides/scaffold.md +152 -19
  143. package/dist/host/guides/study.md +534 -440
  144. package/dist/host/guides/test.md +111 -44
  145. package/dist/host/manifest.json +199 -242
  146. package/dist/host/tests/config.test.ts +1558 -219
  147. package/dist/host/tests/policy.test.ts +127 -1
  148. package/dist/host/tests/setupPolicy.ts +1389 -39
  149. package/dist/src/core/index.cjs +239 -87
  150. package/dist/src/core/index.cjs.map +1 -1
  151. package/dist/src/core/index.d.cts +123 -39
  152. package/dist/src/core/index.d.ts +123 -39
  153. package/dist/src/core/index.js +234 -86
  154. package/dist/src/core/index.js.map +1 -1
  155. package/dist/src/server/index.cjs +20 -27
  156. package/dist/src/server/index.cjs.map +1 -1
  157. package/dist/src/server/index.js +21 -28
  158. package/dist/src/server/index.js.map +1 -1
  159. package/package.json +11 -11
  160. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  161. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  162. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  163. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
  164. package/dist/host/guides/supervisor.md +0 -4890
@@ -0,0 +1,280 @@
1
+ // Run a plan of commands in order and journal each one. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/chain.ts --plan FILE.json --journal DIR
3
+ // The plan is a JSON array of steps, each `{ "name": "...", "command": "...", "args": ["..."] }`
4
+ // with an optional `"cwd"` (a directory, relative to the working directory) and `"env"` (a record
5
+ // of strings laid over this process's environment). A step name is letters, digits, dots,
6
+ // underscores, and hyphens, opens with a letter or digit, and is unique in the plan. Each step runs
7
+ // with no shell and closed stdin, in plan order, with its standard output and standard error
8
+ // written together to `DIR/<name>.txt`; the script prints one `name exit=N durationMs=N` line per
9
+ // step and stops at the first step that does not exit 0. Because no shell runs, a command that is a
10
+ // Windows `.cmd` shim, such as `npm`, starts only through the interpreter it wraps. A step that
11
+ // must not stop the chain belongs in a second plan; launch the chain itself through `launch.ts`
12
+ // for a cap. The script refuses a flag it does not read, the `--flag=value` form included.
13
+ // Exit 0 when every step exited 0, the failing step's exit code when one did not, 2 when a step
14
+ // cannot start, 64 on usage.
15
+ import { spawnSync } from 'node:child_process';
16
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync } from 'node:fs';
17
+ import { join, resolve } from 'node:path';
18
+ /** Lists the flags that take a value. */
19
+ export const VALUE_FLAGS = ['--plan', '--journal'];
20
+ /** Lists every flag the script reads, value flags and switches alike. */
21
+ export const FLAGS = VALUE_FLAGS;
22
+ /** Holds the usage line printed when a call names no plan or no journal. */
23
+ export const USAGE = 'usage: chain.ts --plan FILE.json --journal DIR';
24
+ /** Matches a step name that is safe as a journal file name. */
25
+ export const STEP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
26
+ /**
27
+ * Reads the value that follows a flag.
28
+ *
29
+ * @remarks
30
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
31
+ * and the `--flag=value` form is not read.
32
+ *
33
+ * @param argv - The arguments after the script path.
34
+ * @param name - The flag, such as `--plan`.
35
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
36
+ * another flag.
37
+ */
38
+ export function readOption(argv, name) {
39
+ const index = argv.indexOf(name);
40
+ const value = index === -1 ? undefined : argv[index + 1];
41
+ return value === undefined || value.startsWith('--') ? undefined : value;
42
+ }
43
+ /**
44
+ * Names the value flags a caller gave with no value.
45
+ *
46
+ * @param argv - The arguments after the script path.
47
+ * @param names - The flags that take a value.
48
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
49
+ */
50
+ export function readMissingFlags(argv, names) {
51
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
52
+ }
53
+ /**
54
+ * Names the flags a caller gave that the script does not read.
55
+ *
56
+ * @remarks
57
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
58
+ * scan.
59
+ *
60
+ * @param argv - The arguments after the script path.
61
+ * @param flags - Every flag the script reads, value flags and switches alike.
62
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
63
+ */
64
+ export function readUnknownFlags(argv, flags) {
65
+ const end = argv.indexOf('--');
66
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
67
+ }
68
+ /**
69
+ * Checks whether a value is a non-null, non-array object.
70
+ *
71
+ * @param value - The value to inspect.
72
+ * @returns True if the value is an object other than null or an array; false otherwise.
73
+ */
74
+ export function isRecord(value) {
75
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
76
+ }
77
+ /**
78
+ * Checks whether a value is an array of strings.
79
+ *
80
+ * @param value - The value to inspect.
81
+ * @returns True if the value is an array whose every member is a string; false otherwise.
82
+ */
83
+ export function isStringArray(value) {
84
+ return Array.isArray(value) && value.every((member) => typeof member === 'string');
85
+ }
86
+ /**
87
+ * Checks whether a value is a record whose every member is a string.
88
+ *
89
+ * @param value - The value to inspect.
90
+ * @returns True if the value is a record with only string members; false otherwise.
91
+ */
92
+ export function isStringRecord(value) {
93
+ return isRecord(value) && Object.values(value).every((member) => typeof member === 'string');
94
+ }
95
+ /**
96
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
97
+ *
98
+ * @param text - The text to quote.
99
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
100
+ */
101
+ export function quote(text) {
102
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text);
103
+ }
104
+ /**
105
+ * Lists what is wrong with one plan entry.
106
+ *
107
+ * @param entry - The parsed JSON value at one position of the plan.
108
+ * @param position - The entry's zero-based position, named in each problem.
109
+ * @returns Each problem in the entry, empty when the entry is a step.
110
+ */
111
+ export function readStepProblems(entry, position) {
112
+ const where = `step ${String(position + 1)}`;
113
+ if (!isRecord(entry))
114
+ return [`${where} is not an object`];
115
+ const known = ['name', 'command', 'args', 'cwd', 'env'];
116
+ const unexpected = Object.keys(entry).filter((key) => !known.includes(key));
117
+ return [
118
+ ...(typeof entry.name === 'string' && STEP_NAME.test(entry.name)
119
+ ? []
120
+ : [
121
+ `${where} "name" must be letters, digits, dots, underscores, and hyphens, opening with a letter or digit`,
122
+ ]),
123
+ ...(typeof entry.command === 'string' && entry.command !== ''
124
+ ? []
125
+ : [`${where} "command" must be a non-empty string`]),
126
+ ...(isStringArray(entry.args) ? [] : [`${where} "args" must be an array of strings`]),
127
+ ...(entry.cwd === undefined || (typeof entry.cwd === 'string' && entry.cwd !== '')
128
+ ? []
129
+ : [`${where} "cwd" must be a non-empty string when present`]),
130
+ ...(entry.env === undefined || isStringRecord(entry.env)
131
+ ? []
132
+ : [`${where} "env" must be a record of strings when present`]),
133
+ ...unexpected.map((key) => `${where} carries the unexpected key ${quote(key)}`),
134
+ ];
135
+ }
136
+ /**
137
+ * Reads a plan from its JSON text.
138
+ *
139
+ * @param text - The plan file's text.
140
+ * @returns The steps in order when the plan is valid, and every problem otherwise: not JSON, not an
141
+ * array, empty, a malformed step, or a name used twice.
142
+ */
143
+ export function parsePlan(text) {
144
+ const parsed = parseJson(text);
145
+ if (!Array.isArray(parsed))
146
+ return { steps: [], problems: ['the plan is not a JSON array of steps'] };
147
+ if (parsed.length === 0)
148
+ return { steps: [], problems: ['the plan holds no step'] };
149
+ const problems = parsed.flatMap((entry, position) => readStepProblems(entry, position));
150
+ const names = parsed.flatMap((entry) => isRecord(entry) && typeof entry.name === 'string' ? [entry.name] : []);
151
+ const repeated = [...new Set(names.filter((name, index) => names.indexOf(name) !== index))];
152
+ const all = [
153
+ ...problems,
154
+ ...repeated.map((name) => `the name ${quote(name)} is used by more than one step`),
155
+ ];
156
+ if (all.length > 0)
157
+ return { steps: [], problems: all };
158
+ return { steps: parsed.flatMap((entry) => readStep(entry)), problems: [] };
159
+ }
160
+ /**
161
+ * Parses a JSON text without throwing.
162
+ *
163
+ * @param text - The text to parse.
164
+ * @returns The parsed value, or undefined when the text is not JSON.
165
+ */
166
+ export function parseJson(text) {
167
+ try {
168
+ return JSON.parse(text);
169
+ }
170
+ catch {
171
+ return undefined;
172
+ }
173
+ }
174
+ /**
175
+ * Reads a validated plan entry as a step.
176
+ *
177
+ * @param entry - A plan entry that `readStepProblems` found no problem in.
178
+ * @returns The step as a one-member list, or an empty list when the entry is not a valid step.
179
+ */
180
+ export function readStep(entry) {
181
+ if (!isRecord(entry))
182
+ return [];
183
+ const { name, command, args, cwd, env } = entry;
184
+ if (typeof name !== 'string' || typeof command !== 'string' || !isStringArray(args))
185
+ return [];
186
+ return [
187
+ {
188
+ name,
189
+ command,
190
+ args,
191
+ cwd: typeof cwd === 'string' ? cwd : undefined,
192
+ env: isStringRecord(env) ? env : {},
193
+ },
194
+ ];
195
+ }
196
+ /**
197
+ * Runs one step with no shell, its output and errors written together to a file.
198
+ *
199
+ * @param step - The step to run.
200
+ * @param output - The file that receives the step's standard output and standard error.
201
+ * @returns The exit code, or undefined with the reason when the command cannot start or a signal
202
+ * ended it; and the duration.
203
+ */
204
+ export function runStep(step, output) {
205
+ const started = Date.now();
206
+ const descriptor = openSync(output, 'w');
207
+ try {
208
+ const result = spawnSync(step.command, step.args, {
209
+ cwd: step.cwd === undefined ? undefined : resolve(step.cwd),
210
+ env: { ...process.env, ...step.env },
211
+ stdio: ['ignore', descriptor, descriptor],
212
+ windowsHide: true,
213
+ shell: false,
214
+ });
215
+ const error = result.error?.message ?? (result.signal === null ? undefined : `ended by ${result.signal}`);
216
+ return {
217
+ exit: error === undefined ? (result.status ?? undefined) : undefined,
218
+ error,
219
+ durationMs: Date.now() - started,
220
+ };
221
+ }
222
+ finally {
223
+ closeSync(descriptor);
224
+ }
225
+ }
226
+ /**
227
+ * Formats the line printed for a finished step.
228
+ *
229
+ * @param step - The step.
230
+ * @param finish - How the step ended.
231
+ * @returns `name exit=N durationMs=N`, with `exit=none` and the reason when it never exited.
232
+ */
233
+ export function formatStep(step, finish) {
234
+ const exit = finish.exit === undefined ? 'none' : String(finish.exit);
235
+ const reason = finish.error === undefined ? '' : ` error=${quote(finish.error)}`;
236
+ return `${step.name} exit=${exit} durationMs=${String(finish.durationMs)}${reason}`;
237
+ }
238
+ /**
239
+ * Runs the script on its arguments.
240
+ *
241
+ * @param argv - The arguments after the script path.
242
+ * @returns The exit code: 0 when every step exited 0, the failing step's exit code, 2 when a step
243
+ * cannot start, 64 on usage.
244
+ */
245
+ export function main(argv) {
246
+ const unknown = readUnknownFlags(argv, FLAGS);
247
+ if (unknown.length > 0) {
248
+ console.error(`chain: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
249
+ return 64;
250
+ }
251
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
252
+ if (missing.length > 0) {
253
+ console.error(`chain: ${missing.join(', ')} given with no value`);
254
+ return 64;
255
+ }
256
+ const path = readOption(argv, '--plan');
257
+ const journal = readOption(argv, '--journal');
258
+ if (path === undefined || journal === undefined || !existsSync(path)) {
259
+ console.error(USAGE);
260
+ return 64;
261
+ }
262
+ const plan = parsePlan(readFileSync(path, 'utf8'));
263
+ if (plan.problems.length > 0) {
264
+ console.error(`chain: ${quote(path)} is not a valid plan: ${plan.problems.join('; ')}`);
265
+ return 64;
266
+ }
267
+ mkdirSync(journal, { recursive: true });
268
+ for (const step of plan.steps) {
269
+ const finish = runStep(step, join(journal, `${step.name}.txt`));
270
+ console.log(formatStep(step, finish));
271
+ if (finish.exit === undefined)
272
+ return 2;
273
+ if (finish.exit !== 0)
274
+ return finish.exit;
275
+ }
276
+ return 0;
277
+ }
278
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
279
+ process.exitCode = main(process.argv.slice(2));
280
+ }
@@ -6,18 +6,146 @@
6
6
  // break ends the last line. A citation naming a bare filename that no root-relative
7
7
  // file matches resolves when exactly one file under the root carries that name, outside
8
8
  // node_modules, dist, tmp, .git, .orkestrel, and .idea. The summary lists every citation that does
9
- // not resolve, with the reason. Exit 0 when all resolve, 3 when any does not, 64 on usage.
10
- import { existsSync, readFileSync, statSync } from 'node:fs';
11
- import { basename, relative, resolve } from 'node:path';
12
- import { listFiles, normalizeSlashes, readMissingFlags, readOption } from "./helpers.js";
13
- const CITATION = /(?<![\w/\\.-])((?:\.{1,2}[/\\])?\.?[A-Za-z0-9_@][A-Za-z0-9_./\\@-]*\.[a-z]{1,5}):(\d+)(?:[-–](\d+))?(?![\w:])/gu;
14
- function indexBasenames(root) {
9
+ // not resolve, with the reason. The script refuses a flag it does not read, the `--flag=value` form
10
+ // included. Exit 0 when all resolve, 3 when any does not, 64 on usage.
11
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
12
+ import { basename, join, relative, resolve } from 'node:path';
13
+ /** Matches one `file:line` or `file:line-line` citation in a text. */
14
+ export const CITATION = /(?<![\w/\\.-])((?:\.{1,2}[/\\])?\.?[A-Za-z0-9_@][A-Za-z0-9_./\\@-]*\.[a-z]{1,5}):(\d+)(?:[-–](\d+))?(?![\w:])/gu;
15
+ /** Names the directories a tree walk skips. */
16
+ export const SKIPPED_DIRECTORIES = new Set([
17
+ 'node_modules',
18
+ '.git',
19
+ 'dist',
20
+ 'tmp',
21
+ '.orkestrel',
22
+ '.idea',
23
+ ]);
24
+ /** Lists the flags that take a value. */
25
+ export const VALUE_FLAGS = ['--root'];
26
+ /** Lists every flag the script reads, value flags and switches alike. */
27
+ export const FLAGS = [...VALUE_FLAGS, '--json'];
28
+ /** Holds the usage line printed when a call names no report. */
29
+ export const USAGE = 'usage: cite.ts PATH [--root DIR] [--json]';
30
+ /**
31
+ * Reads the value that follows a flag.
32
+ *
33
+ * @remarks
34
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
35
+ * and the `--flag=value` form is not read.
36
+ *
37
+ * @param argv - The arguments after the script path.
38
+ * @param name - The flag, such as `--root`.
39
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
40
+ * another flag.
41
+ */
42
+ export function readOption(argv, name) {
43
+ const index = argv.indexOf(name);
44
+ const value = index === -1 ? undefined : argv[index + 1];
45
+ return value === undefined || value.startsWith('--') ? undefined : value;
46
+ }
47
+ /**
48
+ * Names the value flags a caller gave with no value.
49
+ *
50
+ * @param argv - The arguments after the script path.
51
+ * @param names - The flags that take a value.
52
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
53
+ */
54
+ export function readMissingFlags(argv, names) {
55
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
56
+ }
57
+ /**
58
+ * Names the flags a caller gave that the script does not read.
59
+ *
60
+ * @remarks
61
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
62
+ * scan.
63
+ *
64
+ * @param argv - The arguments after the script path.
65
+ * @param flags - Every flag the script reads, value flags and switches alike.
66
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
67
+ */
68
+ export function readUnknownFlags(argv, flags) {
69
+ const end = argv.indexOf('--');
70
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
71
+ }
72
+ /**
73
+ * Normalizes a host path to forward slashes.
74
+ *
75
+ * @param path - A host path.
76
+ * @returns The same path with every backslash replaced by a slash.
77
+ */
78
+ export function normalizeSlashes(path) {
79
+ return path.split('\\').join('/');
80
+ }
81
+ /**
82
+ * Lists every regular file beneath a directory, skipping the directories the script never reads.
83
+ *
84
+ * @remarks
85
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
86
+ *
87
+ * @param directory - The directory to walk.
88
+ * @param skipped - The directory names to skip. Default: the skipped directories.
89
+ * @returns Each file's forward-slash path and modification time, in walk order.
90
+ * @throws Thrown when `directory` does not exist or cannot be read.
91
+ */
92
+ export function listFiles(directory, skipped = SKIPPED_DIRECTORIES) {
93
+ const files = [];
94
+ const pending = [directory];
95
+ while (pending.length > 0) {
96
+ const current = pending.pop();
97
+ if (current === undefined)
98
+ break;
99
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
100
+ if (skipped.has(entry.name))
101
+ continue;
102
+ const path = join(current, entry.name);
103
+ if (entry.isDirectory())
104
+ pending.push(path);
105
+ else if (entry.isFile()) {
106
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs });
107
+ }
108
+ }
109
+ }
110
+ return files;
111
+ }
112
+ /**
113
+ * Extracts every citation a text carries.
114
+ *
115
+ * @param text - The report text.
116
+ * @returns Each citation in text order, with its file, its line, and the end of a range.
117
+ */
118
+ export function extractCitations(text) {
119
+ return [...text.matchAll(CITATION)].flatMap((match) => match[1] === undefined
120
+ ? []
121
+ : [
122
+ {
123
+ citation: match[0],
124
+ file: match[1],
125
+ line: Number(match[2]),
126
+ end: match[3] === undefined ? undefined : Number(match[3]),
127
+ },
128
+ ]);
129
+ }
130
+ /**
131
+ * Indexes every file under a root by its basename.
132
+ *
133
+ * @param root - The directory to walk.
134
+ * @returns Each basename mapped to the root-relative forward-slash paths that carry it.
135
+ */
136
+ export function indexBasenames(root) {
15
137
  return new Map([...Map.groupBy(listFiles(root), (file) => basename(file.path))].map(([name, files]) => [
16
138
  name,
17
139
  files.map((file) => normalizeSlashes(relative(root, file.path))),
18
140
  ]));
19
141
  }
20
- function countLines(path) {
142
+ /**
143
+ * Counts the lines of a file.
144
+ *
145
+ * @param path - The file to read.
146
+ * @returns The number of lines, where a trailing line break ends the last line and an empty file has none.
147
+ */
148
+ export function countLines(path) {
21
149
  const text = readFileSync(path, 'utf8');
22
150
  if (text === '')
23
151
  return 0;
@@ -25,15 +153,36 @@ function countLines(path) {
25
153
  // A trailing line break ends the last line rather than opening a phantom one.
26
154
  return lines.at(-1) === '' ? lines.length - 1 : lines.length;
27
155
  }
28
- function resolveDirect(root, file) {
156
+ /**
157
+ * Resolves a citation's file directly under the root.
158
+ *
159
+ * @param root - The directory a citation's path is relative to.
160
+ * @param file - The cited path, a backslash read as a slash.
161
+ * @returns The absolute path of the file, or undefined when no regular file sits there.
162
+ */
163
+ export function resolveDirect(root, file) {
29
164
  const direct = resolve(root, normalizeSlashes(file));
30
165
  return existsSync(direct) && statSync(direct).isFile() ? direct : undefined;
31
166
  }
32
- // A bare filename that no root-relative file matches is the one citation the basename index serves.
33
- function needsIndex(root, file) {
167
+ /**
168
+ * Checks whether a citation can resolve only through the basename index.
169
+ *
170
+ * @param root - The directory a citation's path is relative to.
171
+ * @param file - The cited path.
172
+ * @returns True if no root-relative file matches and the path is a bare filename; false otherwise.
173
+ */
174
+ export function needsIndex(root, file) {
34
175
  return resolveDirect(root, file) === undefined && !/[/\\]/u.test(file);
35
176
  }
36
- function resolveFile(root, file, index) {
177
+ /**
178
+ * Resolves a citation's file, directly or through the basename index.
179
+ *
180
+ * @param root - The directory a citation's path is relative to.
181
+ * @param file - The cited path.
182
+ * @param index - The basename index, or undefined when no citation needed one.
183
+ * @returns The resolved absolute path, or the reason the file did not resolve.
184
+ */
185
+ export function resolveFile(root, file, index) {
37
186
  const direct = resolveDirect(root, file);
38
187
  if (direct !== undefined)
39
188
  return { path: direct, reason: undefined };
@@ -47,7 +196,16 @@ function resolveFile(root, file, index) {
47
196
  return { path: undefined, reason: 'file does not exist' };
48
197
  return { path: undefined, reason: `ambiguous basename: ${candidates.join(', ')}` };
49
198
  }
50
- function judgeLines(path, line, end, counts) {
199
+ /**
200
+ * Judges whether a cited line or range lies within a file.
201
+ *
202
+ * @param path - The resolved file.
203
+ * @param line - The cited line.
204
+ * @param end - The end of a cited range, or undefined for a single line.
205
+ * @param counts - The line counts already read, by file.
206
+ * @returns The reason the line or range is outside the file, or undefined when it is inside.
207
+ */
208
+ export function judgeLines(path, line, end, counts) {
51
209
  const total = counts.get(path) ?? countLines(path);
52
210
  if (line < 1 || line > total)
53
211
  return `line ${line} is outside 1-${total}`;
@@ -55,8 +213,19 @@ function judgeLines(path, line, end, counts) {
55
213
  return `range end ${end} is outside ${line}-${total}`;
56
214
  return undefined;
57
215
  }
58
- function main(argv) {
59
- const missing = readMissingFlags(argv, ['--root']);
216
+ /**
217
+ * Runs the script on its arguments.
218
+ *
219
+ * @param argv - The arguments after the script path.
220
+ * @returns The exit code: 0 when every citation resolves, 3 when one does not, 64 on usage.
221
+ */
222
+ export function main(argv) {
223
+ const unknown = readUnknownFlags(argv, FLAGS);
224
+ if (unknown.length > 0) {
225
+ console.error(`cite: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
226
+ return 64;
227
+ }
228
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
60
229
  if (missing.length > 0) {
61
230
  console.error(`cite: ${missing.join(', ')} given with no value`);
62
231
  return 64;
@@ -64,19 +233,10 @@ function main(argv) {
64
233
  const root = readOption(argv, '--root') ?? process.cwd();
65
234
  const path = argv.find((argument, position) => !argument.startsWith('--') && argv[position - 1] !== '--root');
66
235
  if (path === undefined || !existsSync(path)) {
67
- console.error('usage: cite.ts PATH [--root DIR] [--json]');
236
+ console.error(USAGE);
68
237
  return 64;
69
238
  }
70
- const named = [...readFileSync(path, 'utf8').matchAll(CITATION)].flatMap((match) => match[1] === undefined
71
- ? []
72
- : [
73
- {
74
- citation: match[0],
75
- file: match[1],
76
- line: Number(match[2]),
77
- end: match[3] === undefined ? undefined : Number(match[3]),
78
- },
79
- ]);
239
+ const named = extractCitations(readFileSync(path, 'utf8'));
80
240
  const citations = named.length;
81
241
  // The basename index walks the root, so it is built once and only when a citation needs it.
82
242
  const index = named.some(({ file }) => needsIndex(root, file)) ? indexBasenames(root) : undefined;
@@ -105,4 +265,6 @@ function main(argv) {
105
265
  }
106
266
  return unresolved.length === 0 ? 0 : 3;
107
267
  }
108
- process.exitCode = main(process.argv.slice(2));
268
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
269
+ process.exitCode = main(process.argv.slice(2));
270
+ }