roll-parser 3.0.0-alpha.0 → 3.0.0-beta.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 (80) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +166 -30
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/format.d.ts +1 -1
  5. package/dist/cli/format.d.ts.map +1 -1
  6. package/dist/cli.js +1110 -277
  7. package/dist/cli.js.map +28 -0
  8. package/dist/errors.d.ts +7 -4
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/evaluator/evaluator.d.ts +63 -7
  11. package/dist/evaluator/evaluator.d.ts.map +1 -1
  12. package/dist/evaluator/index.d.ts +2 -2
  13. package/dist/evaluator/index.d.ts.map +1 -1
  14. package/dist/evaluator/modifiers/compare.d.ts +1 -1
  15. package/dist/evaluator/modifiers/compare.d.ts.map +1 -1
  16. package/dist/evaluator/modifiers/crit-threshold.d.ts +28 -0
  17. package/dist/evaluator/modifiers/crit-threshold.d.ts.map +1 -0
  18. package/dist/evaluator/modifiers/explode.d.ts +8 -4
  19. package/dist/evaluator/modifiers/explode.d.ts.map +1 -1
  20. package/dist/evaluator/modifiers/keep-drop.d.ts +1 -1
  21. package/dist/evaluator/modifiers/keep-drop.d.ts.map +1 -1
  22. package/dist/evaluator/modifiers/reroll.d.ts +4 -4
  23. package/dist/evaluator/modifiers/reroll.d.ts.map +1 -1
  24. package/dist/evaluator/modifiers/sort.d.ts +23 -0
  25. package/dist/evaluator/modifiers/sort.d.ts.map +1 -0
  26. package/dist/evaluator/modifiers/success-count.d.ts +1 -1
  27. package/dist/evaluator/modifiers/success-count.d.ts.map +1 -1
  28. package/dist/index.d.ts +13 -13
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1112 -275
  31. package/dist/index.js.map +26 -0
  32. package/dist/lexer/lexer.d.ts +18 -3
  33. package/dist/lexer/lexer.d.ts.map +1 -1
  34. package/dist/lexer/tokens.d.ts +22 -2
  35. package/dist/lexer/tokens.d.ts.map +1 -1
  36. package/dist/parser/ast.d.ts +209 -24
  37. package/dist/parser/ast.d.ts.map +1 -1
  38. package/dist/parser/parser.d.ts +48 -5
  39. package/dist/parser/parser.d.ts.map +1 -1
  40. package/dist/rng/index.d.ts +2 -2
  41. package/dist/rng/index.d.ts.map +1 -1
  42. package/dist/rng/mock.d.ts +1 -1
  43. package/dist/rng/mock.d.ts.map +1 -1
  44. package/dist/rng/seeded.d.ts +8 -1
  45. package/dist/rng/seeded.d.ts.map +1 -1
  46. package/dist/roll.d.ts +6 -2
  47. package/dist/roll.d.ts.map +1 -1
  48. package/dist/testing.d.ts +1 -1
  49. package/dist/testing.d.ts.map +1 -1
  50. package/dist/testing.js +3 -0
  51. package/dist/testing.js.map +11 -0
  52. package/dist/types.d.ts +143 -2
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +22 -18
  55. package/src/cli/args.ts +2 -1
  56. package/src/cli/format.ts +8 -4
  57. package/src/cli/index.ts +22 -5
  58. package/src/errors.ts +15 -3
  59. package/src/evaluator/evaluator.ts +826 -111
  60. package/src/evaluator/index.ts +2 -2
  61. package/src/evaluator/modifiers/compare.ts +1 -1
  62. package/src/evaluator/modifiers/crit-threshold.ts +59 -0
  63. package/src/evaluator/modifiers/explode.ts +29 -25
  64. package/src/evaluator/modifiers/keep-drop.ts +1 -1
  65. package/src/evaluator/modifiers/reroll.ts +18 -25
  66. package/src/evaluator/modifiers/sort.ts +30 -0
  67. package/src/evaluator/modifiers/success-count.ts +2 -2
  68. package/src/index.ts +33 -15
  69. package/src/lexer/lexer.ts +101 -8
  70. package/src/lexer/tokens.ts +42 -2
  71. package/src/parser/ast.ts +397 -30
  72. package/src/parser/parser.ts +590 -67
  73. package/src/rng/index.ts +2 -2
  74. package/src/rng/mock.ts +1 -1
  75. package/src/rng/seeded.ts +31 -1
  76. package/src/roll.ts +14 -6
  77. package/src/testing.ts +1 -1
  78. package/src/types.ts +127 -2
  79. package/dist/index.mjs +0 -1724
  80. package/dist/testing.mjs +0 -39
