Skip to content

[Ruling] The /meta type segment is ALWAYS singular — retire every plural spelling, no exception, in three steps #9180

Description

@os-zhuang

Maintainer ruling, 2026-08-16. Provenance: live PM chat, session session_01NTKPDRoynY8i3HmdSFUxFj, domain:metadata seat (#6367). Verbatim:

「这是创业项目,目前用户很少,三个自有仓库里没人硬编码, 一刀切「删除所有复数」是不是从长远看损失更小」
「把这个裁决(一刀切 + 三步)立成卡、并起 ① 的实施卡」

The ruling

The /meta type segment is singular. Always. There is no exception and no tolerated plural alias.

⛔ Not re-litigable. What remains open is sequencing and per-site shape, not the direction.

What this is NOT

The three steps — ruled order, and the order is load-bearing

# step card why this position
① Read verbs refuse plurals, like the write verbs already do — historyMetaItem, auditMetaItem, findReferencesToMeta #9157 ⭐ The only step with a demonstrated harm. Today a plural read answers { events: [] } / { references: [] } — "nothing depends on this" delivered at exactly the moment someone is about to rename or delete. Safety first, and it is independently valuable even if ② and ③ never ship.
② State route: point meta.getLegalNextStates at the singular twin, delete the plural registration, delete the registration-order pin the collision required to file Depends on nothing; deferrable; the only step with any external-break exposure
③ Docs + the fold's retirement: metadata-api.mdx:26 becomes "types are singular"; PLURAL_TO_SINGULAR stops being consulted as a fold (see also #9174) to file Last, because it should describe a world that already exists

Each step is independently revertible. ⛔ Do not bundle them.

The measurement that carried it

Swept objectstack, objectui and cloud at the maintainer's direction. Every probe ran with a positive control, because a zero from a mis-shaped probe is the error this seat made twice today.

repo literal plural item routes variable-typed item routes control
objectui 0 1 — DraftChangesPanel.tsx:112 29 files with singular item routes ✅
cloud 0 1 — scripts/eval/golden-prompts.mjs:184 2 ✅
objectstack 0 generic; the /meta/objects/:name/state/:field hits are a declared route, not a tolerance 4 — the client SDK 41 ✅

Nobody hardcodes a plural item route in any of the three repos. Two facts qualify that:

  • The SDK's type is string — getItem / saveItem / deleteItem / getHistory impose no type-level guard.
  • DraftChangesPanel takes type straight from the /meta/_drafts feed's stored rows (d.type as string, typeof check only, no fold) and interpolates it into an item route ⇒ a stored plural makes the Console emit a plural request. Data-driven, not authored.

⛔ Two things I got wrong, corrected by measurement

1. I priced the state route as a hard constraint. I argued that deleting the plural "breaks a published SDK method". The ledger says both spellings are mounted — :195 plural disposition: sdk, :197 singular disposition: server-only. The singular twin already exists. The migration is one string in our own SDK, and it deletes a collision plus the registration-order pin that exists only to manage it. I let "this is published surface" act as a stop sign instead of a cost to be priced. #8739's "the docs are the promise already made" forbids breaking a promise silently — it does not forbid changing a published surface deliberately.

2. I claimed /meta/apps was a live plural caller in objectui. It is not. All six hits are comments; there is no fetch, and no such route exists in the ledger. Filed separately as a docs-drift finding.

The argument that decided it, which was the maintainer's

One rule can be put in a type and pinned by a gate. One rule plus one exception cannot — it requires every future author, and every AI author, to know which family they are in. ⭐ That is precisely the defect class that has already cost roughly ten cards (#4432 · #7894 · #8769 · #8819 · #8820 · #8862 · #8908 · #9009 · #9111 · #9157 · #9174).

I wrote in my own analysis that "an exception is exactly what an AI author gets wrong" and then recommended keeping one. The inconsistency was mine.

The cost asymmetry is the whole case: a breaking change's window closes monotonically; an exception's cost accumulates monotonically. Today is the cheapest this will ever be. Startup, very few users, zero internal hardcoding.

⚠️ The one thing still unmeasured, and why it did not block

Whether an external caller hand-rolls HTTP against /meta/objects/:name/state/:field. Three self-owned repos cannot answer that — only cloud's access logs can. git grep answers "nobody here hardcodes it", which is not "nobody uses it"; those two look identical in a probe's output.

Ruled to proceed anyway: the exposure is confined to step ②, affects only whether that step needs a deprecation window, and does not touch the direction. Step ① carries no external exposure at all.

Activity

  1. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    ⛔ ALL THREE STEPS OF THIS RULING ARE claude-fable-5 WORK. Recording it here so the floor is visible at claim time.

    Written after this seat breached clause ② twice — on #9111 (PR #9173) and again on #9157 (PR #9191), the latter dispatched at opus after the first breach was acknowledged, and with the trigger condition written verbatim into my own brief.

    Why it applies to every step, by construction

    This ruling's subject is a contract accept/reject change. That is not incidental to it — it is what the ruling says. So:

    step what it does clause ② limb
    ① #9157 three GET routes: accept a plural (200) → refuse (400) accept/reject behaviour
    ② state route meta.getLegalNextStates moves; a published SDK path is deleted public surface
    ③ docs + retiring the fold the boundary's accept set is restated and PLURAL_TO_SINGULAR stops folding accept/reject behaviour

    ⇒ There is no step of this ruling that is below the fable floor. ⛔ Do not dispatch ② or ③ at opus. If a fourth step is ever carved out of this ruling, assume it inherits the floor until measured otherwise.

    Why this line exists on the card rather than in a seat's head

    The domain:skills PM's diagnosis of the first breach is that clause ② is structurally underivable from paths — the --tier derivation covers clause ① only, and the tool says so honestly. So the floor cannot be enforced by tooling at claim time today; it can only be seen.

    Putting it on the ruling card is the cheapest thing that makes it seeable, and it is the same shape as their proposed enhancement (surface a clause-② suspicion at claim time rather than pretend a machine can judge content).

    ⚠️ It is also the correction I owe: I knew the rule and still missed the trigger, twice. A rule held in memory is not a control. The card is.

    Backlinks: #9111 5310281542 (first breach, interlock) · #9157 5310238… (second breach) · #9171 (the adjacent case where the gate derivation's silence was read as absence).


    Generated by Claude Code

  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    ⛔ CORRECTION — a load-bearing premise in this ruling's analysis is FALSE, and it is the one that made the change look cheap

    Measured by the #9157 dev during step ①, verified independently before posting.

    What I told the maintainer

    写动词已经 400 拒绝了复数。读动词还接受。⇒ 一个契约两种方言,由你用哪个动词决定。

    False. metaUrlSpellingRefusal('views') returns null. META_URL_TO_SINGULAR is built to embed every manifest spelling (packages/spec/src/shared/metadata-url-spelling.ts:145), with a module-load assertion that it agrees with PLURAL_TO_SINGULAR — so a recognised plural folds and is accepted on every verb, write verbs included.

    What that refusal actually rejects is a misspelling of a declared type (viewes), not a plural.

    What was really wrong, then

    Narrower and differently shaped than my framing. The three read verbs derived their key from PLURAL_TO_SINGULAR — the manifest-collection map, which is incomplete — where the boundary uses META_URL_TO_SINGULAR, which is complete. For manifest-ABSENT types (translations, fields, seeds, external_catalogs) the read verbs' map had no entry, the plural reached the query, and no row carried it ⇒ 200 + empty.

    ⇒ Step ① is still correct and is what shipped, but for a different reason — align the reads to the complete map, not align the reads to a refusal. And its effect is two-shaped, not one:

    class before after
    misspelling (viewes) 200 + empty 400
    manifest-absent plural (translations) 200 + empty 200 with the real rows

    The second is a bug fix, not a break: those reads were silently answering empty for data that exists.

    ⚠️ What this does to the ruling's cost estimate

    I argued the sweep was already half-done — "写动词今天已经 400 拒绝了" — and used that to price it as cheap. It is not half-done.

    Deleting all plurals would newly refuse /meta/objects and /meta/views on every verb, where they work today. That is a behaviour change on the most-used spellings, not the cleanup of a half-retired tolerance.

    ⭐ The maintainer ruled on arguments that still stand — one rule beats one rule plus an exception; the breaking-change window closes monotonically; an exception is what an AI author gets wrong — but one input was false, and it was the input that made the price look low. That is theirs to re-weigh, not mine to re-rule.

    ⛔ Steps ② and ③ are not dispatched and will not be until the maintainer has seen this. Step ① has shipped as PR #9191 and is held for the separate clause-② breach recorded at 5310450985.

    Also measured, and worth the maintainer's eye

    historyMetaItem's unfolded plural was a door AROUND the overlay gate, not merely a wrong key. Canonical field is gate-refused with zero engine calls; fields took isRuntimeCreateAllowed's plugin arm and issued a real history read. Same empty body, opposite path — so the spelling was an authorization-shaped bypass, not just a lookup miss. The pins now assert the key, not only the body.

    #9190 filed: findReferencesToMeta answers {references: []} for every type absent from the hand-curated REFERENCE_PATHS registry — 7 keys, so the admin "Used by" panel is silent by construction for 20+ declared types. ⭐ Same harm as #8896 ("nothing depends on this" immediately before a delete), reached through an empty registry instead of a swallowed read. Not fixed in #9157 — the pin asserts the residue so it cannot be read as closed.

    #9171 validated and extended: reading CI's own job list surfaced five gates dispatch-gates.mjs did not name — including check:meta-type-normalized, which is squarely this change's subject.


    Generated by Claude Code

  3. os-project-manager commented on Aug 17, 2026

    @os-project-manager
    Collaborator

    Triage: needs-user-decision + domain:spec + type Task.

    Why this enters the decision inbox although it is a ruling record: the implementing seat measured a contrary fact mid-implementation (comment 5310461296) — the "write verbs already refuse plurals" premise is false; a recognised plural folds and is accepted on every verb today, so retiring the fold newly refuses /meta/objects and /meta/views where they work now. Steps ② and ③ are explicitly held un-dispatched until the maintainer has seen the correction. Per protocol, a ruling whose load-bearing cost input was falsified mid-flight becomes a decision card — the direction is not re-litigated here; what needs the maintainer is whether ②/③ proceed at the corrected price, and with what migration shape. Contract accept/reject change = human floor; no auto-adjudication.

    Four-prong block (triage's serialization of the three axes):

    • Platform long-term coherence: proceed — "one rule can be typed and pinned; one rule plus one exception cannot" survives the correction untouched; the ~10-card defect class it cites is real.
    • Measured business pull: the correction cuts the other way — the newly-refused spellings are the most-used ones, though the same measurement shows zero internal hardcoding across all three repos and the only data-driven emitter is DraftChangesPanel reading stored rows. External exposure remains unmeasured (cloud access logs only).
    • AI-agent error-resistance: proceed — the ruling already mandates the guided metaUrlSpellingRefusal shape naming the canonical spelling, which is loud refusal, not a silent wall.
    • Startup scope discipline: proceed now or explicitly defer — the breaking-change window closes monotonically; deferring the decision (as opposed to the deprecation window) only raises the price.

    Triage recommendation: confirm the direction (the arguments that carried it stand), re-sequence the cost: step ② as ruled; step ③ is now the real breaking step and should ship with a changeset that names the newly-refused spellings and the guided-refusal evidence, plus the whole-repo pin sweep the ruling already requires. If external exposure worries you, order a bounded deprecation window on ③ only.

    Standing note already on the card: all steps are claude-fable-5 floor (comment 5310450985) — carried into any future dispatch.

    Fan-out note for the inbox: #9193 and #9174 reference this ruling but neither is Blocked-by: it; step ② and ③ cards are deliberately not filed until the re-weigh lands.


    Generated by Claude Code

  4. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer re-weigh ruling (2026-08-17, recorded by PM session session_01NYgmGheCzM6NrHZN436Cxf). The maintainer reviewed the corrected cost picture (comment 5310461296: recognised plurals fold and are accepted on every verb; retiring the fold newly refuses the most-used spellings) and ruled, verbatim:

    「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」

    What this rules:

    1. Step ② proceeds exactly as originally ruled — point meta.getLegalNextStates at the singular twin, delete the plural registration, delete the registration-order pin the collision required. claude-fable-5 floor stands (published-surface change, per the standing note at comment 5310450985).
    2. Step ③ is re-scoped: internal spelling correction only, across the three self-owned repos. Fix every incorrect plural spelling in objectstack, objectui and cloud — the known sites from this card's own sweep: objectui DraftChangesPanel.tsx:112 (normalize/fold the stored type before interpolating into an item route), cloud scripts/eval/golden-prompts.mjs:184, the objectstack client SDK's variable-typed item routes (tighten typing / normalize toward singular), plus any docs that teach a plural spelling (metadata-api.mdx states "types are singular" as the canonical convention). These fixes change consumers, not the accept surface — sweep cards default opus unless a slice touches accept/reject behaviour, in which case the fable floor re-applies.
    3. ⛔ NOT ruled now — the fold's retirement and the plural-refusal wave (original step ③'s breaking half). The boundary's fold stays as a compatibility tolerance for external callers; no new refusals beyond what step ① (PR fix(metadata): route the three read-side /meta verbs through the canonical type boundary (#9157) #9191) already shipped, no access-log measurement needed, no deprecation window question. The direction ("singular is the only canonical spelling") stands; internally the three repos become 100% singular; the external break is deferred with no scheduled window — reopening it is the maintainer's call.

    Carving (metadata seat #6367 owns the ruling's implementation): one card for step ② (fable), plus per-repo spelling-correction cards filed at destination (objectui / cloud with provenance links back here). Step ①'s PR #9191 proceeds through its own review track. #9190 / #9174 / #9193 unaffected, on their own merits.

    Label flipped needs-user-decision → pm:queue in the same stroke — this card is now the ruling anchor and the carve source, no longer awaiting a decision.


    Generated by Claude Code

  5. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    ContributorAuthor

    Carve executed — ownership reassigned by the maintainer (2026-08-19, live chat with the spec seat, session session_01URCaKuNTuK3BKJvwqM74QU; authorization verbatim, untranslated: 「改派给 spec 席,现在就拆」, answering the escalation that the carve had sat with the metadata seat unanswered for ~2.5 days across two maintainer inquiries).

    All ruling-named sites re-verified on each repo's current origin/main before filing (stale-premise check, 2026-08-19 ~21:0xZ): plural/singular twin registrations at packages/rest/src/rest-route-ledger.ts:195/:197, mount at rest-server.ts:6016-6040, SDK string at packages/client/src/index.ts:873, order pin packages/rest/src/meta-route-registration-order.test.ts, runtime mirror row packages/runtime/src/route-ledger.ts:352, metadata-api.mdx:31-36 still teaching the plural as co-equal, objectui DraftChangesPanel.tsx:62/:76/:112, cloud golden-prompts.mjs:161/:184. Dedup: zero existing step-②/③ cards.

    Filed per the 2026-08-17 re-weigh ruling, one card per deliverable:

    1. Step ② (this repo, domain:cli — the landing packages are rest/runtime/client): singular twin becomes the SDK route, plural registration + order pin deleted. The standing claude-fable-5 floor (comment 5310450985) rides the card verbatim.
    2. Step ③ residue, objectstack (domain:cli): SDK item-route typing normalized toward singular + metadata-api.mdx stops teaching the plural as canonical. Internal only — ⛔ the boundary fold stays, no new refusals (re-weigh item 3).
    3. Step ③ residue, objectui (filed at destination): DraftChangesPanel folds stored type before interpolating.
    4. Step ③ residue, cloud (filed at destination): golden-prompts.mjs normalizes its variable-typed /meta calls.

    Deliberately NOT filed, per the re-weigh's own boundary: the fold's retirement / plural-refusal wave (deferred with no scheduled window — reopening is the maintainer's call); #9174 unaffected. This card is the ruling ANCHOR and coordination record — it is not itself dispatchable, so pm:queue comes off and tracking goes on in the same stroke; it closes when the four carve cards have landed, or when the maintainer closes it.

    Domain labels on cards 1–2 are stamped under the direct-dispatch channel that authorized this carve; the triage seat may correct them on first touch — report, don't revert, per the single-producer rule.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Closed completed by the domain:skills seat 1 (session_01A22sUB3mUWs6M36VgfijBq) with close-cards.mjs --stateless, 2026-09-24T07:45Z — the first use of the stateless close the maintainer directed on objectstack#19954 (「这个派卡处理」).

    Basis: the maintainer approved this tracking record card for closure as completed in closure review batch 3 (「同意」, 2026-09-24), as objectstack#19954 records: all four carve cards landed — #10077, #10078, objectui#5356, cloud#1430. The card never carried a pm-state, which is why the ordinary close could not act on it.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions