docs: move the guide content into the site - #83
Conversation
058e31b to
087204f
Compare
31c932c to
6b6c5e5
Compare
384f0e7 to
caf7b14
Compare
soimy
left a comment
There was a problem hiding this comment.
Stack #86 review of current head caf7b14b87e480237e50e4e3a69091e8e9466425.
The documentation migration is broadly coherent and the Node 20/22/24 CI matrix is green, including verify:docs, docs:build, coverage, and verify:package. I found three documentation-correctness issues below. They are all in the canonical behavior/user documentation introduced by this PR, so I recommend fixing them before this layer is marked ready.
The README was doing four jobs at once — project entry, install, tutorial and API reference — and CONTRIBUTING carried a Jest/CommonJS description of a vitest/ESM suite plus test and coverage figures that were three revisions stale. The detail now lives where the navigation can reach it: - `docs/user/` gets the guide: getting started, options, packing, rotation and tags, repacking, persistence and troubleshooting. The pages were written against the source and the built bundle rather than copied: the rotation example now uses a rect that only fits rotated (the old one claimed `rot: true` for a rect the packer leaves alone — measured), the `next()` description no longer promises a bin it does not return, and `save()`/`load()` say plainly that they carry free space only. - `docs/contributor/` gets development, testing, architecture, behaviour contracts, compatibility, documentation and releasing, which is the material that used to be spread over AGENTS.md and CONTRIBUTING.md. - `README.md` is an entry point again: what it is, install, one runnable example, and links to the guide, the API reference and the contributor pages. - `CONTRIBUTING.md` keeps the rules a first-time contributor needs and points at the contributor pages for depth; the Jest/CommonJS description and the stale baseline are gone. - `AGENTS.md` gains the documentation map and the routing rules from the proposal (user details to `docs/user/`, designs to `docs/spec/YYYY-MM-DD-<topic>.md`, plans to `docs/plans/`, never edit generated API markdown), and its clone invariant is condensed to the rule plus a link to the full contracts page. - The site navigation covers the guide, the contributor pages, releases and the generated API sidebar. The three principal examples are now checked: `verify-package.mjs` runs every block marked `<!-- docs-example: name -->` in the installed tarball's consumer, so a guide snippet that stops working fails the package gate instead of rotting on the README and the site.
The check runs the command under test in a disposable fixture and compares inventories of every source file, so the guide says that instead of describing the sentinel-per-directory it used to watch.
`docs:build` also writes the legacy redirects and runs the output check now, so the command table and the boundary section say so.
The home page was a hero and one sentence. It now says what the library optimizes and links into the guide, the API reference, the contributor pages and the release notes — the same job the README does for the repository, with the site's own navigation.
An adversarial pass over every guide page, checking each claim against `src/`,
the specs and measurements on the built bundle, found seventeen passages that
were wrong or overstated. The measured ones:
- `load()` does not replace the bins: it writes over them by index, so a 3-bin
packer that loads 1 still holds 3 (`[0, 1, 1]` rects) — the surplus keeps
what it had.
- Per-rect rotation *is* reachable — `add(w, h, { allowRotation: true })`
builds a `Rectangle` whose own `_allowRotation` `place()` honours, which the
"Per rectangle allow rotation" spec pins. What stays true is that a plain
object's field is ignored and that it never overrides the oversized check.
- A rect that already fits unrotated can come back rotated: both orientations
are scored and the rotated one can win (800×300 in a 1000×950 bin → `rot:
true`, 300×800).
- The tag falls back to `rect.tag` when `data.tag` is absent, one tag group can
span several bins, and the bins `addArray()` opens in non-exclusive mode carry
no tag at all.
- The sort tie-break reads a caller-supplied `hash`, it is not computed from the
dimensions.
- `OversizedElementBin.clone()` re-derives `data` from its rect and does not
carry a bin-level `data`.
- A rect only counts as oversized when it fits in *neither* orientation (640×256
in a 512×1024 packer rotates into a normal bin).
Also: the site home no longer claims a minimum bin count, and two contributor
pages lose stale claims — `EDGE_MAX_VALUE` is the default edge size, not unused,
and `dist/maxrects-packer.d.ts` re-exports five names.
Auditing every JSDoc comment against the code and the bundle turned up nine places
where the generated API page described something the library does not do. The
public ones, each measured before rewriting:
- `MaxRectsBin.clone()` promised "the same size, options, tag, data and rects". The
copies are re-packed, so a bin holding rotated rects comes back with different
placements (17x15 with two rotated rects -> 15x18 with none), and an untouched
16x19 bin made `clone()` throw. The `@returns` and the throw paragraph now say so,
and `docs/plans/deferred-work.md` carries both reproducers plus a fix direction
this branch verified through the public API but deliberately did not implement —
changing what `clone()` returns is its own PR.
- `Rectangle.oversized` read as "bigger than the packer itself". It also flags a
rect no bin could grow to hold: 1000x2000 fits a 1024x2048 packer and still lands
in an `OversizedElementBin` under `square`.
- `next()` said it returns a new bin; it returns the index the next bin will take
and creates nothing (`bins.length` unchanged).
- `load()` said it overwrites existing bins; it replaces by index, appends an
oversized one, and keeps whatever is past the loaded array.
- `OversizedElementBin.clone()` promised the same `data`; a two-argument source
reports `null` and its copy `{}`.
- `Rectangle.Clone`'s `@returns` now matches its prose: the source's own values win
over whatever the class's `clone()` returned.
Three more are source-only (`private`, so off the site): `updateBinSize`'s
`considerRotation` only applies when `options.allowRotation` is on, and `sort()`'s
`logic` is the `PACKING_LOGIC` enum, not the strings "area"/"edge" — following the
old text returned the MAX_AREA order silently.
`docs/contributor/behavior-contracts.md` also stops presenting the replay's
placement fidelity as a guarantee, and the ledger gains the finding with its table.
Two of three CI legs failed on this branch with `Test timed out in 5000ms` in `test/efficiency.spec.js > combined best of`, while the third passed on the same commit and re-running the jobs turned all three green. The test measures the whole candidate table: 3328ms on a green run against vitest's 5s default. Recorded in the ledger with its own fix (an explicit timeout for that measurement) rather than changed here, since what a gate tolerates is its own decision.
This page described `scripts/verify-docs-output.mjs` and the three things it asserts, but the script arrives with the deployment phase — the same statement-ahead-of-its-branch shape the review flagged on AGENTS.md. The description moves to the commit that introduces the script, where it can also be written against what the check actually does today (pages, links and anchors, nav reachability, search index, base prefix) rather than the three assertions it started with.
Scanning the built pages for unrendered markdown turned up one real artefact:
VitePress strips `{#custom-id}` from a heading's text but leaves it in the
permalink's `aria-label`, so a screen reader reads the braces out. Measured on
the behaviour-contracts page — nine headings use the syntax, eight of them show
the braces in the label, and exactly one of those anchors is linked from
elsewhere. Recorded with both ways out rather than changed on the spot, because
dropping the eight would turn clean anchors into `_2-tag-grouping`-style slugs.
5094e97 to
dd874d0
Compare
The behaviour-contracts page said both "return that same object", but `addArray()` has no return statement: its return type is `void` and callers get `undefined`. The invariant in `AGENTS.md` carried the same wording and is corrected with it.
`maxWidth`/`maxHeight` are not a bound "every bin stays inside": an oversized rect gets an `OversizedElementBin` sized to the rect itself, so `packer.bins[0].maxWidth` can exceed the packer's width. The options page and the "What a bin holds" list now say normal bin, and point at the oversized section.
A saved bin whose `maxWidth`/`maxHeight` exceeds the packer's is not restored at its index: `load()` appends an `OversizedElementBin` of the saved `width` x `height` and ignores the index, so it carries no options or tag and a later entry can overwrite it. Measured on the sources; the page states the append and the ledger records the overwrite.
The hero tagline promised bins "each staying inside maxWidth x maxHeight", which the oversized placeholder bin does not: it is sized to the rect. The tagline now names that exception, as the options and packing pages do.
|
One more of the same class, found while auditing the contributor pages line by line against the repository: the site home repeated the absolute bound. The hero tagline read "Packs rectangles into few bins, each staying inside maxWidth × maxHeight", which an The rest of that audit came back clean, with the measurements behind it: |
|
The gate looked for `<!-- docs-example -->` markers in README.md and two named guides, so a marker anywhere else escaped it while appearing covered. It now walks the published pages (`docs/`, minus the generated `api/` and the excluded `spec/` and `plans/`). Measured: with the persistence example's import removed, the old list passed and the new one exits 1 on `ReferenceError: MaxRectsPacker is not defined` in `example-persistence.mjs`.
The persistence page's example called `packer.save()` on a packer it never declared and never imported `MaxRectsPacker`, so copying it failed at the first line. It is two labelled, runnable runs now, and it is marked for the example gate. Both it and troubleshooting said already-placed rects "have to be re-added by the caller", which is the wrong move: the restored free space already has their area taken out. Measured on a 10x10 bin holding a 6x10 rect (a 4x10 strip left): a new 4x10 rect fills it in one bin, the 6x10 rect added again needs a second. The README's save/load block did exactly that, with the same `input` array it had just packed; it loads into a fresh packer and adds only new rects now.
It said the library "makes a minimum number of images under a maximum size" — the claim the packing and troubleshooting pages, and the repository rules, say it does not make. It aims for a small number, heuristically, and now says so.
The report's Phase A table adds up to 129 preserved anchors while the deployment page says 105, and it links here for the inventory, so the two contradicted each other. The mapping stands; the counts do not: the column is marked superseded, the five criteria tried are listed (none of them yields 129), and a re-measured section states the 105/170 split and the three buckets it falls into.
|
All four are addressed. Three of them were already fixed on the head that was current when you wrote this (
On the I replied to Greptile's inline threads individually rather than restating them here. |
The symbol section covered old site -> new site. The other direction had not been measured: of the 76 members the source declares public across the eight exported types, every one has an entry on its API page, so the reference the site publishes omits nothing the source exposes.
|
One more completeness measurement, in the direction nobody had checked. The symbol coverage I reported earlier went old site → new site; the reverse — does the generated reference document everything the source exposes — was unmeasured, and it is the direction where a stray JSDoc tag can hide a public member (the Result: 76 public members across the eight exported types ( The method is written into the phase-A report next to the symbol section (commit: |
…ed it Running every js block on the published pages, not only the four marked ones, turned up two that read as complete — each constructs its own packer and prints or places something — but threw `ReferenceError: MaxRectsPacker is not defined` when copied: the oversized example in `docs/user/packing.md` and the tag example in `docs/user/rotation-and-tags.md`. Both import the package now and carry a `docs-example` marker, so `verify:package` runs them: four examples before, six after. The other blocks that fail standalone are fragments by design — placeholder names like `packer.add(width, height, data)`, a signature block, or a one-liner continuing the block above it — and are left as they are.
|
Same method Greptile used to catch the persistence block — run the code — applied to every js block on the published pages, not just the four marked ones. Fourteen blocks across seven files; five execute cleanly; the rest are fragments by design, except two that read as self-contained and were not:
Both now import the package and carry a The blocks I left alone fail standalone for reasons that are correct: |
The report had drifted from what the stack does. Its page-mapping table still said `hierarchy.html` had no successor while the redirect writes one and the re-measured table counts its anchors; two of its three "open decisions" have since been taken — Pages over Vercel, which is what forgoes preview deployments for pull requests, and `excludePrivate: true` — and the phase-B scope never said which PR carried it. The TypeDoc-warnings question stays open, because it is.
|
A coherence pass over the phase-A report, since three review rounds have now edited documents around it and the report is what
Commit: |
Three of the four references in the ledger had drifted: `clone()` was cited two lines into its own docblock (this stack's JSDoc corrections added lines above it), the rotation swap pointed at the `const rotated` read instead of the assignment, and the oversized `load()` branch was cited one line above its `push`. Each corrected number was verified by printing the line it now names. The fourth is left alone on purpose: it points at a line that no longer exists, and says so.
|
Two verifications this round, both of things the stack claims but nothing checked. The 11 legacy redirects point at the right pages, not merely at existing ones. The build gate can only assert that a redirect's target exists — a table entry pointing The ledger's line references had drifted. Three of its four Also checked and clean, for the record: every URL in the published README resolves (the one 404 is |
The commands table reads as if `docs:preview` stands on its own, but it loads the site config, and that config imports the generated sidebar: measured on a cleaned tree, it stops at `Could not resolve "../api/typedoc-sidebar.json"` rather than saying there is no build. The other commands are self-sufficient — `docs:dev` generates the API itself, verified from the same clean tree — so the note names the one that is not.
|
Ran every command the documentation page lists, on a tree that had just been cleaned, because the table reads as if they each stand alone:
That last one is the documented-but-unstated dependency: |
`docs/user/index.md` still opened with "each staying inside a maximum width x height", which the oversized placeholder breaks — the same exception the landing page, `options.md` and `packing.md` already state. The opening now says normal bins and names the fallback; the `square` row in `options.md` loses its "every bin" for the same reason. Measured on the built bundle: a 100x100 packer given a 500x50 rect produces one 500x50 OversizedElementBin, larger than its own bound; with `square: true` a normal bin is square (60x10 -> 60x60), and a rect that cannot grow square inside the bounds (100x50 bounds, 60x10 rect) becomes a 60x10 placeholder rather than a non-square normal bin.
|
Fixed, and I swept the rest of the tree for the same class of claim before touching anything.
One more instance of the same sentence shape, one table row away: The measurements behind the opening sentence, for the record: a 100×100 packer given a 500×50 rect produces one 500×50 Left alone deliberately: the "aims for a small number of images under a maximum size" sentence (here and in the README). That one is the optimizer's target, already qualified as "a heuristic target, not a guarantee", and it is not a claim about every bin. Commit: |
The border/padding paragraph derives its conclusion from the free-space formula (`maxWidth + padding - border * 2`), which reads as if `padding` raised the size the packer takes. It does not: the oversized check compares against `maxWidth`/`maxHeight`. Measured — a 100x100 packer with `padding: 4` gives its first bin a 104-wide free rectangle and still reports a 104-wide rect oversized, while a 100-wide one fits. The sentence now says which of the two the formula describes.
|
After your finding I stopped reading the guide and started executing it: every page that asserts behaviour was loaded against the built bundle (
and a rect that only fits rotated (120×40 in 100×50 bounds) is oversized whatever the per-rect flag says, exactly as the second bullet states. Tags:
|
A CommonJS TypeScript project resolving modules the `node16`/`nodenext` way cannot import the package: the declarations are ESM (`"type": "module"`, no `exports` map), so the compiler reports TS1479 for a static import and TS1471 for `import ... = require(...)`. The runtime is unaffected — a JavaScript `require()` works and the package gate covers exactly that — and `verify:package` cannot see it because its type fixture is deliberately an ESM consumer. Measured on the published tarball from a CommonJS consumer: TS1479/TS1471 as above, while `await import(...)`, `moduleResolution` `node10`/`bundler` and plain `require()` all work, and neither `skipLibCheck` nor the compiler (TS 6 and TS 7) changes anything. Troubleshooting now lists it with the workarounds, and compatibility says what the fixture's coverage stops short of.
|
Found by executing the contributor pages' claims rather than reading them — and this one is a real gap, not a wording problem. A CommonJS TypeScript consumer cannot import the package under
Why the gate cannot see it: What changed (commit The rest of that sweep held up, for the record: |
The page opens with "Each one is pinned by a spec in `test/`", and for contract 1 three of its promises are not: `add()` returning the same object it was given, `addArray()` returning nothing, and the multi-argument overload returning an internal `Rectangle`. Searched the suite — the only `toBe(<a rect>)` calls are six `not.toBe(rect)` isolation checks in `test/clone.spec.js`, and every test that captures an `add(...)` result passes an inline literal, so it only proves the return is defined. The in-place half is pinned; a regression that returned a copy would keep every gate green. The contract now says which half the specs assert and which half is only what the signatures promise, and the ledger records the three assertions that would close it.
|
Audited the opening claim of
The search: the only Commit: For the record, the other seven contracts map cleanly: tag grouping (23 tag assertions in the packer spec), |
`add(width, height, undefined)` under `tag: true` throws an internal
`TypeError: Cannot read properties of undefined (reading 'tag')` — the single-argument overload guards
the same read, this one does not. Measured on the built bundle; it was recorded in the agent guide only,
so the user guide now has a section for it and the `add()` JSDoc says the argument has to be an object in
that mode, which puts the note on the generated API page as well.
Whether a bad call should keep reporting an internal message is a separate decision — the ledger's code
changes section carries it: rejecting `undefined` explicitly, or treating it as `{}`, changes what a
caller sees on a bad call rather than what a valid call packs.
|
Continuing the audit into the rest of the contracts — and the first bullet here is a user-facing gap, not just a missing assertion.
The rest of the contracts held up, with the checks that settled them:
One more unpinned detail, for completeness: contract 2's "a bin whose |
|
Correction to the last paragraph of my previous comment: contract 2's survival claim is pinned, and I misread the suite.
My grep looked for |
|
State of the four items from the stack re-review, all addressed on the current head:
The one finding that arrived after that review — Greptile's "TypeScript 7 rejects node10" — is fixed as well in Stack state after the fix: #83 |
Phase C of #81, stacked on #82. Review order: #82 first — this branch is based on it, so its diff will be rebased onto
masteronce #82 merges (and only then does CI run here; the workflow triggers on PRs intomaster).What moves
The README was doing four jobs at once, and CONTRIBUTING described a Jest/CommonJS suite with figures three revisions stale. The detail now lives where navigation can reach it:
docs/user/: overview, getting started, options, packing, rotation and tags, repacking, persistence, troubleshooting — README is 93 lines againdocs/contributor/: the same eight topics as site pagesCONTRIBUTING.mdkeeps the rules a first-time contributor needs and links out for depth; its stale facts are fixed (vitest/ESM instead of Jest/CommonJS, 8 spec files / 126 passing / 100% coverage instead of 6 / 66 / ~95%, anddocs/is source now rather than something never to commit).AGENTS.mdgains the documentation map and the proposal's routing rules, and its clone invariant is condensed to the rule plus a link to the full contracts page.Claims measured before they were written
Writing the guide against the bundle caught four wrong statements, all fixed:
rot: truefor a rect that already fits — measuredfalse 800 200, because the packer leaves a rect that fits unrotated alone. The example now uses a rect that only fits rotated (512×1024 bin, 1000×300 →true 300 1000);next()returns the index of the next bin, it does not return a bin;addArray()returns nothing, so the documented destructuring could never have worked;bin.repack()returns the rects it could not place, andpacker.repack(false)returns early when nothing is dirty.Examples can no longer rot
verify-package.mjsnow runs every block marked<!-- docs-example: name -->inside the temporary consumer that has the real tarball installed — README, getting started and rotation. It caught a missingimportin the rotation snippet on its first run, which is exactly the class of breakage that used to be invisible.Verification
npm test8 spec files / 126 passed / 2 skipped;cover100% on all four metrics;lint0/0;format:check;typecheck(TS 7.0.2);verify:docs;docs:build(29 pages, dead-link check included, spec and plans excluded from the page tree and the search index);verify:packageincluding the three examples.Content audit — 15 claims were wrong and are now fixed
Every guide page was checked line by line against
src/, the specs and measurements on the built bundle, by a reader with no stake in what it said. The passages that did not survive, each now a corrected sentence:load()replaces the bins; what you packed is goneallowRotation: falseadd(w, h, { allowRotation: true })builds aRectanglewhose own_allowRotationplace()honours — the "Per rectangle allow rotation" spec pins itrot: true, 300×800rect.data.tag, "not a property of the rect itself"rect.tagis a supported fallbackaddArray()tags new bins not at allhashproperty; nothing is computedbin.clone()copies "the rects, the tag and the data"OversizedElementBin.clone()re-derivesdatafrom its rectmaxWidth × maxHeight⇒ oversizedPlus the site home no longer claims a minimum bin count, and
EDGE_MAX_VALUEis documented as the default edge size rather than as unused (theAGENTS.mdcorrection on #82).Changes that live on another branch of this stack are named by their commit subject rather than by hash: the hashes move on every rebase, the subjects do not.
Review round: three more claims did not match the code
Each review finding is fixed in its own commit:
add()/addArray()both "return that same object"addArray()has no return statement — its return type isvoid. The contract separates the two, and theAGENTS.mdinvariant was corrected in the same commitmaxWidth/maxHeightare the bounds "every bin stays inside"new MaxRectsPacker(1024, 1024).add(2000, 2000)leaves anOversizedElementBinwhosewidth,height,maxWidthandmaxHeightare all 2000. Both pages now scope the bound to a normal binload()restores every bin at its index, options and tag includedmaxWidth/maxHeightexceeds the packer's is appended as a placeholderOversizedElementBinof the savedwidth×height, carrying no options and no tagDocumenting the third one turned up a real loss, recorded in
docs/plans/deferred-work.mdinstead of fixed here: a 1024×1024 packer holding one bin that loads[saved 2048-wide bin, saved 512-wide bin]ends with twoMaxRectsBins and no placeholder at all — the second entry assignedbins[1], where the first had just been pushed. Asave()/load()round trip silently losing a bin is a behaviour change with its own PR.Greptile round: five findings, all confirmed and fixed
Greptile reviewed this head with executed reproductions. Every finding held up when I re-checked it against the source, and each fix carries its own measurement:
packer.save()on an undeclared packer and never importedMaxRectsPacker, so the first line threwverify:packageexecutes it against the tarball4x10@6,0free, a new rect fills it in 1 bin while the old rect added again needs 2. The README's save/load block did exactly that, with the sameinputit had just packed; it loads into a fresh packer now✓ documentation examples run (readme-usage, getting-started, persistence, rotation)— four instead of three, and removing the persistence import exits 1 onReferenceError: MaxRectsPacker is not defineddocs:buildwith writing the redirects and checking its output, which arrive with the deployment layerdevelopment.mdalready hadMaintainer review round
it creates a **minimum number of images under a maximum size**is nowit aims for a **small number of images under a maximum size**. That was the one of the four findings still open when the review landed; the persistence example, the re-add guidance and the hard-coded example list had been fixed in the round before it.docs:buildcomment is a per-layer matter, as the review notes: this layer's table says the two steps it runs, the deployment layer says four.privateinsrc/.