package/dist/testing.js CHANGED
@@ -36,3 +36,6 @@ export {
36
36
  createMockRng2 as createMockRng,
37
37
  MockRNGExhaustedError2 as MockRNGExhaustedError
38
38
  };
39
+
40
+ //# debugId=916ECEA68894E64864756E2164756E21
41
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/rng/mock.ts", "../src/testing.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Mock RNG for deterministic testing.\n *\n * @module rng/mock\n */\n\nimport type { RNG } from './types.js';\n\n/**\n * Error thrown when MockRNG exhausts its predefined values.\n *\n * This is intentional behavior to catch incorrect roll counts in tests.\n * If you see this error, your test is consuming more random values than expected.\n */\nexport class MockRNGExhaustedError extends Error {\n readonly consumed: number;\n\n constructor(consumed: number) {\n super(`MockRNG exhausted: consumed ${consumed} values, no more available`);\n this.name = 'MockRNGExhaustedError';\n this.consumed = consumed;\n }\n}\n\n/**\n * Creates a mock RNG that returns predefined values in sequence.\n *\n * IMPORTANT: Throws MockRNGExhaustedError when all values are consumed.\n * This behavior catches incorrect roll counts in tests - it never wraps around.\n *\n * @param values - Array of values to return (dice results for nextInt, floats for next)\n * @returns RNG instance returning predefined values\n *\n * @example\n * ```typescript\n * const rng = createMockRng([4, 2, 6]);\n * rng.nextInt(1, 6); // Returns 4\n * rng.nextInt(1, 6); // Returns 2\n * rng.nextInt(1, 6); // Returns 6\n * rng.nextInt(1, 6); // Throws MockRNGExhaustedError\n * ```\n */\nexport function createMockRng(values: number[]): RNG {\n let index = 0;\n\n const getNext = (): number => {\n const value = values[index];\n if (value === undefined) {\n throw new MockRNGExhaustedError(index);\n }\n index++;\n return value;\n };\n\n return {\n next: getNext,\n nextInt: (min: number, max: number): number => {\n const value = getNext();\n if (value < min || value > max) {\n throw new RangeError(`MockRNG value ${value} is out of bounds [${min}, ${max}]`);\n }\n return value;\n },\n };\n}\n",
6
+ "/**\n * Test utilities for roll-parser consumers.\n *\n * Import from `roll-parser/testing` for deterministic dice testing.\n *\n * @module testing\n */\n\n// Direct value exports force the bundler to inline the code\nimport {\n MockRNGExhaustedError as _MockRNGExhaustedError,\n createMockRng as _createMockRng,\n} from './rng/mock.js';\n\nexport const createMockRng = _createMockRng;\nexport const MockRNGExhaustedError = _MockRNGExhaustedError;\n"
7
+ ],
8
+ "mappings": ";AAcO,MAAM,8BAA8B,MAAM;AAAA,EACtC;AAAA,EAET,WAAW,CAAC,UAAkB;AAAA,IAC5B,MAAM,+BAA+B,oCAAoC;AAAA,IACzE,KAAK,OAAO;AAAA,IACZ,KAAK,WAAW;AAAA;AAEpB;AAoBO,SAAS,aAAa,CAAC,QAAuB;AAAA,EACnD,IAAI,QAAQ;AAAA,EAEZ,MAAM,UAAU,MAAc;AAAA,IAC5B,MAAM,QAAQ,OAAO;AAAA,IACrB,IAAI,UAAU,WAAW;AAAA,MACvB,MAAM,IAAI,sBAAsB,KAAK;AAAA,IACvC;AAAA,IACA;AAAA,IACA,OAAO;AAAA;AAAA,EAGT,OAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,CAAC,KAAa,QAAwB;AAAA,MAC7C,MAAM,QAAQ,QAAQ;AAAA,MACtB,IAAI,QAAQ,OAAO,QAAQ,KAAK;AAAA,QAC9B,MAAM,IAAI,WAAW,iBAAiB,2BAA2B,QAAQ,MAAM;AAAA,MACjF;AAAA,MACA,OAAO;AAAA;AAAA,EAEX;AAAA;;;ACjDK,IAAM,iBAAgB;AACtB,IAAM,yBAAwB;",
9
+ "debugId": "916ECEA68894E64864756E2164756E21",
10
+ "names": []
11
+ }
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module types
5
5
  */
