@bettercms-ai/convert 0.2.1 → 0.4.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.
package/README.md CHANGED
@@ -71,6 +71,56 @@ A path counts as rewritten only when EVERY located occurrence of it was. A value
71
71
  the home page and a shared header is two places a reader sees it, and converting one of them reads
72
72
  as a finished job.
73
73
 
74
+ **A declaration this package did not write still counts.** An element that names the target —
75
+ `data-bcms-field`, `data-bcms-layout-field` or an address in `data-bcms-props` — but gets its copy
76
+ some other way (a literal somebody left there, the project's own data module) is DECLARED: the
77
+ publisher injects into that element and the editor makes it click-to-edit. It is simply not reading
78
+ through the committed helper. Those land in `alreadyDeclared` with a receipt row of their own,
79
+ `readsFromCms: false`, rather than in `NO_ORIGINAL`:
80
+
81
+ ```jsonc
82
+ "declarations": [{ "route": "/", "scope": "page", "path": "hero.title",
83
+ "kind": "text", "file": "src/pages/index.astro", "readsFromCms": false }]
84
+ ```
85
+
86
+ The evidence may never be ANOTHER ROUTE'S PAGE FILE — one the brief itself names, since
87
+ `routeOfFile` reads the repository's convention and the two legitimately disagree about a dynamic
88
+ route — and it may never be an element two routes both claim. The same rule gates a declaration
89
+ that DOES read from the CMS: a home page rendering a teaser of `/about`'s headline reads `/about`'s
90
+ snapshot legitimately, and counting it left `/about`'s own page hardcoded and unreported. Without a read there is nothing in a declaration that says which route it is for, and two
91
+ routes legitimately carry `hero.title` — so "some file declares that string" would mark the other
92
+ route bound and leave it hardcoded forever. A path with no original at all is stricter still: the
93
+ only evidence available is the file's identity, so it has to be the one file the router says serves
94
+ that route. A layout path has no route by construction, and any file may carry it.
95
+
96
+ **A tsconfig is JSONC.** Comments and trailing commas are read (that is what `tsc --init` writes),
97
+ because a config this package cannot parse declares no `paths` aliases as far as it is concerned —
98
+ and then every `@/components/Hero` resolves to nothing and the refusal that follows names the wrong
99
+ cause. Its alias fields are shape-checked too: `"paths": "src/*"` is valid JSON and the wrong type,
100
+ and reading it threw a `TypeError` out of a pure function naming nothing. One it still cannot read
101
+ is a receipt NOTE rather than a silence — on the componentize receipt as well, which resolves a
102
+ page's `<Hero />` through exactly those aliases:
103
+
104
+ ```jsonc
105
+ "notes": [{ "code": "TSCONFIG_UNREADABLE", "file": "tsconfig.json", "message": "…" }]
106
+ ```
107
+
108
+ **The address grammar, in one place.** Page copy is `data-bcms-field="<path>"` plus
109
+ `data-bcms-kind="<kind>"`. The shared chrome is a DIFFERENT attribute carrying a prefixed address,
110
+ `data-bcms-layout-field="layout:<section>:<field>"`, so the block lane's own `[data-bcms-field]`
111
+ query cannot reach a chrome marker. A value that lives in an ATTRIBUTE (a promoted nav link's
112
+ `href`) has no field attribute of its own and rides `data-bcms-props="<address>|<kind>|<attr>;…"` —
113
+ where `<address>` carries the same `layout:` prefix when it is chrome. A section root additionally
114
+ carries `data-bcms-block="<blockId>"`, which is what the editor hit-tests for section chrome and
115
+ reorder — and inside an extracted section that root's own placement decides which lane its elements
116
+ address: the page path while the copy is the page's, `data-bcms-field="<blockId>__overrides.<key>"`
117
+ once the copy is the instance's. The page-lane address is re-based onto **that placement's** group:
118
+ the markup was cut from one page and carries its paths, and a second page renders the same
119
+ component bound to a group of its own, where `hero.title` is a field that does not exist.
120
+ **One `data-bcms-field` per element, always.** Two would leave the publisher and the editor reading
121
+ different attributes off one node, and which of them won would depend on the dialect — JSX takes
122
+ the last, Astro the first.
123
+
74
124
  ## As a library
75
125
 
76
126
  ```ts
@@ -92,6 +142,17 @@ proposer re-derives them from its own scan rather than believing the receipt. A
92
142
  become a loop — non-consecutive siblings, one row shaped differently, the html dialect — is still
93
143
  declared and read per index and reported `REPEATER_FIXED_LENGTH`.
94
144
 
145
+ **A row that holds a list of its own becomes a loop inside the loop.** `cards[i].items[j].label` is
146
+ a nested `.map` in astro and jsx, reading its rows off the ROW — `bcmsRows(card, "items", […])`,
147
+ the same helper one level down, because `bcmsRows` passes a nested array through untouched and
148
+ `bcmsSection` hands a section its group whole. The binding is
149
+ `` data-bcms-field={`cards[${i}].items[${j}].label`} `` and the shape is `cards[*].items[*].label`.
150
+ ONE level: that is what the plan mints and what the component prop validator accepts, so a leaf
151
+ indexed twice keeps the length the template gives it. One inner row is enough to make the loop —
152
+ the plan already says `items` is an array, and reading a card that ships a single bullet as "not a
153
+ list" is how the second bullet an editor adds never renders. The html dialect has no loop form at all, so
154
+ it stays fixed-length there — every index declared, the number of rows the template's.
155
+
95
156
  **A prop-drilled literal is converted in two hops, in one proposal.** `<Hero title="…" />` becomes
96
157
  `title={bcms(…)}` plus `bcmsBindings={{ title: "hero.title" }}`, and the `<h1>{title}</h1>` inside
97
158
  `Hero` — resolved through the file's own imports, relative or through the project's tsconfig
@@ -119,8 +180,10 @@ left alone. A repeater on a dynamic route passes the same parameter to `bcmsRows
119
180
  ## Componentize
120
181
 
121
182
  The second mode. P2 above makes a page's copy editable in place; `--componentize` makes its
122
- SECTIONS movable — each band becomes a component the CMS can reorder, duplicate and swap, while its
123
- copy stays exactly where P2 put it.
183
+ SECTIONS movable — each band becomes a component the CMS can reorder, duplicate and swap. Where the
184
+ copy lives is a separate question, and the plan answers it: **Phase A** leaves it in the page's
185
+ field groups (`props.bind`), **Phase B** moves it onto the instance (`props.overrides`). The same
186
+ extracted files serve both.
124
187
 
125
188
  The division of labour is the one that already works by hand: **the CMS owns STRUCTURE, the repo
126
189
  owns RENDERING.** The server decides which field groups are sections (`get_componentize_plan` →
@@ -142,20 +205,45 @@ modify src/pages/index.astro
142
205
  modify bcms-content/home.json
143
206
  add bcms-content/components.json
144
207
 
145
- 5 sections extracted (0 inline-only, 0 already extracted, 0 pending)
208
+ 5 sections extracted (0 registered, 0 inline-only, 0 already extracted, 0 pending), copy: bind
146
209
  ```
147
210
 
148
211
  `--plan <file>` is required and is only read in this mode. `--dry-run`, `--receipt`, `--strict` and
149
212
  `--overwrite-helper` mean what they mean above; `--strict` exits 2 on any pending SECTION.
150
213
 
214
+ `--copy bind|instance` (default `bind`) says which copy model the plan is expected to describe. It
215
+ is an **assertion, not a switch** — the extracted files are byte-identical either way — and it
216
+ exists to catch one order-of-operations mistake that would otherwise be silent: running
217
+ `--copy instance` in the repository BEFORE the server has moved the copy leaves a tree that looks
218
+ converted and renders every section out of page fields the dashboard is about to hide. A plan whose
219
+ placements all still carry `bind` says so, and every section is refused `COPY_MODE_MISMATCH` with
220
+ nothing written. The receipt carries the mode as `copy`, the same word the server's receipt uses.
221
+
151
222
  ### What is written
152
223
 
153
224
  - **`src/components/bcms/<Name>.<astro|tsx>`** — the section root's markup **verbatim**, P2's
154
- `data-bcms-field` and `bcms(...)` reads included, plus `data-bcms-block={blockId}` on the root
155
- (the editor hit-tests that for section chrome and reorder). Its copy comes from a new
156
- `bcmsSection(page, bind, overrides)` in the committed helper: `overrides ?? pageGroup(bind) ??`
157
- the fallbacks the template already carries. One component serves every page whose group has the
158
- same shape, which is what the plan's `shapeHash` is for.
225
+ `bcms(...)` reads and its page paths included, plus `data-bcms-block={blockId}` on the root (the
226
+ editor hit-tests that for section chrome and reorder; a root that already carried one has it
227
+ REPLACED — and any further ones removed — never joined). Its copy comes from `bcmsSection(page, bind, overrides)` in the
228
+ committed helper — `overrides[key] ?? pageGroup(bind)[key] ??` the fallbacks the template already
229
+ carries. **The placement's own `bind`, with no synthesized default:** `bind ?? "<groupKey>"`
230
+ belonged to Phase A, where every placement was bound and the default could only restate what the
231
+ placement already said; under Phase B it put the page's field group back underneath an instance
232
+ that owns its copy, so a key the instance had not been given rendered stale page copy out of
233
+ fields the dashboard has already hidden. A placement that names no group has none, and every
234
+ `bcms(...)` renders what the template shipped with — and each of P2's bindings is wrapped in the placement's own chooser,
235
+ `bcmsFields({ blockId, bind, overrides }, "<prefix>")`, so the page path is still verbatim in the
236
+ file and the ADDRESS is decided when the component renders. Both names it introduces are
237
+ allocated (`bcmsField_1`, and so on) against the page's own top-level bindings **and against
238
+ every identifier-shaped token in the bytes being lifted** — all of them, since a section using
239
+ `bcmsField` *and* `bcmsField_1` would otherwise just move the capture one suffix along — because
240
+ a page that declared `bcmsField` would
241
+ otherwise have every use of theirs inside the extracted section silently rebound to our chooser
242
+ — and in a TSX page that declaration lives inside the component function, where a module-level
243
+ check cannot see it. Reserving the name puts the reference back on the honest path, which is
244
+ `SECTION_FREE_IDENTIFIERS` naming the identifier that could not travel. One component serves every page whose group has the same shape,
245
+ which is what the plan's `shapeHash` is for — and one component serves both copy models, which is
246
+ what makes moving a page's copy a data change rather than another cut.
159
247
  - **`src/lib/bcms-sections.ts`** — `pageSections(pageSlug)` over the committed snapshots'
160
248
  `blocks` plus `bcms-content/components.json`, returning `[{ blockId, slug, bind, overrides }]`.
161
249
  An instance naming a component this repo does not have is **skipped, never a hole**.
@@ -165,9 +253,26 @@ add bcms-content/components.json
165
253
  look like, so rendering nothing there would blank the site on the first build after the codemod
166
254
  ran, with nothing in the diff to explain it. The fallback is decided on the **raw** block list:
167
255
  a page whose blocks all name components this repo does not have has still been *arranged*, and
168
- putting the shipped bands back would undo an editor's work on the next build.
256
+ putting the shipped bands back would undo an editor's work on the next build. A shipped placement
257
+ carries `bind` only while the plan says its copy is the page's: an instance-owned one has none,
258
+ because binding it there would make a fresh clone render a section out of fields the dashboard
259
+ has already hidden.
260
+ - **Nothing at all, for a section the repository has ALREADY componentised.** A page that renders
261
+ `<Hero />` from a file it imports has made this cut itself: the existing file is REGISTERED under
262
+ the section's slug — the registry imports it where it lives, **in the shape its module actually
263
+ exports**: a default import, `import { Hero }` for a named export, or `import * as Bands` plus the
264
+ member the page's own call site names — and the page keeps calling it. When one module is bound
265
+ twice (`import Default, { Hero } from "./bands"`), the binding the page actually CALLS is the one
266
+ registered. It has to be a file this page imports, the only one it imports that renders the
267
+ group, and one the registry's dialect can render; a section the page ALSO renders inline is two
268
+ answers and is refused. Everything else is `SECTION_ROOT_AMBIGUOUS` with the reason spelled out.
169
269
  - **The page** — the contiguous run of section roots replaced by one `<Sections page="<slug>" />`,
170
- with the import added.
270
+ with the import added. Contiguous means consecutive element siblings **and** nothing but
271
+ whitespace or comments in the bytes between them: `<Hero />Read this first<Features />` has no
272
+ element in the way, and one splice over that run would delete the sentence. A page with a REGISTERED section is all or nothing: `<Sections>` renders
273
+ every placement the page has, so the run is replaced only when every one of the page's sections
274
+ resolved and every call site is a bare tag in that run. Otherwise the call sites stay exactly
275
+ where their author put them — replacing a subset would render a registered section twice.
171
276
  - **The stubs** — `blocks: []` added to each page's snapshot (keys it already has are kept), and
172
277
  `bcms-content/components.json` = `[]`.
173
278
 
@@ -211,15 +316,21 @@ import travels as `import * as X`, not as a named one.
211
316
 
212
317
  | reason | |
213
318
  |---|---|
214
- | `SECTION_ROOT_AMBIGUOUS` | the group's fields sit under no single element, one section's root contains another's, or the group is rendered somewhere other than this route's page file. |
319
+ | `SECTION_ROOT_AMBIGUOUS` | the group's fields sit under no single element, one section's root contains another's, the page renders it BOTH inline and through a component it imports, two imported components render it, or it is rendered somewhere this cannot register — a file this page does not import, a component of another dialect, a namespace the page never calls. The message says which. |
215
320
  | `SECTION_NOT_CONTIGUOUS` | the page's section roots share no container at all. **The components are still written**; the page is left exactly as its author wrote it. |
216
321
  | `SECTION_FREE_IDENTIFIERS` | the markup uses a name the component file cannot be given. The identifier is named in the message. |
217
322
  | `COMPONENT_CONFLICT` | a file already sits at `src/components/bcms/<Name>` and this package did not write it. Decided on our marker, never on the path — that is a name a person may reasonably have chosen first. |
218
323
  | `REGISTRY_CONFLICT` | the same, for `Sections.astro`/`Sections.tsx` or `src/lib/bcms-sections.ts`. Both are rebuilt whole on every run, so an unmarked file there would be destroyed. `--overwrite-helper` proceeds. |
324
+ | `REGISTRY_UNREADABLE` | the registry IS ours and its `REGISTRY`/`SHIPPED` tables cannot be read back — a `REGISTRY` naming no components at all (this module is only emitted when there is at least one, so a table with no rows is one whose rows were lost), a `SHIPPED` row that is not an object with a string `slug` (and string `bind`/`blockId` when present), a page whose placement list is EMPTY (a row is only written for a page that has some, and the fallback rendering that page blank is the worst thing this could accept), or a `SHIPPED` slug the `REGISTRY` does not name. `[null]` is valid JSON and threw a `TypeError` out of a pure function the first time anything dereferenced it. An EMPTY `SHIPPED` is read, deliberately: a run whose only page was `SECTION_NOT_CONTIGUOUS` writes the components and no order at all, so refusing that would strand the repository — a component it names is GONE from the tree, a row is not a property this can read, or the tables are missing. Every property is parsed structurally and all of them must convert; a partial read is what silently drops a page's bands. It is rebuilt whole every run, so re-emitting it from what this run happened to see would blank those pages. No flag overrides this one: deleting the file is a decision only a person can take. |
219
325
  | `ROUTE_FILE_AMBIGUOUS` | two files serve one route. |
220
326
  | `DIALECT_UNSUPPORTED` | the page is svelte, vue or html. Nothing is written for it. |
221
- | `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. `--overwrite-helper` proceeds. |
327
+ | `HELPER_CONFLICT` | `src/bcms-content.ts` is a module this package did not write. Refused **for the whole run, with an empty file list**: `bcmsSection` and `bcmsFields` are what every extracted *and* every upgraded component reads through, so pending it per page while the rest of the run proceeded rewrote v1 components to import an export the helper does not provide. `--overwrite-helper` proceeds. |
222
328
  | `SNAPSHOT_INVALID` | `bcms-content/<page>.json` is not JSON this can add `blocks` to. Refused before a byte is written for that route. |
329
+ | `COPY_MODE_MISMATCH` | `--copy instance` was asked of a plan whose placements are all still bound to page field groups. Refused before this repository is even opened — **the run returns an empty file list**, because a refusal that upgrades the helper and rewrites the registry is not a refusal. The message says to run `componentize_sections { copy: "instance" }` on the server first. The reverse pairing is NOT a refusal: `--copy bind` over an instance plan still produces a tree that renders, so refusing it would only stop a run that works. |
330
+ | `PROP_MAP_INCONSISTENT` | the plan's `fields[].path` is not `<one common prefix> + fields[].key` — `{ path: "hero.heading", key: "title" }`, or two fields that disagree about the prefix. A page path cannot be turned into a prop key losslessly, so the block-lane address would name a key the instance does not hold; falling back to `<groupKey>.` sent every edit to something nothing reads. Checked **before this repository is consulted at all**, so an already-componentized page is held to it too — doing it where the markup is cut reported such a page complete while its component went on addressing the wrong key. And checked again against the addresses it has to CONVERT: `{ groupKey: "hero", key: "title", path: "other.title" }` is internally consistent and wrong about the section, whose markup declares `hero.title` — a prefix that converts none of what the section actually declares would leave every element on the page lane whatever the placement said. A field address this cannot read statically counts as one it cannot convert, and is named in the message. Only two shapes are provable — a string literal and NOTHING else, and a template literal whose every interpolation is a LONE IDENTIFIER used as an index (`[${i}]`, what a repeater writes, which cannot change the prefix; `[${getKey()}]` names whatever that returns and is not). `{computedPath}`, `{"a" + x}` and `{"hero.title".replace("hero","footer")}` are all opaque: starting with a quote is not a proof, since that last one names `footer.title` at render time. A field carrying only one of the two says nothing about the mapping and is skipped, not refused: a Phase A plan spells no `key` at all. |
331
+ | `DUPLICATE_FIELD_BINDING` | one element in the section carries more than one `data-bcms-field`. There is no single address to rewrite, and rewriting both leaves the extracted element still violating the one-attribute invariant. |
332
+ | `INLINE_NEEDS_INSTANCE` | the page cannot be looped and the placement's copy has MOVED. The in-place call site the codemod writes — `<Hero bind="hero" page={pageSnapshot("home")} />` — comes from the plan and carries no `blockId` and no `overrides`: it never consults the CMS's blocks at all, which is what makes the inline form componentisation *without* the page builder. Emitting it would render the page's field group (the copy the dashboard is about to hide) and declare the page lane for an address that no longer holds anything. The component is still written; the page is left as its author wrote it. |
333
+ | `SECTION_UNMIGRATABLE` | a component an OLDER version of this package wrote cannot be brought to the current format: it no longer parses, it no longer declares its placement, its element carries two field attributes, **the prefix its markup was cut with cannot be recovered** (see below), or it already binds `bcmsField`/`bcmsFields` to something this package did not write — adding a second declaration is a file that does not compile, reported as a successful upgrade. A name is not a provenance: an import is reused only when its specifier RESOLVES to the helper module this package writes, and a `bcmsField` only when it is declared as `const bcmsField = <that exact local>(…)` — read structurally, in the module scope **and** in the component function's own body, which is where a `.tsx` component declares it. It is left exactly as it is, and the section is reported rather than counted `alreadyExtracted` on a format it is not on. |
223
334
  | `NOT_IN_SOURCE` | no file we were given renders the group's fields, or the route names no page file. |
224
335
 
225
336
  A plan spanning TWO frameworks gets two of everything that is dialect-shaped: its own components,
@@ -233,29 +344,75 @@ Completion is decided per ROUTE, from that page's own bytes — it imports our r
233
344
  somewhere carries our marker", which is true of every group that has ever been extracted and left a
234
345
  brand-new page untouched.
235
346
 
347
+ **The marker carries a FORMAT VERSION, and anything older is upgraded before it is called done.**
348
+ A 0.3 component (`section v1`: page-path bindings, no render-time lane) carries a marker too, so
349
+ reading the marker alone reported every already-componentized page finished and left its components
350
+ on the page lane forever — `--copy instance` there placed five sections and moved no address at
351
+ all. Each older component is instead brought to the current format in place: every
352
+ `data-bcms-field` wrapped in the chooser, the chooser declared and imported, the marker restamped.
353
+ The Phase A `bind ?? "<group>"` fallback is rewritten away in the same pass — through the module's own AST, by offset and through the file's OWN local name for the helper's export, so neither a call a formatter has reflowed nor one made through an aliased import is missed. A module with no readable program is refused, and so is one where nothing was rewritten while a `??` fallback sits in a call this cannot identify as the helper's. An upgrade never leaves that behaviour under a current marker. It happens once; the next run sees the current version and writes nothing. A component that binds nothing needs no prefix and cannot be ambiguous about one — the marker is its whole upgrade. A component the plan
354
+ says nothing about is left alone — nothing asked about it, and a v1 file is still a correct Phase A
355
+ component. One that cannot be upgraded is `SECTION_UNMIGRATABLE`, never silently skipped — and that refusal is about ONE component, keyed
356
+ by dialect as well as slug, so an unmigratable `Hero.astro` says nothing about the `Hero.tsx`
357
+ beside it. (Each registry also resolves its own extensionless imports to a file its dialect can
358
+ render, which is what makes two same-named components in one directory readable at all.)
359
+
360
+ **Which group an upgraded component was cut from is read off the FILE, not off the plan.** One
361
+ component serves every page whose group has its shape, and its markup carries the paths of the one
362
+ page it was cut from; taking the plan's first placement for that slug gave a component reading
363
+ `hero.*` the prefix `landingHero.` as soon as that placement came first, so every wrapped address
364
+ matched nothing, the block lane never fired, and the file was stamped current for good. The
365
+ candidate prefixes — one per placement of the slug, **server-pending ones included**, because which
366
+ group the bytes were cut from is a fact about the past that a refused placement may be the only
367
+ thing to explain — are matched against the addresses the file actually declares (a repeater's contributes everything before its first interpolation), and a slug
368
+ whose placements cannot be told apart that way is refused rather than guessed at.
369
+
370
+ **An already-componentized page still has a shipped entry, and the plan may have moved its copy.**
371
+ Its placements' `bind` is restated from the current plan on every run — in the order the registry
372
+ already holds, since a page whose markup was cut has no roots left to read an order off — so a bind
373
+ run followed by an instance run drops `bind` from the fallback table and the reverse puts it back.
374
+ Repeated placements of one component (two groups sharing a `shapeHash`, which is what `shapeHash` is
375
+ FOR) are matched **positionally**, registry order against plan order per slug: keying them by slug
376
+ collapsed them, and a page with one group still bound and another moved wrote both from the second.
377
+ Every plan occurrence holds its slot whether or not this run can act on it — one it cannot (refused,
378
+ server-pending) consumes its position and keeps the earlier record, because a missed update is
379
+ recoverable and a wrong one is a page rendering someone else's copy.
380
+ Nothing is extracted there, and the registry is rewritten only if those bytes actually change.
381
+
236
382
  The registry and the sections library are rebuilt WHOLE each run, so they are first read back:
237
383
  components an earlier run wrote are reused rather than rewritten, and the shipped order of every
238
384
  page already served is carried forward. Without that, componentizing one new page emitted a
239
- registry that had never heard of the others, and they rendered blank.
385
+ registry that had never heard of the others, and they rendered blank. The read-back goes through
386
+ the dialect's own PARSER, so a formatter that rewrote our imports (single quotes, a wrapped line)
387
+ changes nothing; the two tables are brace-matched with strings and comments skipped and every
388
+ property is parsed structurally, so a one-line table, a wrapped one, or a comment containing a `}`
389
+ are all read whole. An import must resolve to a file that is actually there — composing the path
390
+ from our own naming convention re-emitted a registry importing a component somebody had deleted.
391
+ When any of that cannot be reconstructed the run refuses by name rather than rebuilding from what
392
+ it happens to see.
240
393
 
241
394
  A run whose roots DO share a container but have a foreign sibling between them is not refused: each
242
395
  root becomes its own `<Hero …/>` where its markup was, counted `inlineOnly`. The markup is
243
- componentised and the ORDER stays the repository's — looping that page would move the sibling too.
396
+ componentised and the ORDER stays the repository's — looping that page would move the sibling too. A placement whose copy the plan has moved cannot take that form and is
397
+ `INLINE_NEEDS_INSTANCE`: the in-place call site carries no `blockId` and no `overrides`.
244
398
 
245
399
  The arithmetic is checked rather than hoped for:
246
400
 
247
401
  ```
248
- plan sections === extracted + inlineOnly + alreadyExtracted + pending.length
402
+ plan sections === extracted + inlineOnly + alreadyExtracted + registered + pending.length
249
403
  ```
250
404
 
251
- `alreadyExtracted` is the fourth bucket the plan document does not name, and it is what makes the
252
- round trip observable: a second run over an already-componentised tree reports `extracted: 0,
405
+ `registered` is orthogonal to placement: nothing was written for those sections — the file was
406
+ somebody's before the run and stays theirs — so counting them `extracted` would claim authorship of
407
+ bytes this package never produced. `alreadyExtracted` is the bucket the plan document does not
408
+ name, and it is what makes the round trip observable: a second run over an already-componentised tree reports `extracted: 0,
253
409
  pending: []` **and** still accounts for every section. Our own output is recognised by a marker
254
410
  comment, never by its path — `src/components/bcms/` is a directory anybody may put a component in.
255
411
 
256
412
  Known leftovers, stated rather than hidden: a page whose every read moved into a component keeps
257
- the now-unused `bcms`/snapshot imports P2 added. They are harmless at build time and removing them
258
- would mean editing statements no section asked about.
413
+ the now-unused `bcms`/snapshot imports P2 added, and a page whose registered call sites became one
414
+ `<Sections>` keeps the imports of the components it used to call. They are harmless at build time
415
+ and removing them would mean editing statements no section asked about.
259
416
 
260
417
  ## Not here yet
261
418
 
@@ -263,6 +420,9 @@ would mean editing statements no section asked about.
263
420
  bind their own copy; only `findPropTargets` is jsx/astro-only, so a drilled prop there is
264
421
  `PROP_TARGET_NOT_FOUND` rather than a wrong edit).
265
422
 
266
- Componentize: svelte and vue extraction, and Phase B of the lane — copy owned by the INSTANCE
267
- (`overrides`) with the page-lane bindings rewritten to the block lane. `bcmsSection` already
268
- prefers `overrides`, so the repo half of Phase B is a data change rather than another codemod.
423
+ Componentize: svelte and vue extraction. Phase B — copy owned by the INSTANCE — is here for both
424
+ dialects this lane extracts: the block-lane address is chosen at render time, so moving a page's
425
+ copy onto its instances is a data change rather than another cut of the repository. What Phase B
426
+ does NOT reach is the `inlineOnly` shape: a page whose roots could not be looped keeps calling
427
+ `<Hero bind="hero" …/>` in place, which never consults the CMS's blocks at all, so its copy stays
428
+ the page's whatever the plan says.