@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
@@ -1,65 +1,135 @@
1
1
  // Map terrain before a brief, an absorption lane, or a rename. Run from the checkout root:
2
2
  // node .agents/skills/orkestrel-scout/scripts/map.ts --tree PATH... [--headings PATH...] [--frontmatter PATH...] [--exports PATH...] [--census TERM...] [--refs PATH] [--anchors PATH...] [--paths a,b] [--ignore-case] [--json] [--out FILE]
3
3
  // --tree lists every file under each path with its line count and size. --headings lists every
4
- // Markdown heading with its line. --frontmatter lists every key of a Markdown frontmatter block, a
5
- // TOML file, or a YAML file with its whole value, a nested key dotted under its parents
6
- // (`interface.default_prompt`) and a TOML key under its table. --exports lists every `export`
7
- // declaration line in a TypeScript file (a text reading, not the compiler's). --census lists every line carrying a term. --refs lists
4
+ // Markdown heading with its line; a heading inside a fenced block is no heading, where a fence
5
+ // opens with a run of three or more backticks or tildes indented up to three spaces and closes with
6
+ // a run of the same character at least as long. --frontmatter lists every key of a Markdown
7
+ // frontmatter block, a TOML file, or a YAML file with its whole value, a nested key dotted under its
8
+ // parents (`interface.default_prompt`) and a TOML key under its table. --exports lists every
9
+ // `export` declaration line in a TypeScript file (a text reading, not the compiler's). --census
10
+ // lists every line carrying a term. --refs lists
8
11
  // every line naming a path or its basename. --anchors lists every `§ Heading` reference whose
9
12
  // heading the named file, or the same file, does not carry. Walks skip node_modules, .git, dist,
10
13
  // tmp, .orkestrel, .idea, and host.json; --paths narrows a census or refs walk. Modes combine, each
11
14
  // printing its section; --json prints one object; --out writes the output to FILE instead of the
12
15
  // terminal. Give each flag once; a later duplicate is not read. A value that opens with `--` is a
13
- // missing value, so census a custom property without its leading dashes. Exit 0, or 64 when no
14
- // mode is given or a flag is given with no value.
15
- import { existsSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
16
- import { basename, dirname } from 'node:path'
16
+ // missing value, so census a custom property without its leading dashes. The script refuses a flag
17
+ // it does not read, the `--flag=value` form included, and names every unknown flag and every flag
18
+ // given with no value in one refusal. Exit 0, or 64 when no mode is given, a flag is given with no
19
+ // value, or a flag is unknown.
17
20
  import {
18
- listFiles,
19
- normalizeSlashes,
20
- readList,
21
- readMissingFlags,
22
- readOption,
23
- } from '../../orkestrel-dispatch/scripts/helpers.ts'
24
-
25
- const EXPORT =
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ realpathSync,
26
+ statSync,
27
+ writeFileSync,
28
+ } from 'node:fs'
29
+ import { basename, dirname, join } from 'node:path'
30
+
31
+ /** Matches an `export` declaration line and captures its kind and name. */
32
+ export const EXPORT =
26
33
  /^export\s+(?:default\s+)?(?:async\s+)?(?:abstract\s+)?(function\*?|const|let|class|interface|type|enum|namespace)\s+([A-Za-z_$][\w$]*)/u
27
- const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/u
28
- const ANCHOR =
34
+
35
+ /** Matches a Markdown heading line and captures its marker and text. */
36
+ export const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/u
37
+
38
+ /** Matches a `§ Heading` reference, with the backticked Markdown path that may precede it. */
39
+ export const ANCHOR =
29
40
  /(?:`([^`\n]+\.md)`\s*)?§\s*"?([^"`\n|;,.)]+?)"?(?=\s*(?:[,.;)|]|\s(?:and|or|names|fixes|owns|states|binds|governs|decides|before|after)\b|$))/gu
30
- const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u
31
41
 
