roll-parser 3.3.0 → 3.4.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +60 -32
  3. package/dist/cli/args.d.ts +11 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +67 -42
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/format.d.ts +8 -1
  8. package/dist/cli/format.d.ts.map +1 -1
  9. package/dist/cli/format.js +4 -3
  10. package/dist/cli/format.js.map +1 -1
  11. package/dist/cli/main.d.ts +3 -3
  12. package/dist/cli/main.d.ts.map +1 -1
  13. package/dist/cli/main.js +50 -19
  14. package/dist/cli/main.js.map +1 -1
  15. package/dist/evaluator/evaluator.d.ts +15 -3
  16. package/dist/evaluator/evaluator.d.ts.map +1 -1
  17. package/dist/evaluator/evaluator.js +134 -105
  18. package/dist/evaluator/evaluator.js.map +1 -1
  19. package/dist/evaluator/modifiers/flags.d.ts +7 -1
  20. package/dist/evaluator/modifiers/flags.d.ts.map +1 -1
  21. package/dist/evaluator/modifiers/flags.js +9 -1
  22. package/dist/evaluator/modifiers/flags.js.map +1 -1
  23. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  24. package/dist/evaluator/modifiers/keep-drop.js +19 -16
  25. package/dist/evaluator/modifiers/keep-drop.js.map +1 -1
  26. package/dist/lexer/lexer.d.ts +5 -0
  27. package/dist/lexer/lexer.d.ts.map +1 -1
  28. package/dist/lexer/lexer.js +3 -0
  29. package/dist/lexer/lexer.js.map +1 -1
  30. package/dist/parser/parser.d.ts +1 -0
  31. package/dist/parser/parser.d.ts.map +1 -1
  32. package/dist/parser/parser.js +23 -20
  33. package/dist/parser/parser.js.map +1 -1
  34. package/dist/render.d.ts.map +1 -1
  35. package/dist/render.js +2 -1
  36. package/dist/render.js.map +1 -1
  37. package/dist/rng/seeded.d.ts.map +1 -1
  38. package/dist/rng/seeded.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/package.json +13 -14
  42. package/src/cli/args.ts +112 -40
  43. package/src/cli/format.ts +11 -3
  44. package/src/cli/main.ts +97 -22
  45. package/src/evaluator/evaluator.ts +189 -150
  46. package/src/evaluator/modifiers/flags.ts +18 -2
  47. package/src/evaluator/modifiers/keep-drop.ts +42 -16
  48. package/src/lexer/lexer.ts +8 -0
  49. package/src/parser/parser.ts +36 -32
  50. package/src/render.ts +2 -1
  51. package/src/rng/seeded.ts +4 -0
  52. package/src/version.ts +1 -1
package/src/cli/main.ts CHANGED
@@ -8,10 +8,15 @@
8
8
  * @module cli/main
9
9
  */
10
10
 
11
- import { getErrorSpan, isRollParserError } from '../errors.js';
11
+ import {
12
+ type ErrorSpan,
13
+ getErrorSpan,
14
+ isRollParserError,
15
+ type RollParserErrorCode,
16
+ } from '../errors.js';
12
17
  import { VERSION } from '../index.js';
13
18
  import { roll } from '../roll.js';
14
- import { parseArgs } from './args.js';
19
+ import { isKnownOption, parseArgs } from './args.js';
15
20
  import { formatResult } from './format.js';
16
21
 
17
22
  const HELP_TEXT = `roll-parser v${VERSION}
@@ -24,12 +29,23 @@ Options:
24
29
  -v, --verbose Show detailed roll breakdown
25
30
  --json Print the whole result as compact JSON (wins over --verbose)
26
31
  --seed <value> Use seed for reproducible rolls
27
- -- Treat every following argument as notation
32
+ -- Notation that looks like an option — put options before it
28
33
 
29
34
  JSON output:
30
- Emits the complete result, including the structured "parts" tree. The
31
- "degree" field (DegreeOfSuccess) serializes as a number: 0 critical failure,
32
- 1 failure, 2 success, 3 critical success. Errors stay plain text on stderr.
35
+ Emits the complete result, including the structured "parts" tree, plus the
36
+ "seed" that produced it and the "version" that fixes the seed-to-dice
37
+ mapping rerun with --seed <seed> on the same major to replay the roll.
38
+ When --seed is omitted the CLI mints one. The "degree" field
39
+ (DegreeOfSuccess) serializes as a number: 0 critical failure, 1 failure,
40
+ 2 success, 3 critical success.
41
+
42
+ JSON errors:
43
+ Every diagnostic is one JSON line on stderr:
44
+ {"error":{"message":...,"code":...,"span":{"start":N}},...}. A roll error
45
+ adds the "notation" and "seed" that produced it, so a failure replays like
46
+ a result does; "code" and "span" are absent when the failure carries
47
+ neither. Usage errors are covered too, but only where --json still reads as
48
+ the flag: it is a seed value in --seed --json, and notation after --.
33
49
 
34
50
  Exit codes:
35
51
  0 Success
@@ -41,9 +57,12 @@ Examples:
41
57
  roll-parser 4d6kh3 --verbose
42
58
  roll-parser 4d6dl1 --seed "character-str"
43
59
  roll-parser "1d20+7 vs 25" --json
44
- roll-parser -- -1d6+3
60
+ roll-parser -1d6+3
45
61
  `;
46
62
 
