fix(platform-objects): decide the six bare metadata types' display en-echoes per leaf, and derive the population from the type registry - #19585
Conversation
…three locales Round 7 of the en-echo decision series — the 12 leaves of the six bare metadata-type display pairs (`seed`, `mapping`, `api`, `doc`, `book`, `capability`). Leaf values only; the extract regeneration and the decision ledger follow. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…round-7 renderings `pnpm i18n:extract` dropped exactly the 12 decided leaves' provenance rows in each of the three locales and added none; the metadata-forms bundles came back byte-identical to the hand-entered values. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…s a ledger One row per string leaf with its per-locale verdict, the reason, and the `en` source — pinned to the live catalog AND to the registry entry that manufactures it. The population is derived from `DEFAULT_METADATA_TYPE_REGISTRY` crossed with a bare-shape predicate, with dark controls outward (17 panel types, `dataset` by name) and inward (four bare types that come back non-echoing). Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
`@objectstack/platform-objects` is publishable (`files: [dist, ...]`, no `private`) and the rendered strings were measured in `dist`: CJK ships as UPPER-CASE \uXXXX escapes, U+0080-U+00FF as UPPER-CASE \xNN, with raw non-ASCII and ASCII negative controls both reading 0 — so a zero on the raw probe reads 'escaped', not 'absent'. patch, not skip-changeset. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
An exploratory tsx probe under `packages/spec/` was swept in by a `git add -A` during the round's census work. It is not part of the change. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cfa24579c115ea8c6c1612cdf62d530492d7e67f && git checkout cfa24579c115ea8c6c1612cdf62d530492d7e67f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3615f1a4cd7f3ff59ff0548530daa042627f732 699e2e8624ad06c7a07c614e9c8368a045479459 && git checkout -B drift-repro b3615f1a4cd7f3ff59ff0548530daa042627f732 && git merge --no-ff 699e2e8624ad06c7a07c614e9c8368a045479459
node scripts/docs-audit/affected-docs.mjs --json b3615f1a4cd7f3ff59ff0548530daa042627f732 |
Part of #19403
Clause-②: no
Round 7 of the en-echo decision series. The six bare metadata types —
seed,mapping,api,doc,book,capability— have no form at all: nofields, nosections. Their registrylabelanddescriptionare the only strings an author ever sees for them, and every one of those twelve leaves was still its English source inzh-CN,ja-JPandes-ES. They are decided here, one leaf at a time, with the reason recorded for each.The three-exit question, answered: EXIT 1 — taken, with a stated substitute
Rounds 5 and 6 both refused this family on one standing reason:
That refusal was right about what it refused. It was reasoning about a population spelled
['seed','mapping','api','doc','book','capability']— a hand-list, which derives nothing and excludes nothing. That is not the only population available, and this round names the substitute rather than deferring a third time.What derives the population, if not one panel's
ensubtree.DEFAULT_METADATA_TYPE_REGISTRY— the spec-side list of every metadata type the platform declares — crossed with one predicate read off the catalog's shape: a type is bare when its catalog entry has nofieldsand nosections, so the registry entry is its whole panel. Both halves are derived and neither is the ledger's own opinion: the registry lives inpackages/spec, a package this round does not touch, and it is the thing that manufactures these leaves —seed.labelin the catalog is the registry entry's ownlabel, and every row is pinned to both. Ten types satisfy the predicate; the six decided here are the six that echoed.What the dark control can exclude — twice, and the second one is the half a panel walk never has.
dataset: it carries a registrydescriptionexactly as these six do, so any walk keyed on "registry entries with a description" would sweep it in. The bare predicate is what leaves it out, and it is already authored, so a walk that wrongly included it would not even go red. Asserted by name.job,datasource,external_catalog,translation) are in the walk and come back non-echoing. A hand-list of six can only ever produce positives; this derivation produces a negative on four of its own members, in the same run, from the same walk. That is the control the refusal said this family could not have, and ablation B below is its proof: refillingjob.label— a leaf no row in this ledger decides — reds the file.And the substitute is strictly stronger than a panel walk in one measurable way. A panel walk is bounded by a type that already exists. This one is bounded by a shape over the registry, so a new metadata type — the actual way this defect class reproduces, since every one of these twelve leaves was born the day its registry entry was added — lands in the population automatically. No single-panel ledger in this series can do that. What is not claimed: that the property is preserved unchanged. It is substituted, and the controls are real but are not the same controls.
The phantom-translation trap — structurally out of reach here
Round 5 measured it on
action.fields.ai.label:AitoAIdiffers in bytes, so it passes the echo predicate in all three locales and drops the census by a key while telling a zh-CN author nothing. An echo that stops matching is not the same thing as a leaf that got translated.Here the shortcut is not merely refused, it is unreachable, and that is a property of the family nobody chose: the
enside of every row is the registry entry inpackages/spec/src/kernel/metadata-plugin.zod.ts, not a string in this package. Touching up the English would be an edit to another package, outside this round's file surface. The only edit reachable from here is the one that actually renders the leaf — and ablation D proves the registry pin is live and independent.The schema readings — five near-misses, each asserted, none described
Resolved through
getMetadataTypeSchema(the registry's own schema map) rather than a hand-picked import list.doc.description,capability.descriptionpackageMetadataProvenanceSchemavalue and both schemas accept_provenance: "package"while refusing a rendered one. Cleared three ways: the key is an envelope field the loader sets (the item parses without it, and the wholeencatalog names no underscore-prefixed key anywhere); these types are bare, so the string labels no input; and where this catalog does render a leaf whose stored value is literallypackage, it renders the display and keeps the value (sys_metadata.fields.managed_by.options.packageis 包 / パッケージ / Paquete).mapping.descriptionrenameTransformTypemember (none/constant/lookup/split/join/javascript/map) — rendered. Butmapis, and "field mapping" contains it:'field mapping'.includes('map')is TRUE while the word-boundary predicate says NO. That pair is the token guard's dark control.seed.descriptionpublishSeedMode, is not one — rendered, with this catalog's own authored 发布 / 公開 / Publicar.api.descriptionpipelineApiEndpointSchema.type, is not one (flow/script/object_operation/proxy) — rendered.book.descriptiongroupstimeoutMs."Capability" in three positions, decided two ways
hook/action.fields.body.capabilities.label— aHookBodyCapabilitytoken list. Round 6 moved both to 能力 / ケイパビリティ. Untouched, and asserted so: the cross-panel invariant stays at 30/30.object.sections.capabilities.label— the object's feature toggles, a different concept under the same English stem. Still 功能开关 / 機能 / Capacidades, deliberately, and asserted so.capability.label— the ADR-0066 metadata type, whose instances are named authorization capability keys (export_data,billing.refund, from the schema's own name-regex message). Takes 能力 / ケイパビリティ / Capacidad — agreeing with the landed pair by re-deriving from the same objects-catalog evidence (sys_user.fields.ai_access.help,sys_email.fields.attachments_json.help) rather than borrowing its decision, and differing from it in Spanish number because this leaf names one capability and that one names a list.A precedent answers only the question it contains; round 6's contained the token list, not this type.
The remainder, in keys AND leaves — two counts, two questions
.labelleavesobject.fields.enable.*(+validations)report.fields.drilldown+runtimeFilter+sections.dataset_bindingCensus re-taken on this branch's base
88920d153and on the merged head699e2e862:enstring leaves /.labelleaves.labelkeys echoing in ALL THREE.labelleaves)zh-CN/ja-JP/es-ESThe headline falls by 6 and the decidable remainder by 12, because this round decides six labels and six descriptions and only the labels move the headline. State which count you mean.
No key added, no key removed
Full flattened key sets compared base against head across all four bundles: 3572 entries, 0 added, 0 removed,
enuntouched. Both directions of the comparator proved on a probe key, each mutation landed on disk and restored:seed.__probe__intozh-CN:base=3572 head=3573 added=1 removed=0.zh-CNseed.description:base=3572 head=3571 added=0 removed=1.Regenerated with
pnpm i18n:extract, which rewrote the three bundles byte-identical to the entered values and dropped exactly the 12 provenance rows per locale that recorded these leaves as unauthored extractor fills, adding none.metadataForms.TYPE.PROPis now empty in all three tables where the base held exactly these twelve.Ablations — 6 runs, 11 distinct assertions, 5 mutation targets across 4 files
Every one through
scripts/ablation-replace.mjs: the anchor had to hit and fall, the blob had to change, and each restore is proved by blob-equals-HEAD plus an emptygit diff HEAD.zh-CNseed.labelrefilled with itsensourcezh-CN: every decided leaf matches its verdict;no leaf in the bare class reads its en source unless the ledger decided it is an echozh-CNjob.labelrefilled — a leaf no row decidesDARK, INWARD — four members of the population come back NON-ECHOING; the same class rule. This is the proof the substitute property is real.encatalogSeed Datareworded toSeed dataevery row is pinned to the live en source;the echo predicate can say "echo". The registry leg stays green — the two legs are independent.packages/specreworded, spec rebuilt…and to the REGISTRY entry that manufactures it — the third leg. The catalog pin stays green.renamemade a legalTransformTypeinpackages/spec, spec rebuiltrename is NOT a TransformType — but map is, and "field mapping" contains itthe BARE predicate splits that population, and is read off the SHAPE not a name list;DARK, INWARD;this class is now DONE.Two disclosures, volunteered.
--deletemode when the replacement count would not rise), not re-run until something landed.Seed data, which pre-exists inpackages/specin 12 unrelated places ('Seed data loader configuration','Seed data records', doc comments). Its--absentrestore leg read RED as the tool's own header warns it can — a surviving hit that was never mine — and, worse, itspresentleg would have passed even had the mutation never reacheddist. That reading was void, the marker was re-authored to the discriminatingtype: "seed", label: "Seed data", and D was re-run from scratch. Both spec-side ablations then carried a real dist preflight (marker present in 4/in 20 built files) and a real restore leg (marker absent from all 216/218 built files, tree clean).Because this ledger imports
@objectstack/spec/kernel— unaliased, so it resolves throughspec/dist— the spec-side ablations do need a rebuild, and each carries one. That is a difference from round 6, whose ledger reached only sibling sources; it is stated rather than inherited.Merge with
origin/main, two-leggedorigin/mainmoved from88920d153tob3615f1a4while this round ran, so the check was not vacuous — and the two commits (#19538, #19513) touch nothing inpackages/platform-objects/src/apps/translations/.git diff --stat 88920d153 origin/main= 10 files, +262 / -60, includingpackages/spec/src/data/object.zod.ts.git diff origin/main HEADis empty on all 10 of those paths. Nothing was reverted.The merged head was then rebuilt and every reading below re-taken on it.
Gates — 60 derived / 60 run / 0 NOT MEASURED / 0 UNRUN
Reconciled by
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran, with every family recorded asCOMMAND :: exit CODEand the exit code captured before any pipe, so the zero is derived rather than claimed:Plus, on the merged head
699e2e862: dependency-closure build exit 0;pnpm --filter @objectstack/platform-objects testexit 0 — 51 files / 770 passed (+1 file, +37 tests vs round 6);typecheckexit 0;pnpm check:i18nexit 0; repo-widepnpm lintexit 0 over 6970 files, 0 messages — the full population, so no narrowing is claimed and none is owed.Three exit 3s, all discharged by building the closure the gate named, then re-run:
check:dual-build-cjs-loadsandcheck:i18nboth wanted a full build (turbo run build --concurrency=2 --filter='!@objectstack/docs', 73 tasks, exit 0);check:type-check-debtnamed@objectstack/platform-objectsspecifically. All three then exit 0, and the final sweep on the merged head produced no exit 3 at all.The
--forwarding trap was avoided by construction: every build in this round is spelledpnpm exec turbo run build --concurrency=2 --filter=....Changeset — measured, not assumed
@objectstack/platform-objectsdeclares noprivateand shipsfiles: ["dist", ...], soskip-changesetis measurably wrong. Probed indistafter a real build, with controls:\uXXXXlower\uXXXXUPPER\xNNlower\xNNUPPERzhseed.label)jacapability.label)es)Endpoint API(pure ASCII)CJK ships as UPPER-CASE
\uXXXX; U+0080–U+00FF as UPPER-CASE\xNN. Both negative controls read 0 everywhere, so a zero on the raw probe reads "escaped", not "absent". The pure-ASCII probe reads 6 in every column because its escaped spellings are identical to its raw one — the sanity note that the counter is counting.patch.Acceptance notes
Zero cards filed, and that is a reading, not a silence. The leaf that could have carried a known trap is
doc.description/capability.description's opening wordPackage: it is a legalMetadataProvenanceSchemavalue, both schemas really do accept_provenance: "package"and refuse a rendered one, and the check was performed at the schema and then pinned in the ledger, together with the derived assertion that theenmetadata-forms catalog names no underscore-prefixed key anywhere. None of the five schema readings produced a defect: four of the words are not enum members at all, and the fifth is one an author never writes on these types because these types have no form.Two observations, neither filed, both prose here because neither carries a class or a carrier:
permission.fields.systemPermissions.helpTextrenders "system capability keys" as システム機能キー inja-JP, using the word round 6 measured as wrong for a capability token and replaced on thehookandactionpanels. It is not an echo — it is authored — so it is outside this card's family, and it is not a defect against any declared contract: no runtime reads it. Whether the objects-catalog rendering (ケイパビリティ) should reach thepermissionpanel is a wording question for whoever takes that panel. 承接者:无 — no open PR and no queued family touchespackages/platform-objects/src/apps/translations/on thepermissionentry, so nothing is scheduled to pass through that file. Dedupe words: permission systemPermissions capability 機能 ja-JP.es-ESanswers "permission sets" two ways in the same catalog —conjuntos de permisos(permission.sections.identity.description) and the Englishpermission sets(position.sections.position.description). This round took the Spanish form, because the leaf it describes is the permission domain's own definition side, and the row records the split rather than harmonising the other leaf. Not a defect; a recorded divergence. Dedupe words: es-ES permission sets conjuntos position panel.One scratch file (
packages/spec/probe-tmp.mts, an exploratory census probe) was swept into a commit by agit add -Aand removed in its own commit before the merge; the final diff is 8 files, all inpackages/platform-objects/src/apps/translations/plus the changeset.Generated by Claude Code