feat: relocate the flat/tree package boundary into document-schema.js - #27
Merged
Conversation
…lates decompose, flattenPackage, factorStyles, and assemblePackage convert a flat ContentDocument to and from the DocumentPackage tree: the two encodings this package already defines, and the transform whose correctness contract is the three laws src/package.ts states. They belong here because ooxml.js, odf.js, markdown-codec, and pdf-codec all depend on this package and none of them depends on documents.js, where the transform previously lived. A codec whose public read/write functions speak DocumentPackage directly can only reach the transform if the transform sits at or below the schema layer; anywhere higher and it is a dependency cycle. This amends the "schemas only" charter deliberately and narrowly. The transform is purely structural and does no I/O: no XML, ZIP, PDF, fonts, layout, or bytes, and nothing format-specific. canonicalise stays module-private -- it exists to give minting one tuple-identity recipe, and publishing it would turn an internal determinism device into an API guarantee about sort order.
…e laws
decompose.test.ts and factor-styles.test.ts pin the tree shape and the minting
mechanism: mandatory section groups, per-container heading/list stacks, the
never-cross-a-shape-boundary rule, construct-boundary promotion and its
imbalance refusal, the ban list, frozen keys, chain-scoped stripping, entry
ordering, and idempotence.
bijection.test.ts runs the three laws over a corpus spanning every document
kind, every leaf the tree vocabulary admits, and every grouping signal decompose
reads. Its anti-vacuity guards keep it honest: at least one entry must mint a
real styles table, every construct entry but the deliberately empty one must
mint a ref onto its own construct group, and every ContentDocument kind must be
represented.
Law (ii) checks for a surviving { node, children } group wrapper rather than for
any key named "style". A ContentStroke carries its own style field
(solid/dashed/dotted/double) as ordinary drawing content, so a key-name scan
reads a dashed line on a draw page as a leaked ref; a wrapper is the only shape
a ref can sit on, so its absence is the stronger statement anyway.
flatten.test.ts covers what only a direct entry reaches: which chain a position
resolves against, that gap-fill never overwrites a carried value, and the two
refusals a hand-built tree can trigger -- a ref with no styles table behind it,
and a formula package holding other than exactly one ContentFormula.
assemblePackage, decompose, flattenPackage, factorStyles, ConstructMarkerImbalanceError, and the PackageChildren type join the barrel. Purely additive: no existing export changes shape. Named exports rather than the `export *` every other module uses, because these modules carry genuine internals -- decompose's per-container helpers that factor-styles calls, factor-styles' own mint entry point, and the canonicaliser behind tuple identity. Adding a name to the public surface should be a decision, not a side effect of exporting it from its module. The smoke test asserts all six load from both the ESM and CJS builds, and the workers suite runs the whole assemble/mint/flatten round trip inside workerd: the transform is the one part of the published surface that is behaviour rather than schema, so "no platform API anywhere in it" becomes a runtime-checked fact rather than a design claim.
Five comments and the package description asserted that decompose, flatten, and styles minting live in documents.js, and the README's charter line said "only Zod schemas ... no behaviour". Both are now wrong: the transform is here. Adds a "The package boundary" README section covering the four entry points, the imbalance refusal, why the charter amendment is narrow (structural, zero-I/O, format-agnostic -- everything the charter guards against stays out), and the split between the law suite here and the real-format corpus documents.js keeps running the same harness over.
…xport The comments in src/index.ts and src/canonicalise.ts claimed decompose's per-container helpers, factor-styles' mint, and canonicalise/canonicalKey stay outside the package's public API because index.ts only re-exports six curated names. That's false as published: package.json's "./*" export maps every dist module to its own subpath, so document-schema.js/decompose, document-schema.js/factor-styles, and document-schema.js/canonicalise already publish those symbols regardless of what the index barrel re-exports, exactly as the README's "every module is also importable directly" section already documents for every module in the package. Reworded both comments to state what curating the barrel's named exports actually achieves -- a smaller index.ts surface, not a smaller published surface.
|
🎉 This PR is included in version 4.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Moves
decompose/flattenPackage/factorStyles/assemblePackageout of documents.js (src/convert/{decompose,flatten,factor-styles}.ts) and into this package, and exports them from the barrel. Purely additive — nothing already exported changes shape, so this is a minor.Why here
The ecosystem is moving to make
DocumentPackagethe only type a codec exposes publicly. ooxml.js, odf.js, markdown-codec, and pdf-codec all depend on document-schema.js and none of them depends on documents.js — documents.js depends on them. So for a codec's public read/write functions to return and acceptDocumentPackagedirectly, the transform has to sit at or below the schema layer. Anywhere higher and it's a cycle.This is a deliberate amendment to the "pure Zod schemas, no behaviour" charter, not drift.
decompose/flatten/factorStylesaren't business logic or format-specific behaviour — they're the canonical, purely structural, zero-I/O transform between the two shapes this package already defines, and their correctness contract is the three lawssrc/package.tsstates. Everything the charter actually guards against (XML, ZIP, PDF, fonts, layout, bytes, filesystem) stays out. The container README and this package's own README/description are updated to say so.Relocation, not rewrite
The three implementation modules are character-identical to documents.js's
origin/maincopies apart from their import blocks and a handful of comment lines that named documents.js as the transform's home. Verified by diffing with comments stripped: zero non-import differences.decompose.test.tsandfactor-styles.test.tscame across verbatim — import-block diffs only.The bijection suite
The three laws are the whole proof this move broke nothing, and they run here over a corpus covering every document kind, every leaf the tree vocabulary admits, and every grouping signal
decomposereads. The construct-boundary corpus came across verbatim.What could not come across is documents.js's corpus itself: it's built from readers, editors, and conversion captures belonging to packages that depend on this one, so importing it would invert the dependency this move exists to keep one-way. documents.js keeps running the same law harness over that real-format corpus, and should keep doing so once it switches to importing from here — the two are complementary, not redundant.
One deliberate change inside the harness: law (ii) now checks for a surviving
{ node, children }group wrapper rather than for any key namedstyle.ContentStrokecarries its ownstylefield (solid/dashed/dotted/double), so the old key-name scan reads a dashed line on a draw page as a leaked style ref — it never fired in documents.js only because that corpus never produced one. A wrapper is the only shape a ref can sit on, so its absence is the stronger statement anyway.Also added, closing gaps the old suite left:
flatten.test.ts— resolution semantics on trees a caller hands in directly (chain order, gap-fill never overwriting), plus the two refusals nothing covered before: a ref with no styles table behind it, and a formula package holding other than exactly oneContentFormula.Public surface
assemblePackage,decompose,flattenPackage,factorStyles,ConstructMarkerImbalanceError, and thePackageChildrentype — the same list documents.js re-exports today. Named exports rather than theexport *every other module uses, because that curates which names this index barrel re-exports as the intended six, rather than re-exporting whateverdecompose/factor-styles/flattenhappen to define. It does not make the modules' other helpers (decompose's per-container helpers,factor-styles'mint, the canonicaliser behind tuple identity) unreachable: per the README's "every module is also importable directly",package.json's"./*"subpath export already resolves each source module on its own, so e.g.document-schema.js/decomposeanddocument-schema.js/factor-stylespublish those helpers regardless of what the barrel re-exports. That subpath behaviour predates this PR and applies to every module in the package, not something this PR could scope down without turning off the wildcard export for the whole package.Follow-up, not in this PR
documents.js still has its own copies. Once this releases, it should import from here and delete
src/convert/{decompose,flatten,factor-styles,canonicalise}.ts, keeping its bijection suite pointed at the relocated implementation — that's the run that proves the real-format corpus still passes. Tracked in ExaDev/documents.js#650.pnpm typecheck,lint,test,test:workers,build, andtest:smokeare all green.Generated by Claude Code