6
- import type { ASTNode } from './parser/ast';
6
+ import type { ASTNode } from './parser/ast.js';
7
7
  /**
8
8
  * Comparison operator for compare points.
9
9
  */
@@ -18,10 +18,23 @@ export type ComparePoint = {
18
18
  operator: CompareOp;
19
19
  value: ASTNode;
20
20
  };
21
+ /**
22
+ * A ComparePoint whose value has been evaluated to a number. Used in
23
+ * `RollPart` where meta-expressions are already resolved.
24
+ */
25
+ export type ResolvedComparePoint = {
26
+ operator: CompareOp;
27
+ value: number;
28
+ };
29
+ /**
30
+ * A resolved crit threshold — `'default'` means the per-die default rule
31
+ * (`result === sides` for critical, `result === 1` for fumble).
32
+ */
33
+ export type ResolvedCritThreshold = ResolvedComparePoint | 'default';
21
34
  /**
22
35
  * Modifier flags applied to individual die results.
23
36
  */
24
- export type DieModifier = 'dropped' | 'kept' | 'exploded' | 'rerolled' | 'success' | 'failure';
37
+ export type DieModifier = 'dropped' | 'kept' | 'exploded' | 'rerolled' | 'success' | 'failure' | 'meta';
25
38
  /**
26
39
  * PF2e Degree of Success. Produced by the `vs` operator when comparing a
27
40
  * roll against a Difficulty Class. Ordering is significant — natural 20
@@ -45,6 +58,13 @@ export type DieResult = {
45
58
  sides: number;
46
59
  /** The rolled value */
47
60
  result: number;
61
+ /**
62
+ * Raw first roll before any mutation (e.g., compound-explode accumulation).
63
+ * Only populated when `result` has been overwritten with a computed value.
64
+ * Consumers that need the original face (nat-20 / nat-1 detection) should
65
+ * read `initialResult ?? result`.
66
+ */
67
+ initialResult?: number;
48
68
  /** Modifiers applied to this die */
49
69
  modifiers: DieModifier[];
50
70
  /** True if rolled the maximum value (always false for Fate dice) */
@@ -52,6 +72,120 @@ export type DieResult = {
52
72
  /** True if rolled 1 (always false for Fate dice) */
53
73
  fumble: boolean;
54
74
  };
