@descent-vtt/spec-harness 0.18.0 → 0.19.0

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.
package/dist/usage.js CHANGED
@@ -13,14 +13,20 @@
13
13
  * `null`. The one thing only the disk knows, whether `--root` is a
14
14
  * directory, is asked of the caller.
15
15
  */
16
+ import { resolve } from 'node:path';
17
+ import { sameId } from './branch.js';
16
18
  import { literal } from './literal.js';
17
19
  /** Whether a value names nothing: empty, or only space. */
18
20
  export function isBlank(value) {
19
21
  return value.trim() === '';
20
22
  }
21
- // An option left out has no value to refuse, and a value that names something
22
- // is passed over as none is: the empty list is equivalent to its mutant.
23
- const one = (value) => (value === undefined ? [] : [value]);
23
+ // Every value an option that takes one was given, so that an empty one is
24
+ // refused wherever it stands: `--root "" --root .` names one directory twice
25
+ // as the root is read, and the empty value is still the unset variable it
26
+ // most often is. An option left out has no value to refuse, and a value that
27
+ // names something is passed over as none is: the empty list is equivalent
28
+ // to its mutant.
29
+ const every = (option) => (line) => line.written[option] ?? [];
24
30
  /**
25
31
  * The options that take a value, and what an empty one fails to name.
26
32
  * `--format` and `--at` are not here: each takes one of a few words, and the
@@ -28,16 +34,50 @@ const one = (value) => (value === undefined ? [] : [value]);
28
34
  * `rule` refuses by name itself.
29
35
  */
30
36
  const VALUED = literal({
31
- brief: { of: (line) => one(line.brief), says: "names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names" },
32
- base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },
33
- root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },
37
+ brief: { of: every('brief'), says: "names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names" },
38
+ base: { of: every('base'), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },
39
+ root: { of: every('root'), says: 'names no directory; give it one, or leave it out to run in the current directory' },
34
40
  path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },
35
- reason: { of: (line) => one(line.reason), says: 'gives no reason; say why the round cannot be done without the file' },
41
+ reason: { of: every('reason'), says: 'gives no reason; say why the round cannot be done without the file' },
36
42
  option: { of: (line) => line.options, says: 'names no choice; give it "<label>: <cost>", or leave it out' },
37
- recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },
38
- show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },
39
- id: { of: (line) => one(line.id), says: "names no probe; give it a probe's id, or leave it out to run every probe" },
43
+ recommend: { of: every('recommend'), says: 'recommends nothing; say which option and why, or leave it out' },
44
+ show: { of: every('show'), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },
45
+ id: { of: every('id'), says: "names no probe; give it a probe's id, or leave it out to run every probe" },
40
46
  });
47
+ /**
48
+ * Whether two values of an option that takes one ask for one thing, as the
49
+ * option reads them: a brief by its id as spec-brief's names are matched,
50
+ * space around it dropped and `12` the brief `012` (ADR-0004); a root by the
51
+ * directory it names from `cwd`, the one an empty root names on the hook's
52
+ * line among them; any other as it is written, since each is read so, a
53
+ * format, a probe's position, an id or text.
54
+ */
55
+ function asksOne(option, first, other, cwd) {
56
+ if (option === 'brief')
57
+ return sameId(first.trim(), other.trim());
58
+ if (option === 'root')
59
+ return resolve(cwd, first) === resolve(cwd, other);
60
+ return first === other;
61
+ }
62
+ /**
63
+ * The first option that takes one value and was given two that read as two,
64
+ * named with both as they were written; `null` where none was. The parser
65
+ * keeps the last, and the earlier went unread with no word of it:
66
+ * `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001`
67
+ * was brief 001 (spec-core ADR-0005). The same value given twice, however it
68
+ * is spelled, is the option once, and the last is the one read. `cwd` is
69
+ * where a relative root is read from. A list, `--path` and `--option`, has
70
+ * every value it is given.
71
+ */
72
+ export function twoValues(line, cwd) {
73
+ for (const [option, values] of Object.entries(line.written)) {
74
+ const first = values[0];
75
+ const other = values.find((value) => !asksOne(option, first, value, cwd));
76
+ if (other !== undefined)
77
+ return `--${option} is given twice, as ${JSON.stringify(first)} and as ${JSON.stringify(other)}, and one of the two would go unread`;
78
+ }
79
+ return null;
80
+ }
41
81
  /**
42
82
  * The line for a value that names nothing. `subject` is the input as its
43
83
  * caller knows it: `--brief` on a command line, `"brief"` in a tool call. The
@@ -92,6 +132,8 @@ const SHAPE = literal({
92
132
  // doctor's report.
93
133
  doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },
94
134
  });
135
+ /** The options every command answers instead of running, which no shape lists. */
136
+ const ANSWERS = ['help', 'version'];
95
137
  // A map, since a command is a word a person types and an object answers to
96
138
  // more names than it was given: `constructor` has no shape.
97
139
  const SHAPES = new Map(Object.entries(SHAPE));
@@ -133,9 +175,11 @@ export function heldHookLine(wrong) {
133
175
  }
134
176
  /**
135
177
  * A line the parser refused: an option no command has, an option left
136
- * without its value, a `--format` or an `--at` that is none. The parser's
137
- * words are kept, on one line: what node says of an option that may have
138
- * lost its value is three.
178
+ * without its value, a `--format` or an `--at` that is none, `--help` beside
179
+ * `--version`; or a line it read with two values for an option that takes
180
+ * one, of which it cannot tell the one meant ({@link twoValues}). The
181
+ * parser's words are kept, on one line: what node says of an option that may
182
+ * have lost its value is three.
139
183
  */
140
184
  export function unreadHookLine(said) {
141
185
  return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\n').join(' ')})`, mend: 'mend it in Claude Code\'s settings, where init writes "hook claude" and nothing after it' } };
