fix: escape exported player text, defer the blob revoke, collapse the success predicates - #71
Merged
Merged
Conversation
… success predicates **A pipe in a flourish word broke the exported table (#32).** `paragraphRow` interpolated player text into a Markdown row with no escaping, and the flourish input has no pattern or sanitisation. `bold | brazen` shifted every cell after it; a pasted newline split the row and terminated the table early. The two cells carrying non-constant text are now escaped, and the frontmatter's `scenario:` scalar is quoted — all four bundled titles are plain, but a title with a colon wrote an invalid document. Demonstrated through the real export rather than only in tests: a letter whose flourishes carry a pipe and a newline downloads with `bold \| brazen`, `grand sweeping`, and all seven rows at the header's cell count. **The download revoked its blob URL synchronously (#31).** `a.click()` only queues the download, so revoking in the same task races the fetch; the issue names Safari as where that has produced cancelled or zero-byte files, and was filed from reading rather than from driving a browser. Deferred by one turn of the event loop, and checked in Safari: 837 bytes on disk. **Three names for one rule (#36).** `isSuperior` and `fineHand` were character-for-character identical apart from the parameter name, and since #28 `flourishHeld` was the same body plus a null guard. They collapse to `succeeded(dice)` in `dice.ts`, beside `countSuccesses` where the "5 or 6" rule already lives. It takes `number[] | null` so the third name collapses too — a roll that never happened did not succeed, which is exactly what the Heart roll is when the player writes plainly. The domain vocabulary is untouched: it lives where "Fine hand" and "(superior)" are rendered, and none of those strings change. **Tests that restated the file above them (#53).** Its scope had already shrunk twice — the store cases were rewritten in milestone 3 and the scenario ones deleted in milestone 2. Deleted here: the valid-rating test (`tsc` rejects anything else), the non-empty-flavour test, and the two lookup tests, which tested `Array.prototype.find`. The `monk` and `courtier` fixtures rebuilt in seven of `rules.test.ts`'s tests are hoisted to module scope. Two deliberate departures from #53, both kept rather than deleted: - The skills test, rewritten. The *count* was a literal restating itself, but "every attribute has exactly one skill that boosts it" is a domain invariant — two skills on Penmanship and none on Heart would leave an attribute the player can never help, and nothing else would notice. - `diceForRating`'s poor/average/good mapping, which #53 files under the same heading while hedging. It is a rulebook rule, and the only place the rating-to-dice conversion is pinned. Closes #32 Closes #31 Closes #36 Closes #53 Co-Authored-By: Claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #32, #31, #36, #53. First of two PRs in milestone 7 — the doc deletion follows separately so it doesn't drown this diff.
A pipe in a flourish word broke the exported table (#32)
paragraphRowinterpolated player text into a Markdown row with no escaping, and the flourish input has no pattern, length limit or sanitisation.bold | brazenshifted every cell after it; a pasted newline split the row and terminated the table early. All six existing export tests used the fixture adjective'solemn', so nothing exercised a metacharacter.Demonstrated through the real export, not only in tests — a letter whose flourishes carry a pipe and a newline, downloaded and read off disk:
The frontmatter
scenario:is now quoted too. All four bundled titles are plain, but a title with a colon wrote an invalid document — this is for the next one.The download revoked its blob URL synchronously (#31)
a.click()only queues the download, so revoking in the same task races the fetch. The issue names Safari as where that has produced cancelled or zero-byte files, and notes it was filed from reading — "I did not drive a browser."I did. Deferred by one turn of the event loop, clicked Download in Safari, and the file is 837 bytes on disk.
Three names for one rule (#36)
isSuperiorandfineHandwere character-for-character identical apart from the parameter name, and since #28 landed,flourishHeldwas the same body plus a null guard. They collapse tosucceeded(dice)indice.ts, besidecountSuccesseswhere the "5 or 6" rule already lives.It takes
number[] | null, so the third name collapses too — a roll that never happened did not succeed, which is exactly what the Heart roll is when the player writes plainly. Three exported names become one. The domain vocabulary isn't lost: it lives where'Fine hand'and(superior)are rendered, and none of those strings change.Tests that restated the file above them (#53)
Scope had already shrunk twice — the store cases were rewritten in milestone 3, the scenario ones deleted in milestone 2. Deleted here: the valid-rating test (
tscrejects anything else), the non-empty-flavour test, and the two lookup tests, which testedArray.prototype.find. Themonk/courtierfixtures rebuilt in seven ofrules.test.ts's tests are hoisted.Two deliberate departures from #53, both kept rather than deleted:
diceForRating's poor/average/good mapping, which A quarter of the test suite asserts that literals are what the file above them says they are #53 files under the same heading while hedging ("cheap and I would not fight for them"). It's a rulebook rule and the only place that conversion is pinned.Verification
bun run check:ciclean. 86 → 82 tests: four tautological ones deleted, three real ones added for the escaping.One existing test caught the frontmatter change on its own —
frontmatter contains character, skill, scenario…asserted the unquoted form and had to be updated, which is the assertion doing its job.