v4.4.0
Highlights
-
@byline/core— Byline now has a single shared field-path grammar (packages/core/src/paths/). Several subsystems addressed fields by dotted path and did not all mean the same thing by it; four independently hand-rolled tree walks had drifted apart, and two of them carried bugs the others didn't. The grammar names the distinction that explains every difference: instance paths address a value in one item of one document (content[1].gallery[0].alt— item selectors required, no block type, because the addressed item carries its own_type), while declaration paths address a field declaration in the schema (content.photoBlock.gallery.alt— no selectors, block type required, since two blocks may declare the same field name). Eliding the selectors from a storage path yields exactly the declaration path: they are one grammar with two serialisations. New public exports areparseDeclarationPath/parseInstancePath,formatDeclarationPath/formatInstancePath,toDeclarationSegments,resolveDeclarationPath(schema-aware, with ablocks: 'qualified' | 'forbidden'policy), andwalkFieldDeclarations(the canonical schema walk, with anonBlockcallback for validation that must observe every block including ones declaring no fields). Parsing is deliberately schema-unaware — a block type and a field name are both bare identifiers, so only resolution against a field set can tell them apart. The upload hook registry, adminfields{}validation, and boot-validation error messages all now route through it; storagefield_path(persisted) and patch paths (wire format) stay frozen and are documented rather than changed. -
monorepo — a new architecture reference, Path Grammar, is the authoritative account of both notations: the segment AST, every notation's shape in one table, what is frozen and why, and the one deliberate narrowing (admin
fields{}keys bar block traversal entirely, because fields inside a block take their overrides from the blockType-keyedblockAdminregistry). The "Schema paths vs instance paths" section of the fields doc is now the short author-facing account and links out to it. Two contract tests pin every notation against a fixture that declares the same field name in two block types — the collision an unqualified path cannot resolve — so a new notation, or a change to an existing one, fails loudly.
Bug Fixes
-
@byline/admin— upload fields declared inside a block now work, where previously two independent defects stacked to make them unusable. First, the upload widget never rendered its drop zone at all:collectionPathwas prop-drilled down through the field tree, andBlocksFieldhad never forwarded it, so afile/imagefield inside any block fell back to a read-only placeholder with no error. (ArrayFieldandGroupFieldwere given the same fix in 4.2.x; blocks were missed because no consumer had an upload inside a block to notice.) Second, even once rendering,upload.contextresolved against the wrong block: the executor matched the first block declaring a field of the next path segment's name, so with two blocks each declaring an intermediate array — legal, since the boot-time uniqueness constraint covers only upload-capable leaf names — the declared context values were silently dropped and never reached server-sidebeforeStore/afterStorehooks. Resolution now reads the addressed item's_typefrom form state and descends into that block; where form data cannot disambiguate, it accepts only a unique match rather than guessing. -
@byline/core— boot-validation error messages now name the block type. A field inside a block was reported ascontent.alt, which is ambiguous the moment two blocks in the same field declarealt— the message could not say which declaration was at fault. Paths are now block-qualified (content.photoBlock.gallery.alt). -
@byline/core— an unresolvablesearchconfig field name is now rejected at boot instead of silently skipped.buildSearchDocumentresolvessearch.{body,facets,filters}names among the collection's top-level fields and skipped anything it could not resolve, so a typo, a dotted path, afacetsentry that was not arelation, or afiltersentry naming a container produced a collection that indexed less than its author intended — with no signal at boot and none at index time. See Breaking Changes for the consumer-side effect. -
@byline/core— a malformed patch path is now rejected rather than applied at a truncated path. The previous parser was lenient:"a["yielded a singleasegment, and the patch was then written toa— a path the client never addressed.parsePatchPathnow delegates to the shared grammar'sparseInstancePath(the two agree on every well-formed path) and returns no segments for a malformed one, so the patch surfaces as an error instead of a silent mis-write.
Chores
-
@byline/db-postgres— the package'snodeunit-test tier was declared in its Vitest config but had no files and atestscript that only echoed a message. It now runs, contributing pure flattener tests that need no Postgres to the rootpnpm test. -
@byline/cli— removed four obsolete document example fixtures. They encoded a storage path notation that never existed (a sequential per-field counter after the block type,content.1.photoBlock.0.display), traced back to an early analysis document and copied forward — andbyline initwas placing them in user projects as reference material for what storage rows look like. The same error was corrected inCLAUDE.mdand the architecture index; the detailed storage reference had always been correct.
Breaking Changes
-
@byline/core— a collection whosesearchconfig names a field that does not resolve will now fail to boot. Previously it started and quietly under-indexed. If yoursearch.{body,facets,filters}entries are correct, nothing changes. If one is a typo, a dotted path, a non-relationinfacets, or a container infilters, boot fails with a message naming the collection, the option, and the offending entry. Action: correct the name. To index content nested inside agroup/array/blocksfield, name the top-level container — it is walked recursively and every nested text leaf is flattened into the body. Addressing one specific nested declaration is not supported and is rejected explicitly rather than accepted and ignored. -
@byline/admin— thecollectionPathprop was removed fromFieldRenderer,ArrayField,GroupField, andBlocksField(all exported from@byline/admin/react). It is constant for the whole form, so threading it meant every nesting-capable container had to remember to forward it, and one that forgot silently rendered upload fields read-only — a defect that recurred twice. It now lives on the form-context store, set once byFormProvider, andFileField/ImageFieldread it viauseFormContext(). Action: none if you use the admin shell. If you render any of those components directly, drop the prop and ensureFormProviderreceivescollectionPath. -
@byline/core— a malformed patch path is now rejected (see Bug Fixes). The admin client builds patch paths structurally and is unaffected; this only reaches a client constructing paths by hand.
All other @byline/* packages bumped to 4.4.0 in lockstep with no behavioural changes this cycle.