Commit 4d0bd23
fix(platform-objects): decide the six bare metadata types' display en-echoes per leaf, and derive the population from the type registry (#19585)
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: no `fields`, no `sections`. Their registry `label` and
`description` are the only strings an author ever sees for them, and
every one of those twelve leaves was still its English source in
`zh-CN`, `ja-JP` and `es-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:
> only a real panel keeps the DERIVED-POPULATION property with a working
dark control — six unrelated types degenerate the derivation and the
control has nothing to exclude.
**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 `en` subtree.**
`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 no
`fields` and no `sections`, so the registry entry *is* its whole panel.
Both halves are derived and neither is the ledger's own opinion: the
registry lives in `packages/spec`, a package this round does not touch,
and it is the thing that manufactures these leaves — `seed.label` in the
catalog *is* the registry entry's own `label`, 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.**
1. **Outward** — the 17 types that do carry a form are excluded, and
their own type-level pairs are already authored. The sharpest single
exclusion is `dataset`: it carries a registry `description` exactly 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.
2. **Inward** — **four** members of the population (`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:
refilling `job.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`: `Ai` to `AI` differs 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 `en` side of every row is the
registry entry in `packages/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.
| leaf | the word | verdict |
|:--|:--|:--|
| `doc.description`, `capability.description` | `package` | **near-miss
REAL** — it is a legal `MetadataProvenanceSchema` value 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 whole `en` catalog 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 literally `package`, it renders the display and keeps the value
(`sys_metadata.fields.managed_by.options.package` is 包 / パッケージ /
Paquete). |
| `mapping.description` | `rename` | not a `TransformType` member
(none/constant/lookup/split/join/javascript/map) — rendered. **But `map`
is**, 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.description` | `publish` | looks like a `SeedMode`, is not one —
rendered, with this catalog's own authored 发布 / 公開 / Publicar. |
| `api.description` | `pipeline` | looks like an
`ApiEndpointSchema.type`, is not one
(flow/script/object_operation/proxy) — rendered. |
| `book.description` | `groups` | a key that takes an **array**, so no
rendered word can land in it — the rule round 6 used to clear
`timeoutMs`. |
## "Capability" in three positions, decided two ways
- `hook`/`action.fields.body.capabilities.label` — a
`HookBodyCapability` **token 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
| family | keys | decidable leaves | `.label` leaves |
|:--|--:|--:|--:|
| `object.fields.enable.*` (+ `validations`) | **9** | 11 | 27 |
| `report.fields.drilldown` + `runtimeFilter` +
`sections.dataset_binding` | 3 | 6 | 9 |
| **remaining total** | **12** | **17** | **36** |
Census re-taken on this branch's base `88920d153` and on the merged head
`699e2e862`:
| reading | base | head |
|:--|--:|--:|
| `en` string leaves / `.label` leaves | 893 / 538 | 893 / 538 — same
population |
| `.label` keys echoing in ALL THREE | **18** (54 `.label` leaves) |
**12** (36) |
| decidable sibling remainder | **29** | **17** |
| POSITIVE CONTROL `zh-CN` / `ja-JP` / `es-ES` | 520 / 504 / 504 | **526
/ 510 / 510** |
The 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**, `en` untouched. Both
directions of the comparator proved on a probe key, each mutation landed
on disk and restored:
- **added** — injecting `seed.__probe__` into `zh-CN`: `base=3572
head=3573 added=1 removed=0`.
- **removed** — deleting `zh-CN` `seed.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.PROP` is 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 empty `git diff HEAD`.
| # | mutation | reds |
|:--|:--|:--|
| A | `zh-CN` `seed.label` refilled with its `en` source | `zh-CN: every
decided leaf matches its verdict`; `no leaf in the bare class reads its
en source unless the ledger decided it is an echo` |
| B | `zh-CN` `job.label` refilled — **a leaf no row decides** | `DARK,
INWARD — four members of the population come back NON-ECHOING`; the same
class rule. **This is the proof the substitute property is real.** |
| C | `en` catalog `Seed Data` reworded to `Seed data` | `every row is
pinned to the live en source`; `the echo predicate can say "echo"`. The
**registry** leg stays green — the two legs are independent. |
| D | the **registry** entry in `packages/spec` reworded, spec rebuilt |
exactly one: `…and to the REGISTRY entry that manufactures it — the
third leg`. The catalog pin stays green. |
| E | `rename` made a legal `TransformType` in `packages/spec`, spec
rebuilt | `rename is NOT a TransformType — but map is, and "field
mapping" contains it` |
| F | the derived population swapped for the **hand-list rounds 5/6
assumed** | `the 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.**
1. The first key-set negative control was **refused by the tool** — "the
anchor count moved 1 to 1, a drop of 0, not the declared 1" — because
the replacement re-contained the anchor. It was **re-authored** as a
true replacement (and the removal direction re-authored again into
`--delete` mode when the replacement count would not rise), not re-run
until something landed.
2. Ablation D's first dist marker was `Seed data`, which **pre-exists**
in `packages/spec` in 12 unrelated places (`'Seed data loader
configuration'`, `'Seed data records'`, doc comments). Its `--absent`
restore leg read RED as the tool's own header warns it can — a surviving
hit that was never mine — and, worse, its `present` leg would have
passed even had the mutation never reached `dist`. **That reading was
void**, the marker was re-authored to the discriminating `type: "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 through `spec/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-legged
`origin/main` moved from `88920d153` to `b3615f1a4` while this round
ran, so the check was **not vacuous** — and the two commits (#19538,
#19513) touch **nothing** in
`packages/platform-objects/src/apps/translations/`.
- **Leg 1 — something really moved:** `git diff --stat 88920d1
origin/main` = 10 files, +262 / -60, including
`packages/spec/src/data/object.zod.ts`.
- **Leg 2 — it survived:** after the merge, `git diff origin/main HEAD`
is 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 as
`COMMAND :: exit CODE` and the exit code captured **before** any pipe,
so the zero is derived rather than claimed:
> 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a
DERIVED zero — all 60 recorded an exit code and none of them is 3).
Plus, on the merged head `699e2e862`: dependency-closure build exit 0;
`pnpm --filter @objectstack/platform-objects test` exit 0 — **51 files /
770 passed** (+1 file, +37 tests vs round 6); `typecheck` exit 0; `pnpm
check:i18n` exit 0; repo-wide `pnpm lint` exit 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-loads` and `check:i18n` both
wanted a full build (`turbo run build --concurrency=2
--filter='!@objectstack/docs'`, 73 tasks, exit 0);
`check:type-check-debt` named `@objectstack/platform-objects`
specifically. 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 spelled `pnpm exec turbo run build --concurrency=2
--filter=...`.
## Changeset — measured, not assumed
`@objectstack/platform-objects` declares no `private` and ships `files:
["dist", ...]`, so `skip-changeset` is measurably wrong. Probed in
`dist` after a real build, with controls:
| probe | raw | `\uXXXX` lower | `\uXXXX` UPPER | `\xNN` lower | `\xNN`
UPPER |
|:--|--:|--:|--:|--:|--:|
| 种子数据 (`zh` `seed.label`) | 0 | 0 | **6** | 0 | 0 |
| ケイパビリティ (`ja` `capability.label`) | 0 | 0 | **42** | 0 | 0 |
| Documentación del paquete (`es`) | 0 | 0 | 0 | 0 | **6** |
| `Endpoint API` (pure ASCII) | **6** | 6 | 6 | 6 | 6 |
| NEGATIVE CONTROL — non-ASCII never written | 0 | 0 | 0 | 0 | 0 |
| NEGATIVE CONTROL — ASCII never written | 0 | 0 | 0 | 0 | 0 |
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 word `Package`: it *is* a legal
`MetadataProvenanceSchema` value, 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 the `en` metadata-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.helpText` renders "system
capability **keys**" as システム**機能**キー in `ja-JP`, using the word round 6
measured as wrong for a capability token and replaced on the `hook` and
`action` panels. 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 the `permission` panel is a wording question for
whoever takes that panel. **承接者:无** — no open PR and no queued family
touches `packages/platform-objects/src/apps/translations/` on the
`permission` entry, so nothing is scheduled to pass through that file.
Dedupe words: permission systemPermissions capability 機能 ja-JP.
- `es-ES` answers "permission sets" two ways in the same catalog —
`conjuntos de permisos` (`permission.sections.identity.description`) and
the English `permission 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 a `git add -A` and removed in its own
commit before the merge; the final diff is 8 files, all in
`packages/platform-objects/src/apps/translations/` plus the changeset.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 731f020 commit 4d0bd23
8 files changed
Lines changed: 968 additions & 72 deletions
File tree
- .changeset
- packages/platform-objects/src/apps/translations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
Lines changed: 918 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 12 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
611 | | - | |
612 | | - | |
| 611 | + | |
| 612 | + | |
613 | 613 | | |
614 | 614 | | |
615 | | - | |
616 | | - | |
| 615 | + | |
| 616 | + | |
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
| |||
1708 | 1708 | | |
1709 | 1709 | | |
1710 | 1710 | | |
1711 | | - | |
1712 | | - | |
| 1711 | + | |
| 1712 | + | |
1713 | 1713 | | |
1714 | 1714 | | |
1715 | 1715 | | |
| |||
1795 | 1795 | | |
1796 | 1796 | | |
1797 | 1797 | | |
1798 | | - | |
1799 | | - | |
| 1798 | + | |
| 1799 | + | |
1800 | 1800 | | |
1801 | 1801 | | |
1802 | | - | |
1803 | | - | |
| 1802 | + | |
| 1803 | + | |
1804 | 1804 | | |
1805 | 1805 | | |
1806 | 1806 | | |
| |||
1875 | 1875 | | |
1876 | 1876 | | |
1877 | 1877 | | |
1878 | | - | |
1879 | | - | |
| 1878 | + | |
| 1879 | + | |
1880 | 1880 | | |
1881 | 1881 | | |
1882 | 1882 | | |
| |||
Lines changed: 0 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | 21 | | |
30 | | - | |
31 | | - | |
32 | 22 | | |
33 | 23 | | |
34 | 24 | | |
| |||
78 | 68 | | |
79 | 69 | | |
80 | 70 | | |
81 | | - | |
82 | | - | |
83 | 71 | | |
84 | 72 | | |
85 | 73 | | |
| |||
Lines changed: 12 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
611 | | - | |
612 | | - | |
| 611 | + | |
| 612 | + | |
613 | 613 | | |
614 | 614 | | |
615 | | - | |
616 | | - | |
| 615 | + | |
| 616 | + | |
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
| |||
1708 | 1708 | | |
1709 | 1709 | | |
1710 | 1710 | | |
1711 | | - | |
1712 | | - | |
| 1711 | + | |
| 1712 | + | |
1713 | 1713 | | |
1714 | 1714 | | |
1715 | 1715 | | |
| |||
1795 | 1795 | | |
1796 | 1796 | | |
1797 | 1797 | | |
1798 | | - | |
1799 | | - | |
| 1798 | + | |
| 1799 | + | |
1800 | 1800 | | |
1801 | 1801 | | |
1802 | | - | |
1803 | | - | |
| 1802 | + | |
| 1803 | + | |
1804 | 1804 | | |
1805 | 1805 | | |
1806 | 1806 | | |
| |||
1875 | 1875 | | |
1876 | 1876 | | |
1877 | 1877 | | |
1878 | | - | |
1879 | | - | |
| 1878 | + | |
| 1879 | + | |
1880 | 1880 | | |
1881 | 1881 | | |
1882 | 1882 | | |
| |||
Lines changed: 0 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | 21 | | |
30 | | - | |
31 | | - | |
32 | 22 | | |
33 | 23 | | |
34 | 24 | | |
| |||
78 | 68 | | |
79 | 69 | | |
80 | 70 | | |
81 | | - | |
82 | | - | |
83 | 71 | | |
84 | 72 | | |
85 | 73 | | |
| |||
Lines changed: 12 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
611 | | - | |
612 | | - | |
| 611 | + | |
| 612 | + | |
613 | 613 | | |
614 | 614 | | |
615 | | - | |
616 | | - | |
| 615 | + | |
| 616 | + | |
617 | 617 | | |
618 | 618 | | |
619 | 619 | | |
| |||
1708 | 1708 | | |
1709 | 1709 | | |
1710 | 1710 | | |
1711 | | - | |
1712 | | - | |
| 1711 | + | |
| 1712 | + | |
1713 | 1713 | | |
1714 | 1714 | | |
1715 | 1715 | | |
| |||
1795 | 1795 | | |
1796 | 1796 | | |
1797 | 1797 | | |
1798 | | - | |
1799 | | - | |
| 1798 | + | |
| 1799 | + | |
1800 | 1800 | | |
1801 | 1801 | | |
1802 | | - | |
1803 | | - | |
| 1802 | + | |
| 1803 | + | |
1804 | 1804 | | |
1805 | 1805 | | |
1806 | 1806 | | |
| |||
1875 | 1875 | | |
1876 | 1876 | | |
1877 | 1877 | | |
1878 | | - | |
1879 | | - | |
| 1878 | + | |
| 1879 | + | |
1880 | 1880 | | |
1881 | 1881 | | |
1882 | 1882 | | |
| |||
Lines changed: 0 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | 21 | | |
32 | 22 | | |
33 | 23 | | |
| |||
45 | 35 | | |
46 | 36 | | |
47 | 37 | | |
48 | | - | |
49 | | - | |
50 | 38 | | |
51 | 39 | | |
52 | 40 | | |
| |||
0 commit comments