32
- interface TreeRow {
42
+ /** Matches the extensions of the binary files a walk reads as no text. */
43
+ export const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u
44
+
45
+ /** Matches a line that can open or close a code fence and captures its run of backticks or tildes. */
46
+ export const FENCE = /^ {0,3}(`{3,}|~{3,})/u
47
+
48
+ /** Names the directories a tree walk skips. */
49
+ export const SKIPPED_DIRECTORIES: ReadonlySet<string> = new Set([
50
+ 'node_modules',
51
+ '.git',
52
+ 'dist',
53
+ 'tmp',
54
+ '.orkestrel',
55
+ '.idea',
56
+ ])
57
+
58
+ /** Lists the flags that take a value. */
59
+ export const VALUE_FLAGS: readonly string[] = [
60
+ '--tree',
61
+ '--headings',
62
+ '--frontmatter',
63
+ '--exports',
64
+ '--census',
65
+ '--refs',
66
+ '--anchors',
67
+ '--paths',
68
+ '--out',
69
+ ]
70
+
71
+ /** Lists every flag the script reads, value flags and switches alike. */
72
+ export const FLAGS: readonly string[] = [...VALUE_FLAGS, '--ignore-case', '--json']
73
+
74
+ /** Holds the usage line printed when a call names no mode. */
75
+ export const USAGE =
76
+ 'usage: map.ts [--tree PATH...] [--headings PATH...] [--frontmatter PATH...] [--exports PATH...] [--census TERM...] [--refs PATH] [--anchors PATH...] [--paths a,b] [--ignore-case] [--json] [--out FILE]'
77
+
78
+ /** Describes one file a tree walk found, with the modification time it carries. */
79
+ export interface WalkedFile {
80
+ readonly path: string
81
+ readonly modified: number
82
+ }
83
+
84
+ /** Describes one file in a tree map: its path, its line count, and its size in bytes. */
85
+ export interface TreeRow {
33
86
  readonly path: string
34
87
  readonly lines: number
35
88
  readonly bytes: number
36
89
  }
37
90
 
38
- interface LineRow {
91
+ /** Describes one matched line: its file, its line number, and its text. */
92
+ export interface LineRow {
39
93
  readonly path: string
40
94
  readonly line: number
41
95
  readonly text: string
42
96
  }
43
97
 
44
- interface TermRows {
98
+ /** Describes the lines that carry one census term. */
99
+ export interface TermRows {
45
100
  readonly term: string
46
101
  readonly rows: readonly LineRow[]
47
102
  }
48
103
 
49
- interface AnchorRow {
104
+ /** Describes one `§` reference whose heading its target does not carry. */
105
+ export interface AnchorRow {
50
106
  readonly path: string
51
107
  readonly line: number
52
108
  readonly target: string
53
109
  readonly heading: string
54
110
  }
55
111
 
56
- interface TomlState {
112
+ /** Describes a line that can open or close a fence: its index and its run of fence characters. */
113
+ export interface FenceLine {
114
+ readonly index: number
115
+ readonly run: string
116
+ }
117
+
118
+ /** Describes the fence scan: the span of every closed fence and the fence still open, if any. */
119
+ export interface FenceState {
120
+ readonly open: FenceLine | undefined
121
+ readonly spans: ReadonlyArray<readonly [number, number]>
122
+ }
123
+
124
+ /** Describes the TOML reader's state: the current table, an open multi-line string, and the rows read. */
125
+ export interface TomlState {
57
126
  readonly table: string
58
127
  readonly fence: string | undefined
59
128
  readonly rows: readonly LineRow[]
60
129
  }
61
130
 
62
- interface YamlLine {
131
+ /** Describes one classified YAML line: its number, indent, content, key, and inline value. */
132
+ export interface YamlLine {
63
133
  readonly line: number
64
134
  readonly indent: number
65
135
  readonly content: string
@@ -67,18 +137,21 @@ interface YamlLine {
67
137
  readonly value: string
68
138
  }
69
139
 
70
- interface YamlHead {
140
+ /** Describes a YAML key line: its position among the classified lines, the line, and its dotted key. */
141
+ export interface YamlHead {
71
142
  readonly position: number
72
143
  readonly line: YamlLine
73
144
  readonly key: string
74
145
  }
75
146
 
76
- interface YamlState {
147
+ /** Describes the YAML reader's state: the stack of parent keys and the key lines read. */
148
+ export interface YamlState {
77
149
  readonly stack: ReadonlyArray<{ readonly indent: number; readonly key: string }>
78
150
  readonly heads: readonly YamlHead[]
79
151
  }
80
152
 
81
- interface Report {
153
+ /** Describes the whole map: one optional section per mode. */
154
+ export interface Report {
82
155
  readonly tree?: readonly TreeRow[]
83
156
  readonly headings?: readonly LineRow[]
84
157
  readonly frontmatter?: readonly LineRow[]
@@ -88,11 +161,133 @@ interface Report {
88
161
  readonly anchors?: readonly AnchorRow[]
89
162
  }
90
163
 
91
- function isText(path: string): boolean {
164
+ /**
165
+ * Reads the value that follows a flag.
166
+ *
167
+ * @remarks
168
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
169
+ * and the `--flag=value` form is not read.
170
+ *
171
+ * @param argv - The arguments after the script path.
172
+ * @param name - The flag, such as `--paths`.
173
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
174
+ * another flag.
175
+ */
176
+ export function readOption(argv: readonly string[], name: string): string | undefined {
177
+ const index = argv.indexOf(name)
178
+ const value = index === -1 ? undefined : argv[index + 1]
179
+ return value === undefined || value.startsWith('--') ? undefined : value
180
+ }
181
+
182
+ /**
183
+ * Names the value flags a caller gave with no value.
184
+ *
185
+ * @param argv - The arguments after the script path.
186
+ * @param names - The flags that take a value.
187
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
188
+ */
189
+ export function readMissingFlags(
190
+ argv: readonly string[],
191
+ names: readonly string[],
192
+ ): readonly string[] {
193
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
194
+ }
195
+
196
+ /**
197
+ * Names the flags a caller gave that the script does not read.
198
+ *
199
+ * @remarks
200
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
201
+ * scan.
202
+ *
203
+ * @param argv - The arguments after the script path.
204
+ * @param flags - Every flag the script reads, value flags and switches alike.
205
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
206
+ */
207
+ export function readUnknownFlags(
208
+ argv: readonly string[],
209
+ flags: readonly string[],
210
+ ): readonly string[] {
211
+ const end = argv.indexOf('--')
212
+ return (end === -1 ? argv : argv.slice(0, end)).filter(
213
+ (argument) => argument.startsWith('--') && !flags.includes(argument),
214
+ )
215
+ }
216
+
217
+ /**
218
+ * Reads the values that follow a flag up to the next flag.
219
+ *
220
+ * @param argv - The arguments after the script path.
221
+ * @param name - The flag, such as `--tree`.
222
+ * @returns The values between the flag and the next `--` argument, empty when the flag is absent.
223
+ */
224
+ export function readList(argv: readonly string[], name: string): readonly string[] {
225
+ const index = argv.indexOf(name)
226
+ if (index === -1) return []
227
+ const rest = argv.slice(index + 1)
228
+ const end = rest.findIndex((argument) => argument.startsWith('--'))
229
+ return end === -1 ? rest : rest.slice(0, end)
230
+ }
231
+
232
+ /**
233
+ * Normalizes a host path to forward slashes.
234
+ *
235
+ * @param path - A host path.
236
+ * @returns The same path with every backslash replaced by a slash.
237
+ */
238
+ export function normalizeSlashes(path: string): string {
239
+ return path.split('\\').join('/')
240
+ }
241
+
242
+ /**
243
+ * Lists every regular file beneath a directory, skipping the directories the script never reads.
244
+ *
245
+ * @remarks
246
+ * A skipped name matches a directory at any depth, exactly and case-sensitively.
247
+ *
248
+ * @param directory - The directory to walk.
249
+ * @param skipped - The directory names to skip. Default: the skipped directories.
250
+ * @returns Each file's forward-slash path and modification time, in walk order.
251
+ * @throws Thrown when `directory` does not exist or cannot be read.
252
+ */
253
+ export function listFiles(
254
+ directory: string,
255
+ skipped: ReadonlySet<string> = SKIPPED_DIRECTORIES,
256
+ ): readonly WalkedFile[] {
257
+ const files: WalkedFile[] = []
258
+ const pending = [directory]
259
+ while (pending.length > 0) {
260
+ const current = pending.pop()
261
+ if (current === undefined) break
262
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
263
+ if (skipped.has(entry.name)) continue
264
+ const path = join(current, entry.name)
265
+ if (entry.isDirectory()) pending.push(path)
266
+ else if (entry.isFile()) {
267
+ files.push({ path: normalizeSlashes(path), modified: statSync(path).mtimeMs })
268
+ }
269
+ }
270
+ }
271
+ return files
272
+ }
273
+
274
+ /**
275
+ * Checks whether a walk reads a file as text.
276
+ *
277
+ * @param path - The file path.
278
+ * @returns True if the path carries no binary extension and is not `host.json`; false otherwise.
279
+ */
280
+ export function isText(path: string): boolean {
92
281
  return !BINARY.test(path) && basename(path) !== 'host.json'
93
282
  }
94
283
 
95
- function expandPaths(paths: readonly string[]): readonly string[] {
284
+ /**
285
+ * Expands paths into the files they name.
286
+ *
287
+ * @param paths - Files and directories; an absent path names nothing.
288
+ * @returns Each file, a directory expanded to the files beneath it.
289
+ */
290
+ export function expandPaths(paths: readonly string[]): readonly string[] {
96
291
  return paths.flatMap((path) => {
97
292
  if (!existsSync(path)) return []
98
293
  if (statSync(path).isDirectory()) return listFiles(path).map((file) => file.path)
@@ -100,11 +295,23 @@ function expandPaths(paths: readonly string[]): readonly string[] {
100
295
  })
101
296
  }
102
297
 
103
- function readLines(path: string): readonly string[] {
298
+ /**
299
+ * Reads a file as lines.
300
+ *
301
+ * @param path - The file to read.
302
+ * @returns The lines, split on either line ending.
303
+ */
304
+ export function readLines(path: string): readonly string[] {
104
305
  return readFileSync(path, 'utf8').split(/\r\n|\n/)
105
306
  }
106
307
 
107
- function mapTree(paths: readonly string[]): readonly TreeRow[] {
308
+ /**
309
+ * Maps the files beneath paths with their line counts and sizes.
310
+ *
311
+ * @param paths - Files and directories.
312
+ * @returns A row per text file, sorted by path.
313
+ */
314
+ export function mapTree(paths: readonly string[]): readonly TreeRow[] {
108
315
  return expandPaths(paths)
109
316
  .filter(isText)
110
317
  .map((path) => {
@@ -118,13 +325,55 @@ function mapTree(paths: readonly string[]): readonly TreeRow[] {
118
325
  .sort((left, right) => left.path.localeCompare(right.path))
119
326
  }
120
327
 
121
- function scanFences(lines: readonly string[]): readonly boolean[] {
122
- const fenced: boolean[] = []
123
- for (const text of lines) fenced.push((fenced.at(-1) ?? false) !== text.startsWith('```'))
124
- return fenced
328
+ /**
329
+ * Finds the lines that can open or close a code fence.
330
+ *
331
+ * @param lines - The lines of a Markdown file.
332
+ * @returns Each line that opens with up to three spaces and a run of three or more backticks or tildes.
333
+ */
334
+ export function findFenceLines(lines: readonly string[]): readonly FenceLine[] {
335
+ return lines.flatMap((text, index) => {
336
+ const run = text.match(FENCE)?.[1]
337
+ return run === undefined ? [] : [{ index, run }]
338
+ })
339
+ }
340
+
341
+ /**
342
+ * Marks the lines that sit inside a code fence.
343
+ *
344
+ * @remarks
345
+ * A fence opens on a run of three or more backticks or tildes, indented up to three spaces, and
346
+ * closes on a run of the same character at least as long; a fence-like line inside an open fence
347
+ * that does not close it is content, and a fence the file never closes runs to its end.
348
+ *
349
+ * @param lines - The lines of a Markdown file.
350
+ * @returns One flag per line: true for the fence lines and the lines between them.
351
+ */
352
+ export function scanFences(lines: readonly string[]): readonly boolean[] {
353
+ const { open, spans } = findFenceLines(lines).reduce<FenceState>(
354
+ (state, fence) => {
355
+ if (state.open === undefined) return { open: fence, spans: state.spans }
356
+ const closes =
357
+ fence.run.startsWith(state.open.run.charAt(0)) && fence.run.length >= state.open.run.length
358
+ return closes
359
+ ? { open: undefined, spans: [...state.spans, [state.open.index, fence.index]] }
360
+ : state
361
+ },
362
+ { open: undefined, spans: [] },
363
+ )
364
+ const all = open === undefined ? spans : [...spans, [open.index, lines.length - 1] as const]
365
+ return [...lines.keys()].map((index) =>
366
+ all.some(([first, last]) => index >= first && index <= last),
367
+ )
125
368
  }
126
369
 
127
- function mapHeadings(paths: readonly string[]): readonly LineRow[] {
370
+ /**
371
+ * Maps the Markdown headings outside code fences.
372
+ *
373
+ * @param paths - Files and directories; only Markdown files are read.
374
+ * @returns A row per heading, its text the marker and the heading text.
375
+ */
376
+ export function mapHeadings(paths: readonly string[]): readonly LineRow[] {
128
377
  return expandPaths(paths)
129
378
  .filter((candidate) => candidate.endsWith('.md'))
130
379
  .flatMap((path) => {
@@ -140,7 +389,13 @@ function mapHeadings(paths: readonly string[]): readonly LineRow[] {
140
389
  })
141
390
  }
142
391
 
143
- function mapExports(paths: readonly string[]): readonly LineRow[] {
392
+ /**
393
+ * Maps the `export` declaration lines of TypeScript files.
394
+ *
395
+ * @param paths - Files and directories; only TypeScript files are read.
396
+ * @returns A row per declaration, its text the kind and name, or the whole `export * from` line.
397
+ */
398
+ export function mapExports(paths: readonly string[]): readonly LineRow[] {
144
399
  return expandPaths(paths)
145
400
  .filter((candidate) => /\.[cm]?ts$/u.test(candidate))
146
401
  .flatMap((path) =>
@@ -156,12 +411,27 @@ function mapExports(paths: readonly string[]): readonly LineRow[] {
156
411
  )
157
412
  }
158
413
 
159
- function buildMatcher(term: string, ignoreCase: boolean): (text: string) => boolean {
414
+ /**
415
+ * Builds the predicate a census applies to a line.
416
+ *
417
+ * @param term - The term to find.
418
+ * @param ignoreCase - If true, compares without regard to letter case; if false, compares exactly.
419
+ * @returns A predicate that is true for a line carrying the term.
420
+ */
421
+ export function buildMatcher(term: string, ignoreCase: boolean): (text: string) => boolean {
160
422
  const needle = ignoreCase ? term.toLowerCase() : term
161
423
  return (text) => (ignoreCase ? text.toLowerCase() : text).includes(needle)
162
424
  }
163
425
 
164
- function mapCensus(
426
+ /**
427
+ * Maps the lines that carry each census term.
428
+ *
429
+ * @param terms - The terms to find.
430
+ * @param roots - The files and directories to read.
431
+ * @param ignoreCase - If true, compares without regard to letter case; if false, compares exactly.
432
+ * @returns The matching lines per term, each line's text trimmed and cut to its first 160 characters.
433
+ */
434
+ export function mapCensus(
165
435
  terms: readonly string[],
166
436
  roots: readonly string[],
167
437
  ignoreCase: boolean,
@@ -180,7 +450,14 @@ function mapCensus(
180
450
  })
181
451
  }
182
452
 
183
- function mapRefs(target: string, roots: readonly string[]): readonly LineRow[] {
453
+ /**
454
+ * Maps the lines that name a path or its basename.
455
+ *
456
+ * @param target - The path to find.
457
+ * @param roots - The files and directories to read; the target file itself is left out.
458
+ * @returns The lines that carry the path or its basename.
459
+ */
460
+ export function mapRefs(target: string, roots: readonly string[]): readonly LineRow[] {
184
461
  const needle = normalizeSlashes(target)
185
462
  const name = basename(needle)
186
463
  return expandPaths(roots)
@@ -195,7 +472,14 @@ function mapRefs(target: string, roots: readonly string[]): readonly LineRow[] {
195
472
  )
196
473
  }
197
474
 
198
- function classifyYamlLines(lines: readonly string[], offset: number): readonly YamlLine[] {
475
+ /**
476
+ * Classifies the lines of a YAML body.
477
+ *
478
+ * @param lines - The body lines.
479
+ * @param offset - The line number of the first body line.
480
+ * @returns Each non-blank, non-comment line with its indent, content, key, and inline value.
481
+ */
482
+ export function classifyYamlLines(lines: readonly string[], offset: number): readonly YamlLine[] {
199
483
  return lines.flatMap((text, index): readonly YamlLine[] => {
200
484
  const content = text.trimStart()
201
485
  if (content === '' || content.startsWith('#')) return []
@@ -212,7 +496,15 @@ function classifyYamlLines(lines: readonly string[], offset: number): readonly Y
212
496
  })
213
497
  }
214
498
 
215
- function stepYamlState(state: YamlState, line: YamlLine, position: number): YamlState {
499
+ /**
500
+ * Steps the YAML reader over one classified line.
501
+ *
502
+ * @param state - The reader's state before the line.
503
+ * @param line - The classified line.
504
+ * @param position - The line's position among the classified lines.
505
+ * @returns The state after the line; a line with no key leaves it unchanged.
506
+ */
507
+ export function stepYamlState(state: YamlState, line: YamlLine, position: number): YamlState {
216
508
  if (line.key === undefined) return state
217
509
  // Indents strictly increase along the stack, so the entries at or past this indent are the tail.
218
510
  const stack = state.stack.filter((entry) => entry.indent < line.indent)
@@ -223,7 +515,20 @@ function stepYamlState(state: YamlState, line: YamlLine, position: number): Yaml
223
515
  }
224
516
  }
225
517
 
226
- function readYamlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
518
+ /**
519
+ * Reads every key of a YAML body with its whole value.
520
+ *
521
+ * @param path - The file the body came from.
522
+ * @param lines - The body lines.
523
+ * @param offset - The line number of the first body line.
524
+ * @returns A row per key, a nested key dotted under its parents and its value extended by the
525
+ * list items and wrapped lines indented under it.
526
+ */
527
+ export function readYamlKeys(
528
+ path: string,
529
+ lines: readonly string[],
530
+ offset: number,
531
+ ): readonly LineRow[] {
227
532
  const classified = classifyYamlLines(lines, offset)
228
533
  const { heads } = classified.reduce<YamlState>(
229
534
  (state, line, position) => stepYamlState(state, line, position),
@@ -246,7 +551,21 @@ function readYamlKeys(path: string, lines: readonly string[], offset: number): r
246
551
  }))
247
552
  }
248
553
 
249
- function stepTomlState(state: TomlState, text: string, path: string, line: number): TomlState {
554
+ /**
555
+ * Steps the TOML reader over one line.
556
+ *
557
+ * @param state - The reader's state before the line.
558
+ * @param text - The line.
559
+ * @param path - The file the line came from.
560
+ * @param line - The line's number.
561
+ * @returns The state after the line, with a row added for a key and the table set for a header.
562
+ */
563
+ export function stepTomlState(
564
+ state: TomlState,
565
+ text: string,
566
+ path: string,
567
+ line: number,
568
+ ): TomlState {
250
569
  if (state.fence !== undefined) {
251
570
  // The body of a multi-line string carries no key.
252
571
  return text.includes(state.fence) ? { ...state, fence: undefined } : state
@@ -256,10 +575,10 @@ function stepTomlState(state: TomlState, text: string, path: string, line: numbe
256
575
  const match = text.match(/^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.*)$/u)
257
576
  if (match?.[1] === undefined) return state
258
577
  const value = (match[2] ?? '').trim()
259
- const opening = value.match(/^("""|''')/u)?.[1]
578
+ const marker = value.match(/^("""|''')/u)?.[1]
260
579
  return {
261
580
  table: state.table,
262
- fence: opening !== undefined && value.indexOf(opening, 3) === -1 ? opening : undefined,
581
+ fence: marker !== undefined && value.indexOf(marker, 3) === -1 ? marker : undefined,
263
582
  rows: [
264
583
  ...state.rows,
265
584
  {
@@ -271,14 +590,32 @@ function stepTomlState(state: TomlState, text: string, path: string, line: numbe
271
590
  }
272
591
  }
273
592
 
274
- function readTomlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
593
+ /**
594
+ * Reads every key of a TOML body with its value.
595
+ *
596
+ * @param path - The file the body came from.
597
+ * @param lines - The body lines.
598
+ * @param offset - The line number of the first body line.
599
+ * @returns A row per key, a key under a table dotted beneath it; a multi-line string's body is no key.
600
+ */
601
+ export function readTomlKeys(
602
+ path: string,
603
+ lines: readonly string[],
604
+ offset: number,
605
+ ): readonly LineRow[] {
275
606
  return lines.reduce<TomlState>(
276
607
  (state, text, index) => stepTomlState(state, text, path, index + offset),
277
608
  { table: '', fence: undefined, rows: [] },
278
609
  ).rows
279
610
  }
280
611
 
281
- function mapFrontmatter(paths: readonly string[]): readonly LineRow[] {
612
+ /**
613
+ * Maps the keys of Markdown frontmatter, TOML, and YAML files.
614
+ *
615
+ * @param paths - Files and directories; only Markdown, MDC, TOML, and YAML files are read.
616
+ * @returns A row per key; a Markdown file without a closed frontmatter block gives none.
617
+ */
618
+ export function mapFrontmatter(paths: readonly string[]): readonly LineRow[] {
282
619
  return expandPaths(paths)
283
620
  .filter((candidate) => /\.(md|mdc|toml|ya?ml)$/u.test(candidate))
284
621
  .flatMap((path) => {
@@ -294,11 +631,24 @@ function mapFrontmatter(paths: readonly string[]): readonly LineRow[] {
294
631
  })
295
632
  }
296
633
 
297
- function readHeadingTexts(path: string): ReadonlySet<string> {
634
+ /**
635
+ * Reads the heading texts of a Markdown file.
636
+ *
637
+ * @param path - The Markdown file.
638
+ * @returns Each heading's text without its marker, lowercased.
639
+ */
640
+ export function readHeadingTexts(path: string): ReadonlySet<string> {
298
641
  return new Set(mapHeadings([path]).map((row) => row.text.replace(/^#+\s+/u, '').toLowerCase()))
299
642
  }
300
643
 
301
- function mapAnchors(paths: readonly string[]): readonly AnchorRow[] {
644
+ /**
645
+ * Maps the `§ Heading` references whose heading their target does not carry.
646
+ *
647
+ * @param paths - Files and directories; only Markdown files are read.
648
+ * @returns A row per unresolved reference. A reference with no backticked path before it targets
649
+ * the file that carries it, and a reference whose target file is absent is unresolved.
650
+ */
651
+ export function mapAnchors(paths: readonly string[]): readonly AnchorRow[] {
302
652
  const citations = expandPaths(paths)
303
653
  .filter((candidate) => candidate.endsWith('.md'))
304
654
  .flatMap((path) =>
@@ -321,7 +671,13 @@ function mapAnchors(paths: readonly string[]): readonly AnchorRow[] {
321
671
  )
322
672
  }
323
673
 
324
- function renderReport(report: Report): string {
674
+ /**
675
+ * Renders a map as text.
676
+ *
677
+ * @param report - The map.
678
+ * @returns One section per mode the map carries, each opened by a `##` heading, ending in a line feed.
679
+ */
680
+ export function renderReport(report: Report): string {
325
681
  const { tree, headings, frontmatter, exports, census, refs, anchors } = report
326
682
  const lines = [
327
683
  ...(tree === undefined
@@ -360,20 +716,22 @@ function renderReport(report: Report): string {
360
716
  return `${lines.join('\n')}\n`
361
717
  }
362
718
 
363
- function main(argv: readonly string[]): number {
364
- const missing = readMissingFlags(argv, [
365
- '--tree',
366
- '--headings',
367
- '--frontmatter',
368
- '--exports',
369
- '--census',
370
- '--refs',
371
- '--anchors',
372
- '--paths',
373
- '--out',
374
- ])
375
- if (missing.length > 0) {
376
- console.error(`map: ${missing.join(', ')} given with no value`)
719
+ /**
720
+ * Runs the script on its arguments.
721
+ *
722
+ * @param argv - The arguments after the script path.
723
+ * @returns The exit code: 0 on success, 64 on usage.
724
+ */
725
+ export function main(argv: readonly string[]): number {
726
+ const unknown = readUnknownFlags(argv, FLAGS)
727
+ const missing = readMissingFlags(argv, VALUE_FLAGS)
728
+ if (unknown.length > 0 || missing.length > 0) {
729
+ console.error(
730
+ `map: ${[
731
+ ...(unknown.length > 0 ? [`${unknown.join(', ')} is not a flag this script reads`] : []),
732
+ ...(missing.length > 0 ? [`${missing.join(', ')} given with no value`] : []),
733
+ ].join('; ')}; ${USAGE}`,
734
+ )
377
735
  return 64
378
736
  }
379
737
  const roots = (readOption(argv, '--paths') ?? '.').split(',')
@@ -395,9 +753,7 @@ function main(argv: readonly string[]): number {
395
753
  0 &&
396
754
  refs === undefined
397
755
  ) {
398
- console.error(
399
- 'usage: map.ts [--tree PATH...] [--headings PATH...] [--frontmatter PATH...] [--exports PATH...] [--census TERM...] [--refs PATH] [--anchors PATH...] [--paths a,b] [--ignore-case] [--json] [--out FILE]',
400
- )
756
+ console.error(USAGE)
401
757
  return 64
402
758
  }
403
759
  const report: Report = {
@@ -420,4 +776,6 @@ function main(argv: readonly string[]): number {
420
776
  return 0
421
777
  }
422
778
 
423
- process.exitCode = main(process.argv.slice(2))
779
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
780
+ process.exitCode = main(process.argv.slice(2))
781
+ }