75
+ /**
76
+ * Per-spec keep/drop entry inside a flattened modifier chain. Counts are
77
+ * resolved at evaluation time (meta-expressions like `kh(1d2)` become the
78
+ * rolled number).
79
+ */
80
+ export type ModifierSpec = {
81
+ kind: 'keep' | 'drop';
82
+ selector: 'highest' | 'lowest';
83
+ count: number;
84
+ };
85
+ /**
86
+ * Fields shared by every RollPart variant. `start`/`end` mirror the source
87
+ * span of the AST node the part was evaluated from — present whenever the
88
+ * AST came from `parse()`, absent on hand-built ASTs.
89
+ */
90
+ type RollPartBase = {
91
+ /** Sub-total this part contributed to its parent. */
92
+ total: number;
93
+ start?: number;
94
+ end?: number;
95
+ };
96
+ /**
97
+ * Structured breakdown of an evaluated expression, mirroring the AST 1:1 —
98
+ * every ASTNode produces exactly one RollPart. Discriminants are lowercase
99
+ * camelCase to distinguish evaluation-tree types from `ASTNode.type`
100
+ * (PascalCase) at a glance.
101
+ *
102
+ * Invariants:
103
+ * - `RollResult.parts.total === RollResult.total`.
104
+ * - `successCount.total === successes - failures`.
105
+ * - `literal.total === value` and `variable.total === value`.
106
+ * - Each part's `rolls[]` shares `DieResult` references with
107
+ * `RollResult.rolls[]`; both reflect post-evaluation state (explode
108
+ * accumulation, reroll flags, keep/drop flags). No deep clone.
109
+ *
110
+ * Meta-expression sub-trees (`4d6kh(1d2)`, `(1+1)d6` counts/sides, computed
111
+ * thresholds) are not surfaced as nested parts — their resolved numbers
112
+ * appear in the owning part, and their dice are inspectable in
113
+ * `RollResult.rolls` via the `'meta'` modifier tag.
114
+ */
115
+ export type RollPart = (RollPartBase & {
116
+ type: 'literal';
117
+ value: number;
118
+ }) | (RollPartBase & {
119
+ type: 'variable';
120
+ name: string;
121
+ value: number;
122
+ }) | (RollPartBase & {
123
+ type: 'dice';
124
+ count: number;
125
+ sides: number;
126
+ rolls: DieResult[];
127
+ }) | (RollPartBase & {
128
+ type: 'fateDice';
129
+ count: number;
130
+ rolls: DieResult[];
131
+ }) | (RollPartBase & {
132
+ type: 'grouped';
133
+ inner: RollPart;
134
+ }) | (RollPartBase & {
135
+ type: 'binaryOp';
136
+ operator: '+' | '-' | '*' | '/' | '%' | '**';
137
+ left: RollPart;
138
+ right: RollPart;
139
+ }) | (RollPartBase & {
140
+ type: 'unaryOp';
141
+ operator: '-';
142
+ operand: RollPart;
143
+ }) | (RollPartBase & {
144
+ type: 'modifier';
145
+ specs: ModifierSpec[];
146
+ target: RollPart;
147
+ }) | (RollPartBase & {
148
+ type: 'explode';
149
+ variant: 'standard' | 'compound' | 'penetrating';
150
+ threshold?: ResolvedComparePoint;
151
+ target: RollPart;
152
+ }) | (RollPartBase & {
153
+ type: 'reroll';
154
+ once: boolean;
155
+ condition: ResolvedComparePoint;
156
+ target: RollPart;
157
+ }) | (RollPartBase & {
158
+ type: 'successCount';
159
+ threshold: ResolvedComparePoint;
160
+ failThreshold?: ResolvedComparePoint;
161
+ target: RollPart;
162
+ successes: number;
163
+ failures: number;
164
+ }) | (RollPartBase & {
165
+ type: 'versus';
166
+ roll: RollPart;
167
+ dc: RollPart;
168
+ degree: DegreeOfSuccess;
169
+ }) | (RollPartBase & {
170
+ type: 'functionCall';
171
+ name: string;
172
+ args: RollPart[];
173
+ }) | (RollPartBase & {
174
+ type: 'group';
175
+ parts: RollPart[];
176
+ keptIndices?: number[];
177
+ }) | (RollPartBase & {
178
+ type: 'sort';
179
+ order: 'ascending' | 'descending';
180
+ target: RollPart;
181
+ }) | (RollPartBase & {
182
+ type: 'critThreshold';
183
+ successThresholds: ResolvedCritThreshold[];
184
+ failThresholds: ResolvedCritThreshold[];
185
+ target: RollPart;
186
+ });
187
+ /** Convenience alias for consumers writing exhaustive switches. */
188
+ export type RollPartType = RollPart['type'];
55
189
  /**
56
190
  * Complete roll result with all metadata.
57
191
  */
@@ -66,6 +200,8 @@ export type RollResult = {
66
200
  rendered: string;
67
201
  /** All individual die results */
68
202
  rolls: DieResult[];
203
+ /** Structured breakdown of the evaluated expression, mirroring the AST 1:1. */
204
+ parts: RollPart;
69
205
  /**
70
206
  * Number of dice tagged as success across the whole expression. Present
71
207
  * only when a success-counting modifier was used. Independent of `total` —
@@ -101,5 +237,10 @@ export type EvaluateOptions = {
101
237
  maxExplodeIterations?: number;
102
238
  /** Maximum reroll iterations allowed per die (default: 1,000) */
103
239
  maxRerollIterations?: number;
240
+ /** Variable context for `@name` / `@{name}` references (default: empty) */
241
+ context?: Record<string, number>;
242
+ /** Behavior when a referenced variable is missing from context (default: 'throw') */
243
+ onMissingVariable?: 'throw' | 'zero';
104
244
  };
