Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions packages/code/src/wiring/misuse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,20 @@ function spoken(command: Command): string {
.join(' ');
}

/** The line to type, exactly as this command's own `--help` prints it. */
function usageOf(command: Command): string {
/**
* The line to type, exactly as this command's own `--help` prints it.
*
* EXPORTED BECAUSE A GUARD READS IT TO TELL THE TWO VOICES APART, and it has to be this
* function rather than a copy. Both of the parser's answers carry this line — the help
* heads it with commander's `Usage: `, and the second line of a misuse above is it,
* indented — and a verb's own refusal never carries it, so it is what says the PARSER
* answered a line and not the path it names. A guard that retyped the text would go on
* passing while matching nothing the day the shape changed, which is what happened:
* `the-refused-run-is-refused-everywhere.test.ts` looked for `Usage: mnema` and therefore
* saw the help and NONE of the seven misuse wordings, because the prefix is the help
* formatter's and not this line's.
*/
export function usageOf(command: Command): string {
return command.createHelp().commandUsage(command);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ const TEST_TREE: readonly TestSource[] = READABLE.flatMap((pkg) =>
* and leaves 2145, which clears that floor and reddens nothing else in this file. Restate
* this number when the tree gains an import, which is the point of writing it down.
*/
const CLAUSES_IN_THE_TREE = 2421;
const CLAUSES_IN_THE_TREE = 2422;

const { importedBy, witnessedBy, unresolved } = witnessing(PRODUCTION, TEST_TREE, codeOnly);

Expand Down
49 changes: 45 additions & 4 deletions packages/code/tests/outside-a-project-the-surface-says-so.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
pathsNamedInTables,
SERVES_A_CONNECTION,
SERVES_OR_REACHES_OUT,
theParserAnswered,
theSurface,
} from './support/the-line-a-path-takes.js';
import { held } from './support/the-record-held.js';
Expand Down Expand Up @@ -148,6 +149,25 @@ const SAYS_NO_TAIL_HOLDS_EVENTS: Readonly<Record<string, string>> = {
const NO_TAIL_TO_WITNESS =
'Refused (NO_TAIL): there is no tail here to witness — nothing has been recorded in these trees';

/**
* THE ONE REASON IN THAT TABLE THAT IS NOT PROSE, because it is the only one a guard can
* ask the surface about.
*
* A reconciliation of NAMES can say a path refused something else first without ever
* reading what it said, so every reason in the table beside it is a note to a reader. This
* one is checked: `support/the-line-a-path-takes.ts` tells the parser's answer from a
* verb's own — off the product's own `usageOf`, which both of the parser's answers carry —
* and the case below asserts it for each path declared with this string, and asserts it is
* FALSE for every path that landed in box 1.
*
* It was written as three copies of a sentence nothing read. What that shape cost was
* measured next door, in the guard that classifies the same three paths for the run pin:
* it looked for the literal `Usage: mnema`, which the help carries and the misuse voice
* does not, and so it saw none of the seven codes the surface words.
*/
const THE_PARSER_ANSWERED_FIRST =
'a group whose bare form routes nothing: the parser answers with usage';

/**
* A path whose own refusal arrives before the project is ever missed — with what it says.
*
Expand All @@ -160,9 +180,9 @@ const NO_TAIL_TO_WITNESS =
* because what a verb SAYS is not a coverage slice's to change.
*/
const REFUSES_SOMETHING_ELSE_FIRST: Readonly<Record<string, string>> = {
run: 'a group whose bare form routes nothing: the parser answers with usage',
key: 'a group whose bare form routes nothing: the parser answers with usage',
tail: 'a group whose bare form routes nothing: the parser answers with usage',
run: THE_PARSER_ANSWERED_FIRST,
key: THE_PARSER_ANSWERED_FIRST,
tail: THE_PARSER_ANSWERED_FIRST,
'run end': 'with no id and no MNEMA_RUN there is no session named, which it says first',
show: 'FINDING: it answers "No record <id> here" where there is no record at all',
'skill export': 'FINDING: it answers "No skill <id> here" where there is no record at all',
Expand All @@ -175,6 +195,8 @@ interface Answered {
readonly path: string;
readonly failed: boolean;
readonly said: string;
/** Whether the PARSER answered rather than the path — read off the declaration. */
readonly parserAnswered: boolean;
}

/** What a declaration table tolerates, and what it does not — checked both ways. */
Expand Down Expand Up @@ -217,7 +239,13 @@ describe('outside a project the surface says so', () => {
},
};
await run(lineFor(routed, NOTHING_FOUNDED), io);
answered.push({ path: routed.path, failed, said: [...err, ...out].join('\n') });
const said = [...err, ...out].join('\n');
answered.push({
path: routed.path,
failed,
said,
parserAnswered: theParserAnswered(routed, said),
});
}
} finally {
process.chdir(restore.cwd);
Expand Down Expand Up @@ -277,6 +305,19 @@ describe('outside a project the surface says so', () => {
stale: [],
});

// AND THE ONE REASON IN BOX 4 THAT IS CHECKABLE IS CHECKED, both ways. A path declared
// as answered by the parser has to have been, and — the half that keeps it from being
// a discriminant that says yes to everything — no path in box 1 may look like one.
// Three copies of that sentence were three notes to a reader before this.
const byTheParser = answered.filter((one) => one.parserAnswered).map((one) => one.path);
expect([...byTheParser].sort()).toEqual(
Object.entries(REFUSES_SOMETHING_ELSE_FIRST)
.filter(([, why]) => why === THE_PARSER_ANSWERED_FIRST)
.map(([path]) => path)
.sort(),
);
expect(missedTheProject.filter((one) => one.parserAnswered).map((one) => one.path)).toEqual([]);

// The four boxes are the whole surface, and box 1 holds most of it — a walk that
// stopped finding paths would leave every reconciliation above passing over nothing.
expect(answered.length).toBe(theSurface().length - Object.keys(SERVES_A_CONNECTION).length);
Expand Down
53 changes: 52 additions & 1 deletion packages/code/tests/support/the-line-a-path-takes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import type { Argument, Command, Option } from 'commander';
import { buildProgram, type CliIo } from '../../src/cli.js';
import { valuesDeclaredOn } from '../../src/wiring/enumerated.js';
import { everyCommandOf, pathOf } from '../../src/wiring/misuse.js';
import { everyCommandOf, pathOf, usageOf } from '../../src/wiring/misuse.js';

/** One invocable command path, and the declaration that says what it takes. */
export interface Routed {
Expand Down Expand Up @@ -240,6 +240,57 @@ export function lineFor(routed: Routed, fixture: Fixture): string[] {
return words;
}

// ---------------------------------------------------------------------------
// Which voice answered a line
// ---------------------------------------------------------------------------

/**
* The shape a usage error takes when the parser raised a code the product has no wording
* for: the code, then the message it came with.
*
* It is the third of the parser's three answers and the only one that carries no usage
* line, so {@link theParserAnswered} would miss it on the line's shape alone. The
* discriminant is the CODE's namespace and not the sentence — `commander.` is an
* identifier, and the sentence after it is a dependency's English.
*
* `wiring/report.ts` frames a refusal this way for the product's own codes too
* (`Refused (UNPROVEN_RUN): …`), which is exactly why the prefix has to be matched WITH
* the namespace: a verb's own no takes the same shape.
*/
export const A_CODE_NOBODY_WORDED = 'Refused (commander.';

/**
* WHETHER THE PARSER ANSWERED A LINE, rather than the path the line names.
*
* A caller that walks the surface asking what each path DID has one way of being wrong
* that it cannot see: a synthesised line the declaration could not answer for is refused
* before any action runs, and it then looks exactly like a path that does not do the
* thing being measured. So the parser's answer has to be identifiable, and this is the
* one place that says how.
*
* IT IS THE PRODUCT'S OWN FUNCTION THAT IS READ, not a copy of its text.
* `wiring/misuse.ts` gives every command of the program one voice for a misuse and puts
* {@link usageOf} under the sentence; commander heads the help with `Usage: ` and the same
* line. So the line to type is what both of the parser's answers carry and a verb's own
* refusal never does — and calling `usageOf` is what keeps this from drifting from the
* shape it is looking for.
*
* WHAT THAT COST WHEN IT WAS A LITERAL, since this function exists because of it. The
* guard used to look for `Usage: mnema`, which the help carries and the misuse voice does
* NOT: the prefix is commander's help formatter's, added outside `usageOf`. All seven
* worded codes therefore read as "the parser did not answer" — measured against the built
* binary, `mnema task`, `mnema decision <title>`, `mnema link a b --rel` and
* `mnema completion powershell` each print their sentence and an indented usage line with
* no `Usage:` anywhere on it.
*
* `said` is what went to the stream a refusal goes to. Both of the parser's answers are
* written there — the misuse voice by `speakUsageErrors`, and the help shown INSTEAD of an
* error by commander itself.
*/
export function theParserAnswered(routed: Routed, said: string): boolean {
return said.includes(usageOf(routed.command)) || said.includes(A_CODE_NOBODY_WORDED);
}

/** Every table here that names a path, for a caller reconciling them against the walk. */
export function pathsNamedInTables(): string[] {
return [
Expand Down
Loading