Skip to content

fix(metadata): refuse a /meta type name the platform does not have, instead of minting a namespace for it (#8421) - #8770

Merged
hotlong merged 12 commits into
mainfrom
claude/issue-8421-unknown-meta-type-refusal
Aug 15, 2026
Merged

fix(metadata): refuse a /meta type name the platform does not have, instead of minting a namespace for it (#8421)#8770
hotlong merged 12 commits into
mainfrom
claude/issue-8421-unknown-meta-type-refusal

Conversation

@hotlong

@hotlong hotlong commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #8421

PUT /api/v1/meta/fieldz/showcase_task.title answered 200 {"success":true} and persisted a sys_metadata row under type='fieldz' — a namespace for a metadata type that does not exist and never will. It now answers 400 INVALID_REQUEST and persists nothing.

Status: the hold is lifted. The four deliberately-red tests this PR carried are now correct and green, and the accept-set question they were waiting on is ruled. See "The four reds, resolved" below for why each expectation changed.

The rulings this implements

Ruling 1 — the premise (maintainer, 2026-08-15, verbatim, untranslated):

暂时不考虑让插件申明新的元数据类型

Plugins do not declare new metadata types. The "permissive by construction" posture — anything without a static registry entry is treated as plugin-registered, hence writable — is therefore a rule whose premise has expired.

⚠️ 暂时 is load-bearing and is landed as such: a CURRENT posture, not a permanent architectural closure. Both code sites that encode the deferral name the decision and its date in place — getMetaTypes()'s synthesis comment and isRuntimeCreateAllowed's doc in packages/metadata-protocol/src/protocol.ts — so a future author who needs plugin-declared kinds finds the decision rather than re-deriving it from the code's silence. The changeset carries the same trail for a reader who never opens the source.

Ruling 2 — the accept set (four prisms aligned): GET /meta/types stops advertising policy / data / package / kind as runtime-creatable. Read and write doors agree again, at the honest value.

Ruling 0 — the original refusal (maintainer, 2026-08-14, verbatim: 「同意」), joint with #8586: the simplified option B — refuse by the static contract, no live-set sampling. Option C stays ruled out; the measurement on the card showed its hazard is steady-state rather than a boot race, because the live type set is ITEM-POPULATED and therefore omits every legitimate kind that has no items yet.

Ruling 2, implemented: one predicate, two doors

Not a blanket flip of the synthesis. Six legitimate plugin kinds have no static registry entry — theme, webhook, connector, sharing_rule, analytics_cube, rag_pipeline — and PUT /meta/theme/dark is the operation the plugin path exists to serve. Breaking those is the one outcome that would make this change worse than the defect it closes.

getMetaTypes() now derives allowRuntimeCreate from unrecognisedMetaTypeRefusal — the same predicate refuseUnmintableMetaType enforces — instead of synthesising true for anything lacking a registry entry. One fact, read twice, so the doors agree by construction rather than via two independently-maintained rules:

GET /api/v1/meta/types  ->  entries[] where type is one of policy, data, package, kind
  before: allowRuntimeCreate: true    (advertised writable, refused on write)
  after : allowRuntimeCreate: false   (agrees with the door)

GET /api/v1/meta/types  ->  entries[] where type is theme, webhook, connector,
                            sharing_rule, analytics_cube, rag_pipeline
  before: allowRuntimeCreate: true
  after : allowRuntimeCreate: true    (UNCHANGED, and pinned by name)

Every live type is still LISTED either way — withdrawing the advertisement is not withdrawing the type, and reads still answer for all four. Only the value of one boolean changes.

isRuntimeCreateAllowed deliberately did NOT narrow, and its doc now says why. It was the write-side twin of the read-side rule; it is now the residue/clearance arm behind the mint door. By the time it is consulted for an out-of-contract type, the caller is in deleteMetaItem / historyMetaItem / rollbackMetaItem / promoteDraftForPublish, or in a save behind one of the mint door's two exemptions. Returning false there would route pre-existing residue rows off the repository path and strand them — turning the accumulation this card was filed about into an accumulation nobody can clear, which is the exact reasoning that kept deleteMetaItem open in the first place. Narrowing it would close nothing.

No packages/spec source change was needed for any of this. unrecognisedMetaTypeRefusal is unchanged and allowRuntimeCreate: z.boolean().default(true) in the spec schema is untouched — only a consumer of the predicate moved. (This PR's pre-existing spec edits are the ones the original refusal shipped with.)

The four reds, resolved — one line each

# package · test why its expectation changed
1 objectql overlay-precedence.test.ts — runtime-creatable specimens trigger was DEBT independent of any ruling: ADR-0088 retired the kind, and this same file asserts allowedFromRegistry.has('trigger') is false ~150 lines below. It was green only through the "no static registry entry" fall-through this card closes, while reading as a pin on the declared allowRuntimeCreate tier. REPLACED by mapping, which really does declare allowOrgOverride: false, allowRuntimeCreate: true.
2 objectql protocol-meta.test.ts — "accepts brand-new trigger and seed" same dead kind, same reason, same remedy. Now "accepts brand-new mapping and seed"; the seed half is untouched.
3 objectql protocol-meta.test.ts — "accepts brand-new plugin-registered type" ruling 2. The theme half stays and stays green — it is the discriminating control. The policy half becomes its OWN case, CHANGED BEHAVIOUR — policy is no longer one of them, asserting the ADR-0112 envelope and that nothing is persisted. Split rather than edited so the narrowing is visible to a reviewer, and so the theme control cannot be masked by it.
4 runtime meta-field-overlay-lock.test.ts#7894 POSITIVE CONTROL its metaUrlSpellingRefusal claim is UNCHANGED and still holds by construction: my_plugin_kind is a misspelling of nothing, so #7894's verdict cannot fire on it. What moved is the BOUNDARY, which now consults a second verdict. The case asserts the refusal is the not-a-type-at-all one (names the type, offers no replacement spelling) and that no namespace is minted. The theme positive control directly above it is untouched and still 200.

⛔ None of the four was edited to pass. Each was measured red at the merged head first, and the two trigger cases would have been corrected even had ruling 2 gone the other way.

The discriminating control — new, and the reason the narrowing is reviewable

packages/metadata-protocol/src/protocol.meta-types-mint-door-agreement.test.ts drives the REAL getMetaTypes() and the REAL saveMetaItem() on one protocol instance over a stub engine whose registry returns a registerApp-shaped live set, and asserts the two doors agree across a sample spanning all three classes:

  • statically declaredview, hook (creatable) and agent (NOT creatable, so the false direction is present too);
  • URL-map-only plugin kindtheme, advertised creatable AND driven end-to-end through a write that persists a row;
  • withdrawnpolicy, data, package, kind, advertised false and refused with the ADR-0112 envelope, persisting nothing.

A fourth case quantifies all six URL-map-only kinds on the advertisement, and a fifth compares "advertised" against "write honoured" as a cross-product so a drift in either direction fails even if both tables were updated together and wrongly. A suite covering only the withdrawn four could not show the doors agree; one without theme could not show the narrowing is narrow.

Reverse verification — direction predicted before running, both legs

leg ablation predicted measured
A restore the blanket allowRuntimeCreate: true 5 RED / 13 GREEN in the new file (4 advertise cases + the cross-product; every PUT case GREEN, since the mint door is untouched) 5 failed / 1465 passed package-wide, exactly policy, data, package, kind and the cross-product
B the BLANKET FLIP hazard — synthesise false for everything without a registry entry 3 RED / 15 GREEN (theme's advertisement, the cross-product, the six-kind quantifier); the withdrawn four GREEN 3 failed / 15 passed, exactly those three

Leg B is the one that matters for review: it proves the control can actually distinguish "narrow" from "the outcome that would be worse than the defect". Both legs were run from the committed fix and restored with git checkout off the branch; the restore was verified byte-identical (git diff HEAD empty), not by re-typing.

What #8770 already established — re-verified on the merged base, not inherited

The base was stale by 36 commits (protocol.ts alone moved on six landings). main was merged FIRST — cleanly, no conflicts — and every measurement in this PR was re-taken at the merged head rather than carried over.

The rework's two exemptions are unchanged and still hold, and their own pins (protocol.stored-residue-resave.test.ts, meta-compound-arity-mint-door.test.ts) are green in the runs below: the compound arity (/metadata/lead/views/all_leads puts an OBJECT name in the :type slot, so the exemption keys on the request's arity, never the string in the type slot) and already-existing namespaces (measured: migrateStoredMetadata never reaches the door, duplicatePackage does, and one residue row made a whole package unduplicatable — so the exemption keys on the STORE, cannot be spoofed by a request, and an unprovisioned store counts as "no rows").

⚠️ Correction to this PR's earlier verification section: @objectstack/rest was reported "byte-identical to its control at ab8b10f82" for the previous round. That comparison was NOT re-run this round — main has moved 36 commits and rest gained tests. What is measured here is rest fully green at this head (1948 passed), which is the claim that survives; the byte-identity sentence should be read as historical.

Verification

Union re-derived from the REAL changed paths (node scripts/pm/dispatch-gates.mjs) and run at eac5a2583 — this branch's head, on a clean tree, after the final commit. Exit codes read unpiped, all 0:

check:nul-bytes, check:engine-double-contract, check:error-code-casing, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:merge-driver, check:objectui-changeset, check:spec-parsed-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check-adr-0087-registration, check-changeset-no-major, check-cross-package-test-inputs, check-empty-changeset, check-dev-prereqs, check:doc-formula-expressions, plus the two convention-triggered ratchets: check:type-check-debt --re-measure (33 ledger entries, 1926 raw errors, none above its recorded number — the one surplus is @objectstack/lint at -1 and predates this branch) and spec check:generated (all 13 artifacts up to date, check:api-surface / check:export-origins / check:authorable-surface among them). Both ratchets ran on a FULLY built workspace, as lint.yml does.

One gate the previous derivation did not name: node scripts/check-engine-split-ratio.mjs, surfaced by re-deriving against the actual changed paths (matched via packages/objectql). Run, exit 0.

⚠️ metadata-protocol hides its tests from tsc, so its pnpm typecheck says nothing about the TEST_DEBT ledger — the ledger was therefore measured directly, via the --re-measure ratchet above. The new test file added no error above any recorded ceiling.

Test suites, all re-run at eac5a2583 on a clean tree with the workspace closure built:

package this branch, at eac5a2583 measured at the merged head BEFORE this round (e0cfc1daa)
@objectstack/objectql Test Files 210 passed, Tests 3672 passed (3672) 3 failed / 3668 passed (3671) — the three reds
@objectstack/runtime Test Files 161 passed, Tests 2421 passed (2421) 1 failed / 2420 passed (2421) — the fourth red
@objectstack/metadata-protocol Test Files 99 passed, Tests 1470 passed (1470) not separately run before the edit; the ablation legs bracket it (5 failed / 1465 passed on leg A)
@objectstack/rest Test Files 118 passed, Tests 1948 passed (1948) not separately run before the edit — nothing in this round touches rest source
@objectstack/spec Test Files 402 passed, Tests 10644 passed (10644) not separately run before the edit — spec source is unchanged this round

A contract-surface change must run its consumers, so objectql, runtime and rest are all here — omitting exactly those three is what cost this card its first lap. objectql gains 1 test (the split policy case) and metadata-protocol gains 18 (the agreement suite); everything else is flat.

Package typecheck is green for metadata-protocol, objectql, runtime and spec.

Generated by Claude Code

claude added 5 commits August 14, 2026 21:43
…8421)

`PUT /api/v1/meta/fieldz/showcase_task.title` answered 200 and persisted a
`sys_metadata` row under `type='fieldz'` — a namespace for a metadata type
that does not exist. #7894 closed the sibling case (a plural spelling of a
DECLARED type) and left this one open, because a static predicate cannot tell
`fieldz` from a plugin kind and the live-registry alternative was measured to
be worse than the defect.

Maintainer ruling 2026-08-14, joint with #8586: retiring `additionalTypes`
removed the last channel by which a plugin could DECLARE a metadata kind, so
an unrecognised name can no longer be a declaration the boundary has not heard
about.

`@objectstack/spec` gains `unrecognisedMetaTypeRefusal`, deliberately separate
from the #7894 verdict: one says you spelled a declared type wrongly and can
name the replacement, the other says there is no such type and never guesses.
Its accept set is the static spelling contract — the map's keys AND the
canonical singulars they fold to — so the six plugin kinds with no registry
entry (`theme`, `webhook`, `connector`, `sharing_rule`, `analytics_cube`,
`rag_pipeline`) stay writable, including the first create of a kind that has
no items yet.

The boundary applies it at `saveMetaItem` only, which is measured rather than
timid: an ordinary `registerApp` puts `data`, `kind` and `package` into the
live type set that `GET /api/v1/meta/types` advertises, so a read-side refusal
would 400 types this same service publishes; and refusing DELETE would strand
rows minted under an unrecognised type before this change.

The residue pin in `metadata-url-spelling.test.ts` is flipped, not deleted,
and #7894's positive control keeps every assertion it was written with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…test.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…t-set narrowing

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…tsc ratchet at 63

The stub's `keyOf` took `Record<string, unknown>`, which a `Row` interface is
not assignable to — one new TS2345 in a package whose measured error count is a
shrink-only ledger entry. Reading the four key fields structurally serves both
callers without a cast.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 15, 2026 10:04am

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

hotlong commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

CI triage measurement — Test Core failures ARE caused by this PR

Measurement only; no product code, fixtures or gates were changed. Posted so the evidence survives independently of the triage session. This is not an os-dev-report completion signal — the card remains held.

Verdict: caused by the PR. Five test failures across three packages, one root cause.

Measured at PR head f59a135f0 in a dedicated worktree, controlled against origin/main ab8b10f82.

package at f59a135f0 (PR head) at ab8b10f82 (control)
@objectstack/objectql Tests 3 failed | 3626 passed (3629) Tests 3629 passed (3629)
@objectstack/runtime Tests 1 failed | 2390 passed (2391) Tests 2391 passed (2391)
@objectstack/rest Tests 1 failed | 1920 passed (1921) Tests 1921 passed (1921)

Control runs were FULL suites, all green. The objectql numbers reproduce the CI job exactly.

Every failure raises the same verdict at the same frame — canonicalizeMetaRequestType at packages/metadata-protocol/src/protocol.ts:259, reached from saveMetaItem at :11303.

The five failures, by refused spelling

# package · test refused classification
1 objectql overlay-precedence.test.ts — accepts brand-new trigger trigger specimen in a runtime-creatable loop
2 objectql protocol-meta.test.ts — accepts brand-new trigger and seed trigger as above
3 objectql protocol-meta.test.ts — accepts brand-new plugin-registered type policy see below
4 runtime meta-field-overlay-lock.test.ts#7894 POSITIVE CONTROL my_plugin_kind deliberate positive control
5 rest meta-published-overlay.test.ts — §6 COMPOUND arity lead collateral: fails in the fixture's ARRANGE step

Three points the PR body does not currently cover

1. policy is advertised as writable by the same service that now refuses it. The failing objectql case carries a standing comment naming the invariant it guards:

theme, connector, data, policy, sharing_rule, webhook, analytics_cube, package are registered by plugins at runtime — not in DEFAULT_METADATA_TYPE_REGISTRY. getMetaTypes() synthesises descriptors with allowRuntimeCreate: true for them so the admin UI advertises them as writable. The write gate must agree, otherwise users see "writable" types 403 on save.

The accept set is 68 spellings folding to 33 canonical types. It carries theme, webhook, connector, sharing_rule, analytics_cube, rag_pipeline — but not policy, data, package or kind. So the PR closes the write door on a type GET /api/v1/meta/types still advertises as writable. That is the exact hazard the fixture was written to catch, restated as a 400 instead of a 403.

2. The compound arity puts an OBJECT name in the :type segment. runtime's own dispatcher fixture documents the shape verbatim:

/metadata/lead/views/all_leads -- type lead, name views/all_leads. The dispatcher reaches ONE saveMetaItem for both name shapes.

lead is an object, not a metadata type. Since the refusal is applied to the :type segment on the mint path, the compound-name PUT form now refuses every object name that is not coincidentally a metadata type. A second site in runtime (domains/meta-save-capability-gate.test.ts:118) uses the same shape and stays green only because a PERMISSION_DENIED 403 fires first — a latent site, masked rather than unaffected.

3. Two PRODUCTION paths re-save STORED rows through the refused door. The PR deliberately leaves deleteMetaItem open so pre-existing residue rows stay clearable. But residue rows are also re-saved, not only deleted:

  • packages/metadata-protocol/src/protocol.ts:12442saveMetaItem({ type: singular, ..., source: 'migrate-stored' })
  • packages/metadata-protocol/src/protocol.ts:14391saveMetaItem({ type: row.type, ... }) in the copy/clone path

Both take their type from an existing sys_metadata row. A row minted under an unrecognised type before this change would now be refused on migrate and on copy. No test covers either path with such a row, so this is reasoned from the call sites, not measured.

On the reported EPIPE — it is teardown noise, and it masked a second real failure

The Test Core (2/3) log has 37,540 lines; the failures sit around line 1,868, far above the tail. Reading the whole log resolves all three puzzles:

  • objectql failed with 3 assertions and printed the run's only Test Files summary (line 1913).
  • @objectstack/rest also failed in that same job× §6 the COMPOUND arity resolves the runtime-published sub-resource at line 4165, inside the rest:test group (1978–4897), ending ELIFECYCLE Test failed at 4896. Its summary block never printed, which is why a tail-scoped or summary-scoped grep finds no trace of it.
  • The write EPIPE is inside the connector-rest group (opens 4898), after ✓ src/rest-provider.test.ts (4 tests). It is the fork-pool pipe closing as turbo tore the run down following the earlier failures.

So turbo's Failed: @objectstack/objectql#test label and the visible EPIPE do belong to different tasks, and neither names the rest failure. EPIPE appeared in no local run across six suite executions.

Commands

git worktree add ../objectstack-triage-8770 f59a135f05e2033c9e73272cf144795762757821
pnpm --filter '@objectstack/objectql^...' --filter '@objectstack/runtime^...' \
     --filter '@objectstack/rest^...' --filter '@objectstack/metadata-protocol^...' build   # exit 0
pnpm --filter @objectstack/objectql test -- --maxWorkers=2                                  # exit 1
pnpm --filter @objectstack/runtime  test -- --maxWorkers=2                                  # exit 1
pnpm --filter @objectstack/rest     test -- --maxWorkers=2                                  # exit 1

Consumer sweep beyond the three above: metadata-core (162 passed), hono (73 passed), plugin-email (passed), plus a repo-wide static scan of 4,048 files for mint-path type literals outside the accept set. connector-rest failed locally only on Failed to resolve entry for package "@objectstack/service-automation" — an artifact of my partial build closure, unrelated to this PR.

Generated by Claude Code


Generated by Claude Code

@github-actions github-actions Bot added size/xl and removed size/l labels Aug 15, 2026

hotlong commented Aug 15, 2026

Copy link
Copy Markdown
Contributor Author

CI red at 043d0697f is expected and intentional. The blocker is a pending maintainer decision, not a defect.

Test Core (2/3) failed. Recording why no fix is being pushed, so nobody reads this red as unattended.

What the red is

The rework closed both regressions it was scoped to:

  • rest: 1 failed → 1921 passed, exit 0, byte-identical to the origin/main control at ab8b10f82.
  • runtime: the compound-arity regression is fixed; +5 new pins, all passing.

What remains is 4 tests, all at the same frame (refuseUnmintableMetaTypesaveMetaItem) with the literal message '<type>' is not a metadata type:

test type
objectql overlay-precedence — accepts brand-new trigger trigger
objectql protocol-meta — accepts brand-new trigger and seed trigger
objectql protocol-meta — accepts brand-new plugin-registered type policy
runtime meta-field-overlay-lock#7894 POSITIVE CONTROL my_plugin_kind

All four are simple-arity saves of a type with no stored rows, so neither exemption this rework adds can reach them. They are the unratified accept-set narrowing itself — the thing awaiting a ruling on #8421 — and making them green would erase the evidence that decision rests on.

⛔ Why this PR is not being pushed toward green

Two of the three routes to green are unavailable to me and the third is wrong:

  • widening the accept set to admit policy / data / package / kind is the pending decision;
  • narrowing what GET /meta/types advertises is the same decision from the other side;
  • editing the fixtures to encode the divergence would ratify it silently — and it means GET /meta/types advertising a type the write door refuses.

One thing I could not verify, stated rather than assumed

I confirmed from the CI log that the failing task is @objectstack/objectql#test. I could not independently confirm the failure count from CI: the objectql vitest summary sits above the fetchable window (482KB for 2000 lines; a larger tail exceeds the tool's limit). The "4 remaining" figure comes from the dev's full-suite run at this exact head plus an independent triage run at the previous head that found the same three objectql failures.

I am flagging that gap because I made the opposite mistake on this very PR earlier tonight — concluding "no assertion failure anywhere" from a log tail, when the failures sat ~2,900 lines above it. Two independent local measurements agreeing is good evidence; it is not the same as having read CI's own summary, and I would rather say so.

Also newly measured, and it needs its own disposition

Two of the four refuse trigger — a kind ADR-0088 retired outright, which the objectql fixtures still exercise as a runtime-creatable specimen. So this class has five names, not four, and trigger may be a stale fixture rather than part of the accept-set question. ⚠️ Not established: whether that retirement is complete was not measured.

Blocked on: the maintainer's ratification on #8421 — the two ruling narrowings, plus the accept-set contents, plus trigger's disposition. Nothing further from this seat until then.


Generated by Claude Code

claude added 3 commits August 15, 2026 09:11
…oor's own contract (#8421)

GET /meta/types synthesised allowRuntimeCreate: true for every live type with
no static registry entry, while the mint door added by this card refuses the
subset of them that is outside the static spelling contract. Two endpoints of
one service contradicting each other is worse for an AI author than a narrower
surface, so both doors now read one predicate.

Maintainer ruling 2026-08-15 (verbatim, untranslated):
暂时不考虑让插件申明新的元数据类型

The premise of 'no registry entry => assume plugin-declared => writable' has
expired, so policy/data/package/kind stop being advertised as runtime-creatable.
The six URL-map-only plugin kinds (theme, webhook, connector, sharing_rule,
analytics_cube, rag_pipeline) are unaffected and pinned as the discriminating
control. 暂时 is recorded as a CURRENT posture at both sites.

isRuntimeCreateAllowed keeps its permissive arm deliberately: it is now the
residue/clearance arm behind the mint door, not the read door's twin, and
narrowing it would strand rows minted before the refusal shipped.

Fixture corrections: trigger (ADR-0088-retired) replaced by mapping as the
runtime-creatable specimen in two objectql cases; policy's acceptance becomes
its own CHANGED BEHAVIOUR refusal case; #7894's never-heard-of-kind positive
control keeps its metaUrlSpellingRefusal half and updates its boundary half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…the 暂时 posture (#8421)

The changeset described the mint-door refusal only, and its 'what breaks'
section still named the accept-set question as open with the maintainer. Both
are now settled: GET /api/v1/meta/types stops advertising policy/data/package/
kind as allowRuntimeCreate, the six URL-map-only plugin kinds are explicitly
unaffected, and the ruling's 暂时 is recorded as a current posture with the two
code sites that carry the trail back to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

meta-unknown-type-namespace: a /meta type name that is not a plural of anything still mints a namespace — PUT /meta/fieldz/x answers 200

2 participants