245
+ export {};
105
246
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/F;;;;GAIG;AACH,oBAAY,eAAe;IACzB,eAAe,IAAI;IACnB,OAAO,IAAI;IACX,OAAO,IAAI;IACX,eAAe,IAAI;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,28 +1,29 @@
1
1
  {
2
2
  "name": "roll-parser",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.0.0-beta.0",
4
4
  "description": "High-performance dice notation parser for tabletop RPGs. TypeScript-first, Bun-optimized.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
- "module": "./dist/index.mjs",
8
7
  "types": "./dist/index.d.ts",
9
- "bin": { "roll-parser": "./dist/cli.js" },
8
+ "bin": {
9
+ "roll-parser": "./dist/cli.js"
10
+ },
10
11
  "sideEffects": false,
11
12
  "exports": {
12
13
  ".": {
13
14
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.mjs",
15
- "require": "./dist/index.js"
15
+ "default": "./dist/index.js"
16
16
  },
17
17
  "./testing": {
18
18
  "types": "./dist/testing.d.ts",
19
- "import": "./dist/testing.mjs",
20
- "require": "./dist/testing.js"
21
- }
19
+ "default": "./dist/testing.js"
20
+ },
21
+ "./package.json": "./package.json"
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
25
  "src",
26
+ "CHANGELOG.md",
26
27
  "!src/**/*.test.ts"
27
28
  ],
28
29
  "keywords": [
@@ -60,26 +61,29 @@
60
61
  "check:version": "bun scripts/check-version.ts",
61
62
  "check:changelog": "bun scripts/check-changelog.ts",
62
63
  "clean": "rm -rf dist coverage",
63
- "build": "bun run clean && bun run build:esm && bun run build:cjs && bun run build:esm:testing && bun run build:cjs:testing && bun run build:cli && bun run build:types",
64
- "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target bun",
65
- "build:cjs": "bun build src/index.ts --outfile dist/index.js --target node",
66
- "build:esm:testing": "bun build src/testing.ts --outfile dist/testing.mjs --target bun",
67
- "build:cjs:testing": "bun build src/testing.ts --outfile dist/testing.js --target node",
68
- "build:cli": "bun build src/cli/index.ts --outfile dist/cli.js --target node",
64
+ "build": "bun run clean && bun run build:lib && bun run build:testing && bun run build:cli && bun run build:types",
65
+ "build:lib": "bun build src/index.ts --outdir dist --target node --sourcemap=linked",
66
+ "build:testing": "bun build src/testing.ts --outdir dist --target node --sourcemap=linked",
67
+ "build:cli": "bun build src/cli/index.ts --outdir dist --entry-naming 'cli.[ext]' --target node --sourcemap=linked",
69
68
  "build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
69
+ "check:package": "attw --pack . --profile esm-only && publint",
70
+ "bench": "bun run bench/roll.bench.ts",
70
71
  "test": "bun test",
71
- "test:node": "node -e \"import('./dist/index.mjs').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })\" && node -e \"const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('CJS smoke failed: total=' + r.total); console.log('Node.js CJS smoke: ok');\"",
72
+ "test:node": "node -e \"import('./dist/index.js').then(m => { const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('ESM smoke failed: total=' + r.total); console.log('Node.js ESM smoke: ok'); })\" && node -e \"const m = require('./dist/index.js'); const r = m.roll('1d6'); if (!(r.total >= 1 && r.total <= 6)) throw new Error('require(esm) smoke failed: total=' + r.total); console.log('Node.js require(esm) smoke: ok');\"",
72
73
  "test:watch": "bun test --watch",
73
- "test:ci": "bun test --bail",
74
- "validate": "bun run check && bun run build && bun test:ci",
74
+ "test:ci": "bun test --bail --coverage",
75
+ "validate": "bun run check && bun run build && bun run check:package && bun test:ci",
75
76
  "coverage": "bun test --coverage",
76
- "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun test",
77
+ "release:dry": "bun run check:version && bun run check:changelog && bun run check && bun run build && bun run check:package && bun test",
77
78
  "prepublishOnly": "bun run release:dry"
78
79
  },