63
+ // The build sets `types: []`, so no ambient runtime globals are declared.
64
+ declare const crypto: { randomUUID(): string };
65
+
47
66
  /** Sink for one stream's worth of CLI output. */
48
67
  export type WriteFn = (text: string) => void;
49
68
 
@@ -74,26 +93,73 @@ export function writeErrorContext(notation: string, error: unknown, write: Write
74
93
  write(` ${' '.repeat(column)}^\n`);
75
94
  }
76
95
 
96
+ /**
97
+ * Names the ordering rule when `--` swallowed an option. Gated on `terminated`,
98
+ * never on `argv` holding a `--`: `--seed --` makes that `--` a seed value, so a
99
+ * textual scan would name a terminator that never ran.
100
+ */
101
+ function writeOptionHint(terminated: boolean, notation: string, write: WriteFn): void {
102
+ if (terminated && notation.split(' ').some(isKnownOption)) {
103
+ write('Hint: options must come before "--"\n');
104
+ }
105
+ }
106
+
107
+ /** The `error` member of the `--json` failure record. */
108
+ type JsonErrorBody = {
109
+ message: string;
110
+ code?: RollParserErrorCode;
111
+ span?: ErrorSpan | undefined;
112
+ };
113
+
114
+ /**
115
+ * What the failure record needs to replay a roll that got as far as the
116
+ * dice. A usage error passes no context — it never reached the dice, so
117
+ * there is no roll to reproduce.
118
+ */
119
+ type RollContext = {
120
+ notation: string;
121
+ seed: string;
122
+ };
123
+
124
+ /**
125
+ * Writes one failure record as compact JSON. `JSON.stringify` drops the
126
+ * absent members, so a lexer error carries no `end` and a usage error carries
127
+ * neither `code` nor `span` — the key is missing rather than null.
128
+ */
129
+ function writeJsonError(write: WriteFn, error: JsonErrorBody, context?: RollContext): void {
130
+ write(`${JSON.stringify({ error, ...context, version: VERSION })}\n`);
131
+ }
132
+
133
+ /** Reports a usage error and returns the exit code for one. */
134
+ function failUsage(stderr: WriteFn, message: string, json: boolean): number {
135
+ if (json) {
136
+ writeJsonError(stderr, { message });
137
+ } else {
138
+ stderr(`Error: ${message}\n`);
139
+ stderr('Run "roll-parser --help" for usage.\n');
140
+ }
141
+
142
+ return 2;
143
+ }
144
+
77
145
  /**
78
146
  * Runs one CLI invocation and returns the process exit code: `0` on success,
79
147
  * `1` for a roll-parser error, `2` for a usage error. Anything that is not a
80
148
  * `RollParserError` propagates so the runtime reports it with a stack.
81
149
  *
82
- * `--json` swaps the success payload only diagnostics stay plain text on
83
- * stderr and the exit codes are identical, so scripts can branch on the code
84
- * before parsing stdout.
150
+ * `--json` swaps the success payload on stdout and every diagnostic on
151
+ * stderr, usage errors included. Exit codes are identical either way, so
152
+ * scripts can still branch on the code before reading a stream.
85
153
  */
86
154
  export function main(env: CliEnv): number {
87
155
  const { argv, stdout, stderr } = env;
88
156
  const parsed = parseArgs(argv);
89
157
 
90
158
  if (!parsed.ok) {
91
- stderr(`Error: ${parsed.error}\n`);
92
- stderr('Run "roll-parser --help" for usage.\n');
93
- return 2;
159
+ return failUsage(stderr, parsed.error, parsed.json);
94
160
  }
95
161
 
96
- const { args } = parsed;
162
+ const { args, terminated } = parsed;
97
163
 
98
164
  if (args.showHelp) {
99
165
  stdout(HELP_TEXT);
@@ -106,20 +172,29 @@ export function main(env: CliEnv): number {
106
172
  }
107
173
 
108
174
  if (args.notation == null) {
109
- stderr('Error: No dice notation provided.\n');
110
- stderr('Run "roll-parser --help" for usage.\n');
111
- return 2;
175
+ return failUsage(stderr, 'No dice notation provided.', args.json);
112
176
  }
113
177
 
178
+ // `SeededRNG`'s auto-seed is unreachable, so mint one that can be echoed back.
179
+ const seed = args.seed ?? crypto.randomUUID();
180
+
114
181
  try {
115
- const options = args.seed != null ? { seed: args.seed } : {};
116
- const result = roll(args.notation, options);
117
- const output = formatResult(result, { json: args.json, verbose: args.verbose });
182
+ const result = roll(args.notation, { seed });
183
+ const output = formatResult(result, { json: args.json, verbose: args.verbose, seed });
118
184
  stdout(`${output}\n`);
119
185
  } catch (error) {
120
186
  if (isRollParserError(error)) {
121
- stderr(`Error: ${error.message}\n`);
122
- writeErrorContext(args.notation, error, stderr);
187
+ if (args.json) {
188
+ writeJsonError(
189
+ stderr,
190
+ { message: error.message, code: error.code, span: getErrorSpan(error) },
191
+ { notation: args.notation, seed },
192
+ );
193
+ } else {
194
+ stderr(`Error: ${error.message}\n`);
195
+ writeErrorContext(args.notation, error, stderr);
196
+ writeOptionHint(terminated, args.notation, stderr);
197
+ }
123
198
  return 1;
124
199
  }
125
200
  throw error;