@@ -215,6 +259,9 @@ export function holderHookLine(line, tree, path) {
215
259
  * A line that guards says which brief it names, where it names one: the hook
216
260
  * passes over a `--brief` that is empty, as every release has, and holds
217
261
  * every write under one that names no brief, which only spec-brief can tell.
262
+ * A line with two values for an option that takes one is a line the hook
263
+ * cannot read, whichever option it is, read from `cwd` as the hook reads it
264
+ * from where it is started (cli.ts).
218
265
  */
219
266
  export function claudeHookLine(words, parse, cwd, isDirectory) {
220
267
  let line;
@@ -227,6 +274,9 @@ export function claudeHookLine(words, parse, cwd, isDirectory) {
227
274
  }
228
275
  if (!isClaudeHook(line.command, line.positionals[0]))
229
276
  return null;
277
+ const twice = twoValues(line, cwd);
278
+ if (twice !== null)
279
+ return unreadHookLine(twice);
230
280
  const read = readHookLine(line.root, cwd, isDirectory);
231
281
  return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;
232
282
  }
@@ -234,9 +284,10 @@ export function claudeHookLine(words, parse, cwd, isDirectory) {
234
284
  * Why the command line is refused, or `null` when the command can run:
235
285
  * an option it does not read, alone or beside another, an argument it does
236
286
  * not take, a brief named twice, a value or an argument that names nothing,
237
- * a `--root` that is no directory. Asked once a command is known and neither
238
- * `--help` nor `--version` answers instead; a command with no shape is none,
239
- * which its caller says.
287
+ * a `--root` that is no directory. Asked once a command is known, and before
288
+ * `--help` or `--version` answers: a line that is wrong is refused with
289
+ * either on it, as one the parser refuses always was (spec-core ADR-0005).
290
+ * A command with no shape is none, which its caller says.
240
291
  */
241
292
  export function refusal(line, isDirectory) {
242
293
  const { command, positionals } = line;
@@ -247,15 +298,18 @@ export function refusal(line, isDirectory) {
247
298
  const shape = SHAPES.get(command);
248
299
  if (shape === undefined || isClaudeHook(command, positionals[0]))
249
300
  return null;
250
- const stray = line.given.find((option) => !shape.options.includes(option));
301
+ // What the line asks the command, beside the help or the version it may
302
+ // ask for instead, which every command answers.
303
+ const asked = line.given.filter((option) => !ANSWERS.includes(option));
304
+ const stray = asked.find((option) => !shape.options.includes(option));
251
305
  if (stray !== undefined)
252
306
  return `${command} does not take --${stray}; ${listed(shape.options)}`;
253
307
  // An option the command reads, beside one that leaves it unread:
254
308
  // `escalate --list --path src/a.ts` printed the list, exit 0, and the
255
309
  // request it also spelled out was never raised.
256
- const mode = shape.modes?.find((candidate) => line.given.includes(candidate.option));
310
+ const mode = shape.modes?.find((candidate) => asked.includes(candidate.option));
257
311
  if (mode !== undefined) {
258
- const unread = line.given.find((option) => option !== mode.option && !mode.beside.includes(option));
312
+ const unread = asked.find((option) => option !== mode.option && !mode.beside.includes(option));
259
313
  if (unread !== undefined)
260
314
  return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;
261
315
  }
package/dist/usage.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\nimport { literal } from './literal.js';\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// An option left out has no value to refuse, and a value that names something\n// is passed over as none is: the empty list is equivalent to its mutant.\nconst one = (value: string | undefined): readonly string[] => (value === undefined ? [] : [value]);\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = literal({\n brief: { of: (line) => one(line.brief), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: (line) => one(line.reason), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: (line) => one(line.id), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n}) satisfies Record<string, Valued>;\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });\nconst ONE_BRIEF = literal({\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n});\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = literal({\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n}) satisfies Record<string, Shape>;\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)!}`;\n}\n\n/**\n * Whether a line is Claude Code's hook, by its command and the argument\n * after it. It is exempt from every refusal here, and read as each release\n * since 0.1.0 read it: the line lives in settings files and in the plugin of\n * every release, which this one cannot rewrite, and exit 2 from it holds\n * every write of a session, the write that would fix the line among them.\n * A line the parser cannot read is asked the same, and the hook says it\n * with exit 1 (cli.ts).\n */\nexport function isClaudeHook(command: string | undefined, argument: string | undefined): boolean {\n return command === 'hook' && argument === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/** What is wrong with the line of Claude Code's hook, and what mends it. */\nexport interface LineFault {\n readonly wrong: string;\n readonly mend: string;\n}\n\n/**\n * What Claude Code's hook does with its own line, as far as the line decides\n * it: it guards, in the root the line gives it where it gives one and under\n * the brief it names where it names one; or the guard is off, for a fault\n * the hook says with exit 1 and holds no write for; or every write is held,\n * exit 2, for a brief the line names that is not one (ADR-0005). The hook\n * reads its line through the functions below, and `doctor` reads a line it\n * finds in Claude Code's settings through the same ones, so what it says of\n * a line is what the hook does with it.\n */\nexport type HookLine = Guards | GuardOff | WritesHeld;\n\nexport interface Guards {\n readonly state: 'guards';\n readonly root: string | undefined;\n /**\n * The brief the line names with `--brief`, as written. Whether it is a\n * brief is spec-brief's to say, which the caller asks. Absent where the\n * line names none.\n */\n readonly brief?: string;\n /**\n * The work tree the root is in, where that is not the project's own and\n * holds it: the hook opens the work tree its root is in, so a write to a\n * file of the project is checked there, by that work tree's briefs, under\n * the path the file has in it. Only git can tell, so the caller asks it.\n * Absent where the line gives no root, or one in the project's work tree.\n */\n readonly holder?: OtherTree & { readonly path: string };\n}\n\n/** A work tree the root on the hook's line is in that is not the project's own. */\nexport interface OtherTree {\n /** Its top, as git names it. */\n readonly top: string;\n /** Whether it is a work tree of the project's own repository, linked or main, where it is otherwise another repository's. */\n readonly ours: boolean;\n}\n\n/** How such a work tree is named: by its top, and whose it is. */\nexport function namedTree({ top, ours }: OtherTree): string {\n return `${top}, ${ours ? 'another work tree of this repository' : 'the work tree of another repository'}`;\n}\n\nexport interface GuardOff {\n readonly state: 'off';\n readonly fault: LineFault;\n /**\n * Set where the hook says nothing of the fault and exits 0, so that no\n * run of it fails: a root outside any work tree, or in a work tree that\n * does not hold the project. Every other fault it says, with exit 1.\n */\n readonly silent?: true;\n /**\n * What goes unchecked, where that is not every write: under a root in a\n * work tree that does not hold the project, the hook checks a write to a\n * file of that work tree, and none to a file of the project.\n */\n readonly unchecked?: string;\n}\n\nexport interface WritesHeld {\n readonly state: 'held';\n /** What is said of the name, by what named the brief (workspace.ts). */\n readonly wrong: string;\n}\n\n/**\n * A line under which the round has no brief to be checked against, since\n * what names one names a brief spec-brief does not know, or one it has\n * archived: the hook holds the write, exit 2, and every write after it until\n * the name is mended (ADR-0004). `wrong` is what is said of the name.\n */\nexport function heldHookLine(wrong: string): WritesHeld {\n return { state: 'held', wrong };\n}\n\n/**\n * A line the parser refused: an option no command has, an option left\n * without its value, a `--format` or an `--at` that is none. The parser's\n * words are kept, on one line: what node says of an option that may have\n * lost its value is three.\n */\nexport function unreadHookLine(said: string): GuardOff {\n return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\\n').join(' ')})`, mend: 'mend it in Claude Code\\'s settings, where init writes \"hook claude\" and nothing after it' } };\n}\n\n/**\n * A line that parsed, by the root it gives. An empty root is the directory\n * the hook was started in: git starts there when it is handed no directory,\n * so the hook has guarded there for an empty root in every release. A root\n * that is a file, or is not there, is one git starts in neither of, which\n * read as a session outside a work tree, exit 0 and no word, and the guard\n * someone had configured was off.\n */\nexport function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff {\n const given = root === '' ? cwd : root;\n if (given !== undefined && !isDirectory(given)) {\n return { state: 'off', fault: { wrong: noDirectory(given), mend: \"give the hook's line a directory that exists, or leave --root out for the project Claude Code names\" } };\n }\n return { state: 'guards', root: given };\n}\n\n/**\n * A line whose root is a directory outside any git work tree. The hook\n * reads that as a session outside one, where it has nothing to guard: it\n * checks no write and says nothing, exit 0 (ADR-0005), and that stays. Only\n * git can tell such a root, so the caller asks it. A line that carries one\n * is a guard someone set up that checks nothing, and `doctor` says it as\n * off.\n */\nexport function outsideHookLine(root: string): GuardOff {\n return {\n state: 'off',\n silent: true,\n fault: {\n wrong: `--root is \"${root}\", a directory outside any git work tree, which the hook reads as a session outside one, exit 0 and no word`,\n mend: \"give the hook's line a directory in the work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line whose root is in a git work tree that does not hold the project:\n * another repository beside it, or a work tree inside it, a repository of\n * its own, a submodule or a linked worktree. The hook opens the work tree\n * its root is in and decides by that work tree's briefs. A file of the\n * project is outside it, which the hook reads as a file outside the\n * repository: it says nothing, exit 0, as it has in every release, and that\n * stays. So no write to a file of the project is checked under the line,\n * and `doctor` says it as a guard that is off. Only git can tell such a\n * root, so the caller asks it.\n */\nexport function elsewhereHookLine(root: string, tree: OtherTree): GuardOff {\n return {\n state: 'off',\n silent: true,\n unchecked: 'no write to a file of this project is checked',\n fault: {\n wrong: `--root is \"${root}\", a directory in ${namedTree(tree)}, which does not hold this project: the hook checks a write to a file of that work tree, by its briefs, and passes any other with no word, exit 0`,\n mend: \"give the hook's line a directory in this work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line that guards under a root in a work tree which holds the project\n * and is not its own: a repository the project is nested in, or the main\n * work tree around a linked one. `path` is the project's top as that work\n * tree names it.\n */\nexport function holderHookLine(line: Guards, tree: OtherTree, path: string): Guards {\n return { ...line, holder: { ...tree, path } };\n}\n\n/**\n * What Claude Code's hook does with the words it is handed, or `null` for\n * words that are not its line: read by the parser the command line reads\n * every line with, and known by its command and the argument after it on a\n * line that parsed and on one that did not, as the command line knows it\n * (cli.ts). `parse` is that parser, which throws its refusal with the words\n * it could still read.\n *\n * A line that guards says which brief it names, where it names one: the hook\n * passes over a `--brief` that is empty, as every release has, and holds\n * every write under one that names no brief, which only spec-brief can tell.\n */\nexport function claudeHookLine(\n words: readonly string[],\n parse: (argv: readonly string[]) => CommandLine,\n cwd: string,\n isDirectory: (path: string) => boolean,\n): HookLine | null {\n let line: CommandLine;\n try {\n line = parse(words);\n } catch (error) {\n const refused = error as { readonly message: string; readonly words: readonly string[] };\n return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;\n }\n if (!isClaudeHook(line.command, line.positionals[0])) return null;\n const read = readHookLine(line.root, cwd, isDirectory);\n return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known and neither\n * `--help` nor `--version` answers instead; a command with no shape is none,\n * which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(command, positionals[0])) return null;\n const stray = line.given.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => line.given.includes(candidate.option));\n if (mode !== undefined) {\n const unread = line.given.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a command line that parsed has still to be before it runs.\n *\n * The parser says what was typed. Whether it names anything was left to each\n * command, and most never asked: an option another command reads was taken\n * and did nothing, an argument no command asked for was dropped, and an\n * option given an empty value, as `--root \"$DIR\"` is where the variable is\n * not set, was read as the option left out. Each run then answered a\n * question nobody had put, exit 0, where a tool never falls back to its\n * defaults and reports clean (spec-core ADR-0005).\n *\n * Pure: the command line as it was read, and the line that refuses it, or\n * `null`. The one thing only the disk knows, whether `--root` is a\n * directory, is asked of the caller.\n */\n\nimport { resolve } from 'node:path';\n\nimport { sameId } from './branch.js';\nimport { literal } from './literal.js';\n\n/** A command line as the parser read it; what `parseOptions` returns is one. */\nexport interface CommandLine {\n readonly command: string | undefined;\n readonly positionals: readonly string[];\n /** The long name of every option on it, in the order given. */\n readonly given: readonly string[];\n /** Every value of each option that takes one, by its long name, in the order given; the fields below hold the last. */\n readonly written: Readonly<Record<string, readonly string[]>>;\n readonly brief: string | undefined;\n readonly base: string | undefined;\n readonly root: string | undefined;\n readonly paths: readonly string[];\n readonly reason: string | undefined;\n readonly options: readonly string[];\n readonly recommend: string | undefined;\n readonly show: string | undefined;\n readonly id: string | undefined;\n}\n\n/** Whether a value names nothing: empty, or only space. */\nexport function isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\ninterface Valued {\n readonly of: (line: CommandLine) => readonly string[];\n /** What is said of a value that names nothing, and what to do instead. */\n readonly says: string;\n}\n\n// Every value an option that takes one was given, so that an empty one is\n// refused wherever it stands: `--root \"\" --root .` names one directory twice\n// as the root is read, and the empty value is still the unset variable it\n// most often is. An option left out has no value to refuse, and a value that\n// names something is passed over as none is: the empty list is equivalent\n// to its mutant.\nconst every =\n (option: string) =>\n (line: CommandLine): readonly string[] =>\n line.written[option] ?? [];\n\n/**\n * The options that take a value, and what an empty one fails to name.\n * `--format` and `--at` are not here: each takes one of a few words, and the\n * parser refuses any other, the empty one with them. Nor is `--note`, which\n * `rule` refuses by name itself.\n */\nconst VALUED = literal({\n brief: { of: every('brief'), says: \"names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names\" },\n base: { of: every('base'), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },\n root: { of: every('root'), says: 'names no directory; give it one, or leave it out to run in the current directory' },\n path: { of: (line) => line.paths, says: 'names no file; give it the path of a file' },\n reason: { of: every('reason'), says: 'gives no reason; say why the round cannot be done without the file' },\n option: { of: (line) => line.options, says: 'names no choice; give it \"<label>: <cost>\", or leave it out' },\n recommend: { of: every('recommend'), says: 'recommends nothing; say which option and why, or leave it out' },\n show: { of: every('show'), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },\n id: { of: every('id'), says: \"names no probe; give it a probe's id, or leave it out to run every probe\" },\n}) satisfies Record<string, Valued>;\n\n/**\n * Whether two values of an option that takes one ask for one thing, as the\n * option reads them: a brief by its id as spec-brief's names are matched,\n * space around it dropped and `12` the brief `012` (ADR-0004); a root by the\n * directory it names from `cwd`, the one an empty root names on the hook's\n * line among them; any other as it is written, since each is read so, a\n * format, a probe's position, an id or text.\n */\nfunction asksOne(option: string, first: string, other: string, cwd: string): boolean {\n if (option === 'brief') return sameId(first.trim(), other.trim());\n if (option === 'root') return resolve(cwd, first) === resolve(cwd, other);\n return first === other;\n}\n\n/**\n * The first option that takes one value and was given two that read as two,\n * named with both as they were written; `null` where none was. The parser\n * keeps the last, and the earlier went unread with no word of it:\n * `doctor --root nowhere --root .` ran in `.`, and `--brief 404 --brief 001`\n * was brief 001 (spec-core ADR-0005). The same value given twice, however it\n * is spelled, is the option once, and the last is the one read. `cwd` is\n * where a relative root is read from. A list, `--path` and `--option`, has\n * every value it is given.\n */\nexport function twoValues(line: Pick<CommandLine, 'written'>, cwd: string): string | null {\n for (const [option, values] of Object.entries(line.written)) {\n const first = values[0]!;\n const other = values.find((value) => !asksOne(option, first, value, cwd));\n if (other !== undefined) return `--${option} is given twice, as ${JSON.stringify(first)} and as ${JSON.stringify(other)}, and one of the two would go unread`;\n }\n return null;\n}\n\nexport type ValuedOption = keyof typeof VALUED;\n\n/**\n * The line for a value that names nothing. `subject` is the input as its\n * caller knows it: `--brief` on a command line, `\"brief\"` in a tool call. The\n * value is shown as JSON, where a tab or a line break in it can be seen.\n */\nexport function emptyValue(subject: string, option: ValuedOption, value: string): string {\n return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;\n}\n\ninterface Shape {\n /** The options the command reads, by their long names. */\n readonly options: readonly string[];\n /** The most arguments it takes, with that said for the refusal of one more; `null` where it takes any number. */\n readonly limit: { readonly most: number; readonly takes: string } | null;\n /**\n * What an argument of it is, and what to do about an empty one; absent\n * where it takes none, or says so itself. `option` is the option that\n * names the same thing, where there is one: the two together are refused.\n * Only a brief is named both ways.\n */\n readonly argument?: { readonly role: string; readonly instead: string; readonly option?: 'brief' };\n /** The options whose empty value the command says in its own report. */\n readonly reports?: readonly string[];\n /**\n * The options that make the command another one, each with the only\n * options it reads beside it. The first on the list that is given decides,\n * as the command itself decides.\n */\n readonly modes?: readonly { readonly option: string; readonly beside: readonly string[] }[];\n}\n\nconst NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });\nconst ONE_BRIEF = literal({\n limit: { most: 1, takes: \"one argument at most, a brief's id\" },\n argument: { role: 'the brief', instead: \"give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names\", option: 'brief' },\n});\n\n/**\n * Every command, with what it reads of a command line. An option is listed\n * for a command only where the command's answer depends on it: `init`\n * chooses the base itself and the server is told the brief by each call, so\n * neither takes `--base` or `--brief`, and taking them would tell someone\n * the base or the brief had been set.\n */\nconst SHAPE = literal({\n context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n guard: {\n options: ['brief', 'base', 'root', 'format', 'strict'],\n limit: null,\n argument: { role: 'a path', instead: 'give it the paths the round would write' },\n },\n hook: { options: ['brief', 'base', 'root'], limit: { most: 1, takes: 'one argument, \"claude\" or \"git\"' } },\n // `--list` and `--show` print what waits and raise nothing: neither reads\n // the parts of a request, nor a brief, since what waits is every round's.\n escalate: {\n options: ['brief', 'root', 'format', 'path', 'reason', 'option', 'recommend', 'list', 'show'],\n ...NO_ARGUMENT,\n modes: [\n { option: 'list', beside: ['root', 'format'] },\n { option: 'show', beside: ['root', 'format'] },\n ],\n },\n rule: {\n options: ['root', 'format', 'allow', 'deny', 'note'],\n limit: { most: 1, takes: \"one argument, the escalation's id\" },\n argument: { role: 'the escalation', instead: 'give it an id, as spec-harness escalate --list prints them' },\n },\n rulings: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },\n audit: { options: ['brief', 'base', 'root', 'format', 'strict'], ...ONE_BRIEF },\n probe: { options: ['brief', 'base', 'root', 'format', 'id', 'at'], ...ONE_BRIEF },\n premises: { options: ['brief', 'root', 'format', 'strict'], ...NO_ARGUMENT },\n init: { options: ['root', 'format', 'write', 'git-hook'], ...NO_ARGUMENT },\n mcp: { options: ['root'], ...NO_ARGUMENT },\n // A diagnosis says what is wrong with what it was given, where a command\n // that acts on it stops: a brief or a base that names nothing is a line of\n // doctor's report.\n doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },\n}) satisfies Record<string, Shape>;\n\n/** The options every command answers instead of running, which no shape lists. */\nconst ANSWERS: readonly string[] = ['help', 'version'];\n\n/** Every command there is: the command line dispatches on these names and no other. */\nexport type Command = keyof typeof SHAPE;\n\n// A map, since a command is a word a person types and an object answers to\n// more names than it was given: `constructor` has no shape.\nconst SHAPES: ReadonlyMap<string, Shape> = new Map(Object.entries(SHAPE));\n\nfunction listed(options: readonly string[]): string {\n const names = options.map((option) => `--${option}`);\n // Every command reads at least `--root`, so a list is never empty.\n if (names.length === 1) return `its only option is ${names[0] as string}`;\n return `its options are ${names.slice(0, -1).join(', ')} and ${names.at(-1)!}`;\n}\n\n/**\n * Whether a line is Claude Code's hook, by its command and the argument\n * after it. It is exempt from every refusal here, and read as each release\n * since 0.1.0 read it: the line lives in settings files and in the plugin of\n * every release, which this one cannot rewrite, and exit 2 from it holds\n * every write of a session, the write that would fix the line among them.\n * A line the parser cannot read is asked the same, and the hook says it\n * with exit 1 (cli.ts).\n */\nexport function isClaudeHook(command: string | undefined, argument: string | undefined): boolean {\n return command === 'hook' && argument === 'claude';\n}\n\n/** The words for a `--root` that is a file, or is not there, wherever it is said. */\nexport function noDirectory(root: string): string {\n return `--root is \"${root}\", which is not a directory`;\n}\n\n/** What is wrong with the line of Claude Code's hook, and what mends it. */\nexport interface LineFault {\n readonly wrong: string;\n readonly mend: string;\n}\n\n/**\n * What Claude Code's hook does with its own line, as far as the line decides\n * it: it guards, in the root the line gives it where it gives one and under\n * the brief it names where it names one; or the guard is off, for a fault\n * the hook says with exit 1 and holds no write for; or every write is held,\n * exit 2, for a brief the line names that is not one (ADR-0005). The hook\n * reads its line through the functions below, and `doctor` reads a line it\n * finds in Claude Code's settings through the same ones, so what it says of\n * a line is what the hook does with it.\n */\nexport type HookLine = Guards | GuardOff | WritesHeld;\n\nexport interface Guards {\n readonly state: 'guards';\n readonly root: string | undefined;\n /**\n * The brief the line names with `--brief`, as written. Whether it is a\n * brief is spec-brief's to say, which the caller asks. Absent where the\n * line names none.\n */\n readonly brief?: string;\n /**\n * The work tree the root is in, where that is not the project's own and\n * holds it: the hook opens the work tree its root is in, so a write to a\n * file of the project is checked there, by that work tree's briefs, under\n * the path the file has in it. Only git can tell, so the caller asks it.\n * Absent where the line gives no root, or one in the project's work tree.\n */\n readonly holder?: OtherTree & { readonly path: string };\n}\n\n/** A work tree the root on the hook's line is in that is not the project's own. */\nexport interface OtherTree {\n /** Its top, as git names it. */\n readonly top: string;\n /** Whether it is a work tree of the project's own repository, linked or main, where it is otherwise another repository's. */\n readonly ours: boolean;\n}\n\n/** How such a work tree is named: by its top, and whose it is. */\nexport function namedTree({ top, ours }: OtherTree): string {\n return `${top}, ${ours ? 'another work tree of this repository' : 'the work tree of another repository'}`;\n}\n\nexport interface GuardOff {\n readonly state: 'off';\n readonly fault: LineFault;\n /**\n * Set where the hook says nothing of the fault and exits 0, so that no\n * run of it fails: a root outside any work tree, or in a work tree that\n * does not hold the project. Every other fault it says, with exit 1.\n */\n readonly silent?: true;\n /**\n * What goes unchecked, where that is not every write: under a root in a\n * work tree that does not hold the project, the hook checks a write to a\n * file of that work tree, and none to a file of the project.\n */\n readonly unchecked?: string;\n}\n\nexport interface WritesHeld {\n readonly state: 'held';\n /** What is said of the name, by what named the brief (workspace.ts). */\n readonly wrong: string;\n}\n\n/**\n * A line under which the round has no brief to be checked against, since\n * what names one names a brief spec-brief does not know, or one it has\n * archived: the hook holds the write, exit 2, and every write after it until\n * the name is mended (ADR-0004). `wrong` is what is said of the name.\n */\nexport function heldHookLine(wrong: string): WritesHeld {\n return { state: 'held', wrong };\n}\n\n/**\n * A line the parser refused: an option no command has, an option left\n * without its value, a `--format` or an `--at` that is none, `--help` beside\n * `--version`; or a line it read with two values for an option that takes\n * one, of which it cannot tell the one meant ({@link twoValues}). The\n * parser's words are kept, on one line: what node says of an option that may\n * have lost its value is three.\n */\nexport function unreadHookLine(said: string): GuardOff {\n return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\\n').join(' ')})`, mend: 'mend it in Claude Code\\'s settings, where init writes \"hook claude\" and nothing after it' } };\n}\n\n/**\n * A line that parsed, by the root it gives. An empty root is the directory\n * the hook was started in: git starts there when it is handed no directory,\n * so the hook has guarded there for an empty root in every release. A root\n * that is a file, or is not there, is one git starts in neither of, which\n * read as a session outside a work tree, exit 0 and no word, and the guard\n * someone had configured was off.\n */\nexport function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff {\n const given = root === '' ? cwd : root;\n if (given !== undefined && !isDirectory(given)) {\n return { state: 'off', fault: { wrong: noDirectory(given), mend: \"give the hook's line a directory that exists, or leave --root out for the project Claude Code names\" } };\n }\n return { state: 'guards', root: given };\n}\n\n/**\n * A line whose root is a directory outside any git work tree. The hook\n * reads that as a session outside one, where it has nothing to guard: it\n * checks no write and says nothing, exit 0 (ADR-0005), and that stays. Only\n * git can tell such a root, so the caller asks it. A line that carries one\n * is a guard someone set up that checks nothing, and `doctor` says it as\n * off.\n */\nexport function outsideHookLine(root: string): GuardOff {\n return {\n state: 'off',\n silent: true,\n fault: {\n wrong: `--root is \"${root}\", a directory outside any git work tree, which the hook reads as a session outside one, exit 0 and no word`,\n mend: \"give the hook's line a directory in the work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line whose root is in a git work tree that does not hold the project:\n * another repository beside it, or a work tree inside it, a repository of\n * its own, a submodule or a linked worktree. The hook opens the work tree\n * its root is in and decides by that work tree's briefs. A file of the\n * project is outside it, which the hook reads as a file outside the\n * repository: it says nothing, exit 0, as it has in every release, and that\n * stays. So no write to a file of the project is checked under the line,\n * and `doctor` says it as a guard that is off. Only git can tell such a\n * root, so the caller asks it.\n */\nexport function elsewhereHookLine(root: string, tree: OtherTree): GuardOff {\n return {\n state: 'off',\n silent: true,\n unchecked: 'no write to a file of this project is checked',\n fault: {\n wrong: `--root is \"${root}\", a directory in ${namedTree(tree)}, which does not hold this project: the hook checks a write to a file of that work tree, by its briefs, and passes any other with no word, exit 0`,\n mend: \"give the hook's line a directory in this work tree, or leave --root out for the project Claude Code names\",\n },\n };\n}\n\n/**\n * A line that guards under a root in a work tree which holds the project\n * and is not its own: a repository the project is nested in, or the main\n * work tree around a linked one. `path` is the project's top as that work\n * tree names it.\n */\nexport function holderHookLine(line: Guards, tree: OtherTree, path: string): Guards {\n return { ...line, holder: { ...tree, path } };\n}\n\n/**\n * What Claude Code's hook does with the words it is handed, or `null` for\n * words that are not its line: read by the parser the command line reads\n * every line with, and known by its command and the argument after it on a\n * line that parsed and on one that did not, as the command line knows it\n * (cli.ts). `parse` is that parser, which throws its refusal with the words\n * it could still read.\n *\n * A line that guards says which brief it names, where it names one: the hook\n * passes over a `--brief` that is empty, as every release has, and holds\n * every write under one that names no brief, which only spec-brief can tell.\n * A line with two values for an option that takes one is a line the hook\n * cannot read, whichever option it is, read from `cwd` as the hook reads it\n * from where it is started (cli.ts).\n */\nexport function claudeHookLine(\n words: readonly string[],\n parse: (argv: readonly string[]) => CommandLine,\n cwd: string,\n isDirectory: (path: string) => boolean,\n): HookLine | null {\n let line: CommandLine;\n try {\n line = parse(words);\n } catch (error) {\n const refused = error as { readonly message: string; readonly words: readonly string[] };\n return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;\n }\n if (!isClaudeHook(line.command, line.positionals[0])) return null;\n const twice = twoValues(line, cwd);\n if (twice !== null) return unreadHookLine(twice);\n const read = readHookLine(line.root, cwd, isDirectory);\n return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;\n}\n\n/**\n * Why the command line is refused, or `null` when the command can run:\n * an option it does not read, alone or beside another, an argument it does\n * not take, a brief named twice, a value or an argument that names nothing,\n * a `--root` that is no directory. Asked once a command is known, and before\n * `--help` or `--version` answers: a line that is wrong is refused with\n * either on it, as one the parser refuses always was (spec-core ADR-0005).\n * A command with no shape is none, which its caller says.\n */\nexport function refusal(line: CommandLine, isDirectory: (path: string) => boolean): string | null {\n const { command, positionals } = line;\n // No shape is kept under `undefined`, so the first check is the compiler's,\n // and its mutant is equivalent.\n if (command === undefined) return null;\n const shape = SHAPES.get(command);\n if (shape === undefined || isClaudeHook(command, positionals[0])) return null;\n // What the line asks the command, beside the help or the version it may\n // ask for instead, which every command answers.\n const asked = line.given.filter((option) => !ANSWERS.includes(option));\n const stray = asked.find((option) => !shape.options.includes(option));\n if (stray !== undefined) return `${command} does not take --${stray}; ${listed(shape.options)}`;\n // An option the command reads, beside one that leaves it unread:\n // `escalate --list --path src/a.ts` printed the list, exit 0, and the\n // request it also spelled out was never raised.\n const mode = shape.modes?.find((candidate) => asked.includes(candidate.option));\n if (mode !== undefined) {\n const unread = asked.find((option) => option !== mode.option && !mode.beside.includes(option));\n if (unread !== undefined) return `${command} --${mode.option} does not take --${unread}; beside --${mode.option} ${listed(mode.beside)}`;\n }\n if (shape.limit !== null && positionals.length > shape.limit.most) {\n return `${command} takes ${shape.limit.takes}, not ${positionals.map((argument) => JSON.stringify(argument)).join(', ')}`;\n }\n // One thing named two ways, whatever the two hold: the argument won with\n // no word, so `audit 012 --brief 013` audited 012 for someone who had\n // also typed 013.\n const { argument } = shape;\n if (argument?.option !== undefined && positionals.length > 0 && line.given.includes(argument.option)) {\n return `${command} was given ${argument.role} twice, as its argument ${JSON.stringify(positionals[0])} and as --${argument.option} ${JSON.stringify(line[argument.option])}; give one of them`;\n }\n for (const [option, valued] of Object.entries(VALUED) as [ValuedOption, Valued][]) {\n if (shape.reports?.includes(option)) continue;\n const empty = valued.of(line).find(isBlank);\n if (empty !== undefined) return emptyValue(`--${option}`, option, empty);\n }\n const empty = positionals.find(isBlank);\n if (shape.argument !== undefined && empty !== undefined) {\n return `${command} was given ${JSON.stringify(empty)} as ${shape.argument.role}, which names none; ${shape.argument.instead}`;\n }\n // An empty one was refused above, so this is a path that was meant: a file,\n // or a directory that is not there. git starts in neither, and the run\n // ended saying the path was outside a work tree, which named neither the\n // option nor what was wrong with what it was given.\n if (line.root !== undefined && !isDirectory(line.root)) {\n return `${noDirectory(line.root)}; give it one that exists, or leave it out to run in the current directory`;\n }\n return null;\n}\n"]}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { type ActiveBrief } from './briefs.js';
6
6
  import type { HarnessConfig } from './config.js';
7
+ import { type WorkTree } from './git.js';
7
8
  import type { FindingFormat } from './formats.js';
8
9
  import { type Siblings } from './siblings.js';
9
10
  import type { BriefRow } from './types.js';
@@ -41,6 +42,12 @@ export interface Options {
41
42
  readonly positionals: readonly string[];
42
43
  /** The long name of every option given, in the order given: what tells an option left out from one a command does not read. */
43
44
  readonly given: readonly string[];
45
+ /**
46
+ * Every value given to each option that takes one value, by its long name,
47
+ * in the order given. The fields below hold the last of them, which is
48
+ * what the parser keeps.
49
+ */
50
+ readonly written: Readonly<Record<string, readonly string[]>>;
44
51
  readonly brief: string | undefined;
45
52
  readonly base: string | undefined;
46
53
  readonly root: string | undefined;
@@ -81,7 +88,37 @@ export interface Workspace {
81
88
  readonly branchSource?: string | undefined;
82
89
  readonly cwd: string;
83
90
  }
84
- export declare function openWorkspace(options: Pick<Options, 'root'>, io: CliIO): Promise<Workspace>;
91
+ /**
92
+ * A work tree as the first wave of opening it found it: the directory it
93
+ * was asked from, its top, what one start of git said from there, and the
94
+ * branch where it was asked beside them.
95
+ */
96
+ export interface FoundTree {
97
+ readonly cwd: string;
98
+ readonly root: string;
99
+ readonly tree: WorkTree | 'unread';
100
+ readonly early: string | null | undefined;
101
+ }
102
+ /** The work tree that holds `cwd`, or `null` where none does. */
103
+ export declare function findTree(cwd: string): Promise<FoundTree | null>;
104
+ /**
105
+ * The git directory a found work tree's repository keeps, as git names it
106
+ * from the directory it was found from: one repository's work trees, linked
107
+ * and main, share it, and no two repositories do. Asked again only where
108
+ * the one start of git could not be read.
109
+ */
110
+ export declare function foundCommonDir(found: FoundTree): Promise<string | null>;
111
+ export declare function openWorkspace(options: Pick<Options, 'root'>, io: CliIO, installs?: Installs): Promise<Workspace>;
112
+ /** The installs a work tree's siblings are looked for in after its own, given its top and its common directory (siblings.ts). */
113
+ export type Installs = (root: string, commonDir: string) => readonly string[];
114
+ /**
115
+ * git's hook and doctor: a linked worktree with no install of its own is
116
+ * checked with the main work tree's, where `git worktree add` leaves the
117
+ * project's `node_modules` (ADR-0005).
118
+ */
119
+ export declare const MAIN_INSTALL: Installs;
120
+ /** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */
121
+ export declare function openFound(found: FoundTree, io: CliIO, installs?: Installs): Promise<Workspace>;
85
122
  /**
86
123
  * What one of several things asked at once answered, or what it threw,
87
124
  * thrown from here: the things are asked through `Promise.allSettled`, and
package/dist/workspace.js CHANGED
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { findActive } from './briefs.js';
8
8
  import { briefIdFromBranch, ciBranch } from './branch.js';
9
- import { holdsGit, isTop, loadConfig } from './fs.js';
9
+ import { holdsGit, isTop, loadConfig, mainWorkTree } from './fs.js';
10
10
  import { commonDirectory, currentBranch, workTree, workTreeRoot } from './git.js';
11
11
  import { createSiblings } from './siblings.js';
12
12
  export const EXIT_OK = 0;
@@ -64,7 +64,7 @@ export function parseOptions(argv) {
64
64
  };
65
65
  let parsed;
66
66
  try {
67
- parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options });
67
+ parsed = parseArgs({ args: [...argv], allowPositionals: true, strict: true, options, tokens: true });
68
68
  }
69
69
  catch (error) {
70
70
  // Read again with nothing refused, for whose line it was and nothing
@@ -73,7 +73,25 @@ export function parseOptions(argv) {
73
73
  // so that a value is not taken for the command it stands before.
74
74
  throw new UnreadableLine(error.message, parseArgs({ args: [...argv], strict: false, options }).positionals);
75
75
  }
76
- const { values, positionals } = parsed;
76
+ const { values, positionals, tokens } = parsed;
77
+ // The parser keeps the last value of an option given twice, and the earlier
78
+ // went unread with no word of it: `--root nowhere --root .` ran in `.`. So
79
+ // every value is kept, for the refusal of two (usage.ts). A list keeps
80
+ // every value already.
81
+ const table = options;
82
+ const written = {};
83
+ for (const token of tokens) {
84
+ if (token.kind === 'option' && token.value !== undefined && table[token.name].multiple !== true)
85
+ (written[token.name] ??= []).push(token.value);
86
+ }
87
+ // Two answers asked for at once: the version was printed, and the help went
88
+ // unread. A line that is wrong, whoever's it is (spec-core ADR-0005). Only
89
+ // an option's token has a name, so the test of its kind is the compiler's,
90
+ // and its mutant is equivalent.
91
+ const answers = tokens.flatMap((token) => (token.kind === 'option' && (token.name === 'help' || token.name === 'version') ? [token] : []));
92
+ const other = answers.find((token) => token.name !== answers[0].name);
93
+ if (other !== undefined)
94
+ throw new UnreadableLine(`${answers[0].rawName} is given beside ${other.rawName}, and one of the two answers would go unread; give one of them`, positionals);
77
95
  const asked = values.format ?? 'pretty';
78
96
  const format = asked;
79
97
  const formats = ['pretty', 'json', 'gitlab', 'sarif', 'github'];
@@ -91,6 +109,7 @@ export function parseOptions(argv) {
91
109
  command,
92
110
  positionals: positionals.slice(1),
93
111
  given: Object.keys(values),
112
+ written,
94
113
  brief: values.brief,
95
114
  base: values.base,
96
115
  root: values.root,
@@ -113,8 +132,8 @@ export function parseOptions(argv) {
113
132
  at,
114
133
  };
115
134
  }
116
- export async function openWorkspace(options, io) {
117
- const cwd = options.root ?? io.cwd;
135
+ /** The work tree that holds `cwd`, or `null` where none does. */
136
+ export async function findTree(cwd) {
118
137
  // Each git command is a process, a few milliseconds on Linux and a few
119
138
  // hundred on a Windows workstation, and every command opens a work tree,
120
139
  // the hook before every write among them. So the top and the common
@@ -124,9 +143,50 @@ export async function openWorkspace(options, io) {
124
143
  // it did: below a top it would be asked twice, and in no work tree for
125
144
  // nothing.
126
145
  const [tree, early] = await Promise.all([workTree(cwd), holdsGit(cwd) ? currentBranch(cwd) : undefined]);
127
- const root = tree === 'unread' ? await workTreeRoot(cwd) : (tree?.root ?? null);
128
- if (root === null)
146
+ if (tree === null)
147
+ return null;
148
+ const root = tree === 'unread' ? await workTreeRoot(cwd) : tree.root;
149
+ return root === null ? null : { cwd, root, tree, early };
150
+ }
151
+ /**
152
+ * The git directory a found work tree's repository keeps, as git names it
153
+ * from the directory it was found from: one repository's work trees, linked
154
+ * and main, share it, and no two repositories do. Asked again only where
155
+ * the one start of git could not be read.
156
+ */
157
+ export async function foundCommonDir(found) {
158
+ // Asked again from the top, git names the directory the one start named:
159
+ // the mutant that always asks again is equivalent, at a process's cost.
160
+ return found.tree === 'unread' ? commonDirectory(found.root) : found.tree.commonDir;
161
+ }
162
+ export async function openWorkspace(options, io, installs = NO_INSTALLS) {
163
+ const cwd = options.root ?? io.cwd;
164
+ const found = await findTree(cwd);
165
+ if (found === null)
129
166
  throw new UsageError(`${cwd} is not inside a git work tree; spec-harness measures rounds by their commits`);
167
+ return openFound(found, io, installs);
168
+ }
169
+ /**
170
+ * Every command but the hooks and doctor: a work tree is checked with its
171
+ * own install, as it was. A name put on the list is a directory relative to
172
+ * where the command runs, which holds no install, and no list at all is
173
+ * the siblings' own default of none, so those mutants are equivalent.
174
+ */
175
+ const NO_INSTALLS = () => [];
176
+ /**
177
+ * git's hook and doctor: a linked worktree with no install of its own is
178
+ * checked with the main work tree's, where `git worktree add` leaves the
179
+ * project's `node_modules` (ADR-0005).
180
+ */
181
+ export const MAIN_INSTALL = (root, commonDir) => {
182
+ const main = mainWorkTree(commonDir);
183
+ // The main work tree's own install is the one looked in first: the mutant
184
+ // that keeps it here as well looks in it twice and finds what it found.
185
+ return main === null || main === root ? [] : [main];
186
+ };
187
+ /** The rest of opening a work tree, once it is found: its common directory, its configuration and its branch. */
188
+ export async function openFound(found, io, installs = NO_INSTALLS) {
189
+ const { cwd, root, tree, early } = found;
130
190
  // The common directory and the branch are asked of the root. What git
131
191
  // said from `cwd` is that answer where `cwd` is the root, the same
132
192
  // directory asked the same thing, and nowhere else. Measured with git
@@ -146,7 +206,7 @@ export async function openWorkspace(options, io) {
146
206
  // out, asks the forge which branch the run builds.
147
207
  const ci = checkedOut === null ? ciBranch(io.env) : null;
148
208
  const named = ci === null ? { branch: checkedOut } : { branch: ci.branch, branchSource: ci.source };
149
- return { root, commonDir, config, siblings: createSiblings(root, config), ...named, cwd: io.cwd };
209
+ return { root, commonDir, config, siblings: createSiblings(root, config, installs(root, commonDir)), ...named, cwd: io.cwd };
150
210
  }
151
211
  /**
152
212
  * What one of several things asked at once answered, or what it threw,