79
80
  "devDependencies": {
81
+ "@arethetypeswrong/cli": "^0.18.4",
80
82
  "@biomejs/biome": "^2.4.9",
81
83
  "@types/bun": "^1.3.0",
82
84
  "fast-check": "^4.6.0",
85
+ "mitata": "^1.0.34",
86
+ "publint": "^0.3.21",
83
87
  "typescript": "^6.0.2"
84
88
  },
85
89
  "engines": {
package/src/cli/args.ts CHANGED
@@ -59,7 +59,8 @@ export function parseArgs(argv: string[]): ParseArgsResult {
59
59
  seed = value;
60
60
  } else if (arg.startsWith('--')) {
61
61
  return { ok: false, error: `Unknown option: ${arg}` };
62
- } else if (arg.startsWith('-') && arg.length > 1 && !/^\d/.test(arg.slice(1))) {
62
+ } else if (arg.startsWith('-') && arg.length > 1 && !/^[\ddD(]/.test(arg.slice(1))) {
63
+ // Negative prefix notation (-3, -d6, -D6, -dF, -(2d6)) stays positional
63
64
  return { ok: false, error: `Unknown option: ${arg}` };
64
65
  } else {
65
66
  positional.push(arg);
package/src/cli/format.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * @module cli/format
7
7
  */
8
8
 
9
- import type { RollResult } from '../types';
9
+ import type { RollResult } from '../types.js';
10
10
 
11
11
  /**
12
12
  * Formats a roll result for terminal display.
@@ -30,16 +30,20 @@ export function formatResult(result: RollResult, verbose: boolean): string {
30
30
  * Converts markdown-style dice markers to terminal-friendly forms.
31
31
  *
32
32
  * The evaluator uses markdown syntax in the rendered field:
33
- * `~~value~~` — dropped dice
33
+ * `~~value~~` — dropped dice or dropped group sub-rolls
34
34
  * `**value**` — dice counted as success
35
35
  * `__value__` — dice counted as failure
36
36
  *
37
37
  * For plain terminals these become `(value)`, `[value]`, and `{value}` so
38
38
  * the per-die classification stays visible without any markup dependency.
39
+ * Dropped spans can wrap a whole sub-roll (e.g. `~~1d8[2]~~` from
40
+ * `{1d8, 1d10}kh1`), so the strikethrough pattern accepts any tilde-free
41
+ * content, not just a single number. The evaluator never nests `~~`
42
+ * (see `stripInnerMarkers`), so the tilde-free span match is safe.
39
43
  */
40
44
  function formatRendered(rendered: string): string {
41
45
  return rendered
42
- .replace(/~~(-?\d+)~~/g, '($1)')
43
46
  .replace(/\*\*(-?\d+)\*\*/g, '[$1]')
44
- .replace(/__(-?\d+)__/g, '{$1}');
47
+ .replace(/__(-?\d+)__/g, '{$1}')
48
+ .replace(/~~([^~]+)~~/g, '($1)');
45
49
  }
package/src/cli/index.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  * @module cli/index
7
7
  */
8
8
 
9
- import { isRollParserError } from '../errors';
10
- import { VERSION } from '../index';
11
- import { roll } from '../roll';
12
- import { parseArgs } from './args';
13
- import { formatResult } from './format';
9
+ import { isRollParserError } from '../errors.js';
10
+ import { VERSION } from '../index.js';
11
+ import { roll } from '../roll.js';
12
+ import { parseArgs } from './args.js';
13
+ import { formatResult } from './format.js';
14
14
 
15
15
  const HELP_TEXT = `roll-parser v${VERSION}
16
16
 
@@ -28,6 +28,22 @@ Examples:
28
28
  roll-parser 4d6dl1 --seed "character-str"
29
29
  `;
30
30
 
31
+ /**
32
+ * Prints the notation with a caret under the error position for errors that
33
+ * carry one (LexerError/ParseError `position`, EvaluatorError `start`).
34
+ * Skipped for multi-line notations and out-of-range positions to keep the
35
+ * caret honest.
36
+ */
37
+ function writeErrorContext(notation: string, error: Error): void {
38
+ const position =
39
+ 'position' in error ? error.position : 'start' in error ? error.start : undefined;
40
+ if (typeof position !== 'number' || !Number.isInteger(position)) return;
41
+ if (notation.includes('\n') || position < 0 || position > notation.length) return;
42
+
43
+ process.stderr.write(` ${notation}\n`);
44
+ process.stderr.write(` ${' '.repeat(position)}^\n`);
45
+ }
46
+
31
47
  function main(): void {
32
48
  const parsed = parseArgs(process.argv.slice(2));
33
49
 
@@ -65,6 +81,7 @@ function main(): void {
65
81
  } catch (error) {
66
82
  if (isRollParserError(error)) {
67
83
  process.stderr.write(`Error: ${error.message}\n`);
84
+ writeErrorContext(args.notation, error);
68
85
  process.exitCode = 1;
69
86
  return;
70
87
  }
package/src/errors.ts CHANGED
@@ -11,12 +11,15 @@
11
11
  *
12
12
  * Lexer: `UNEXPECTED_CHARACTER`, `UNEXPECTED_IDENTIFIER`
13
13
  * Parser: `UNEXPECTED_TOKEN`, `UNEXPECTED_END`, `EXPECTED_TOKEN`,
14
- * `INVALID_EXPLODE_TARGET`, `INVALID_SUCCESS_COUNT_TARGET`, `NESTED_VERSUS`,
15
- * `INVALID_FUNCTION_ARITY`
14
+ * `INVALID_MODIFIER_TARGET`, `INVALID_EXPLODE_TARGET`, `INVALID_REROLL_TARGET`,
15
+ * `INVALID_SUCCESS_COUNT_TARGET`, `INVALID_SORT_TARGET`,
16
+ * `INVALID_CRIT_THRESHOLD_TARGET`, `NESTED_VERSUS`, `INVALID_FUNCTION_ARITY`,
17
+ * `AMBIGUOUS_DICE_CHAIN`, `MAX_DEPTH_EXCEEDED`
16
18
  * Evaluator: `INVALID_DICE_COUNT`, `INVALID_DICE_SIDES`, `DICE_LIMIT_EXCEEDED`,
17
19
  * `DIVISION_BY_ZERO`, `MODULO_BY_ZERO`, `UNKNOWN_OPERATOR`, `UNKNOWN_NODE_TYPE`,
18
20
  * `INVALID_MODIFIER_COUNT`, `EXPLODE_LIMIT_EXCEEDED`, `REROLL_LIMIT_EXCEEDED`,
19
- * `INVALID_THRESHOLD`, `NESTED_VERSUS`, `UNKNOWN_FUNCTION`
21
+ * `INVALID_THRESHOLD`, `NESTED_VERSUS`, `UNKNOWN_FUNCTION`, `UNDEFINED_VARIABLE`,
22
+ * `INVALID_VARIABLE_VALUE`, `NON_FINITE_RESULT`
20
23
  */
21
24
  const ROLL_PARSER_ERROR_CODES = [
22
25
  'UNEXPECTED_CHARACTER',
@@ -32,14 +35,23 @@ const ROLL_PARSER_ERROR_CODES = [
32
35
  'UNKNOWN_OPERATOR',
33
36
  'UNKNOWN_NODE_TYPE',
34
37
  'INVALID_MODIFIER_COUNT',
38
+ 'INVALID_MODIFIER_TARGET',
35
39
  'EXPLODE_LIMIT_EXCEEDED',
36
40
  'INVALID_EXPLODE_TARGET',
37
41
  'REROLL_LIMIT_EXCEEDED',
42
+ 'INVALID_REROLL_TARGET',
38
43
  'INVALID_SUCCESS_COUNT_TARGET',
44
+ 'INVALID_SORT_TARGET',
45
+ 'INVALID_CRIT_THRESHOLD_TARGET',
39
46
  'INVALID_THRESHOLD',
40
47
  'NESTED_VERSUS',
41
48
  'INVALID_FUNCTION_ARITY',
42
49
  'UNKNOWN_FUNCTION',
50
+ 'UNDEFINED_VARIABLE',
51
+ 'INVALID_VARIABLE_VALUE',
52
+ 'AMBIGUOUS_DICE_CHAIN',
53
+ 'MAX_DEPTH_EXCEEDED',
54
+ 'NON_FINITE_RESULT',
43
55
  ] as const;
44
56
 
45
57
  export type RollParserErrorCode = (typeof ROLL_PARSER_ERROR_CODES)[number];