Skip to content

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

Draft
hotlong wants to merge 9 commits into
mainfrom
claude/issue-8421-unknown-meta-type-refusal
Draft

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

Conversation

@hotlong

@hotlonghotlong 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.

The ruling this implements

Maintainer ruling of 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.

What changed is the platform, not the boundary's information: with additionalTypes retired (#8586 / PR #8702), there is no channel left by which a plugin can DECLARE a metadata kind, so an unrecognised name can no longer be a declaration the refusal has not heard about.

Both premises the domain:spec seat flagged were re-verified on the merged tree rather than inherited:

  1. the registry writer is still one call — packages/metadata/src/plugin.ts:272, the only non-test caller of setTypeRegistry;
  2. a kind still enters the live set only as a side effect of registering an item — the spec's own retirement prescription says so, and registerKind was confirmed to store kind DESCRIPTORS as items of the kind type rather than as new types.

Two deviations from a literal reading, both measured

1. The accept set is the spelling contract's closure, not the registry. Six legal kinds have no static registry entry at all — theme, webhook, connector, sharing_rule, analytics_cube, rag_pipeline. A registry-quantified refusal would break PUT /meta/theme/dark, which is the operation the plugin path exists to serve. So the verdict accepts the URL map's keys AND the canonical singulars they fold to, which is derived rather than hand-written.

2. The refusal is on the door that MINTS, not on all six entry points. Measured against a real registerApp (real ObjectQL, not a mock):

LIVE registry types: ["data","kind","object","package","theme"]
LIVE but OUTSIDE the static spelling contract: ["data","kind","package"]

data (a manifest's seed datasets), kind (contributes.kinds) and package all enter SchemaRegistry during an ordinary app registration, and getMetaTypes() enumerates exactly that live set — so refusing unrecognised names on the READ entries would answer 400 for types the same service advertises at GET /api/v1/meta/types. deleteMetaItem stays open for the mirror-image reason: rows minted under an unrecognised type before this change are real, nothing rewrites them on upgrade, and refusing their deletion would turn the accumulation this card is about into an accumulation nobody can clear.

⚠️ The literal ruling text says "refuse a /meta type spelling", unqualified as to verb. This PR refuses a strict SUBSET of that — the mint verb only. If reads were meant to refuse too, that is a follow-up, and the measurement above is the argument against it.

REWORK (2026-08-15) — two measured regressions of the first cut, fixed

CI triage at f59a135f0 found 5 failures across 3 packages, all raising the same verdict at the same frame, in three consumer packages the first cut never ran. Two of the five were regressions this rework repairs; the rest are the accept-set question, which is with the maintainer and deliberately left red (see the last section). No fixture encoding that question was edited and no gate was touched.

Fix A — the COMPOUND arity is not a metadata-type claim

/metadata/lead/views/all_leads is type='lead', name='views/all_leads': one operation reaching one saveMetaItem, the shape the runtime dispatcher's own /meta branch and rest's PUBLISHED_COMPOUND route both document verbatim. The segment in the :type position is an OBJECT name — runtime data no static contract can enumerate — so applying a static type verdict there refused every object name that is not coincidentally a metadata type. The ruling is about metadata TYPE names like fieldz; this was not a narrowing anyone approved.

The exemption is the request's ARITY (name carries a /), never the string in the type slot: at the simple arity PUT /meta/lead/all_leads is still refused, and that pair is pinned on both sides.

⚠️ Residue, stated rather than hidden: PUT /meta/fieldz/a/b is therefore still accepted, because at that arity fieldz is a claim about an OBJECT, and the only way to check it is the live-registry lookup this very card ruled out.

The second, masked site is now unmasked.runtime's domains/meta-save-capability-gate.test.ts:118 drives this exact path and stayed GREEN through the regression — its caller holds no capabilities, so PERMISSION_DENIED answers before the protocol is resolved, and its saveMetaItem is a vi.fn() that could not have refused anything. A 403 arriving first is not evidence about the door behind it. packages/runtime/src/meta-compound-arity-mint-door.test.ts is new: manage_metadata held, the REAL ObjectStackProtocolImplementation over a real store, and the assertion reads the stored ROW rather than the response body. Its last case keeps the capability gate itself pinned — the fix moved the door behind it, not the gate.

Fix B — a namespace that ALREADY EXISTS is not being minted (measured first)

The triage read two production re-save paths off their call sites. Measured before changing anything, and they do NOT behave the same:

pathwhat actually happens at f59a135f0
migrateStoredMetadata (source: 'migrate-stored')never reaches the door.applyConversionsToStoredItem keys the ADR-0087 chain on the type's manifest collection, and an unrecognised type has none — body untouched, no notice, outcome: 'canonical', saveMetaItem not called. Structural, not lucky: nothing can declare a conversion for a type the platform does not have. Re-measured with a convertible-looking body under the residue type: still canonical.
duplicatePackage (copy/clone)fires. A package holding one residue row answered {success: false, copiedCount: 0, failedCount: 1} with the refusal text in failed[0].error — one pre-existing row made a whole package unduplicatable.

So the exemption is real and needed, and it is keyed on the STORE rather than on anything the caller says: once the static verdict has fired, sys_metadata is probed for the type, and a type that already has rows is not being minted. The copy re-saves under a NEW name, so an exemption keyed on the exact item would not have reached it. The probe never runs on the ordinary save path, and it cannot be spoofed by a request.

An UNPROVISIONED store counts as "no rows" and the refusal stands — a fresh deployment has no residue to protect. Any other read failure propagates as 503 SERVICE_UNAVAILABLE rather than being invented into an existence claim, which is what check:durability-log-level (AGENTS.md "Absence must be loud", #5186) requires and what it caught on the first draft of this probe.

Reverse verification of the rework — direction predicted before running

Both legs ablate the fix in srcand rebuild@objectstack/metadata-protocol, because runtime resolves it through dist and stack traces source-map back to src — a source-only revert measures the pre-mutation artifact. scripts/ablation-dist-preflight.mjs proved the mutation reached the artifact each time (--absent on ablate, present on restore).

legablationpredictedmeasured
Adrop the compound exemption1 RED metadata-protocol, 2 RED / 3 GREEN runtime1 RED / 20 GREEN, 2 RED / 3 GREEN — case for case
Bdrop the already-stored exemption2 RED metadata-protocol (the copy, the new-item-under-residue-type)2 RED / 19 GREEN, exactly those two

The tripwires

Verification

Union re-derived from the real changed paths (node scripts/pm/dispatch-gates.mjs, three-dot origin/main...HEAD) and run at 043d0697f, this branch's head, 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 the derivation named as convention-triggered ratchets: check:type-check-debt --re-measure (33 ledger entries, 1968 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 were run on a FULLY built workspace, as lint.yml does.

Test suites, all at 043d0697f with the dependency closures built:

packagethis branchcontrol at origin/mainab8b10f82
@objectstack/restTests 1921 passed (1921), exit 01921 passed
@objectstack/runtimeTests 1 failed | 2395 passed (2396)2391 passed
@objectstack/objectqlTests 3 failed | 3626 passed (3629)3629 passed
@objectstack/metadata-protocolnew + updated pins 21 passed (21)
@objectstack/specmetadata-url-spelling.test.ts 20 passed (20)

rest is now byte-identical to its control: the compound-arity failure is gone. runtime gained 5 tests (the new unmasked-site file), all passing.

⛔ The remaining red is DELIBERATE — 4 tests, all the accept-set question

The triage's 5 failures are now 4, and every one of them refuses a name at refuseUnmintableMetaType for the reason the maintainer is being asked to rule on. Making them green would decide that question and erase the evidence, so nothing here touches them:

#package · testrefusedwhy it is legitimately red
1objectqloverlay-precedence.test.ts — accepts brand-new triggertriggera kind ADR-0088 retired outright, still used as a runtime-creatable specimen
2objectqlprotocol-meta.test.ts — accepts brand-new trigger and seedtriggeras above (seed is accepted; trigger is what fails)
3objectqlprotocol-meta.test.ts — accepts brand-new plugin-registered typepolicygetMetaTypes() advertises policy as allowRuntimeCreate: true while the mint door refuses it
4runtimemeta-field-overlay-lock.test.ts#7894 POSITIVE CONTROLmy_plugin_kinda synthetic never-heard-of kind, deliberately minted by that control

All four are simple-arity saves of a type with no stored rows, so neither exemption added by this rework can reach them — which is exactly why they are unchanged from the triage measurement rather than accidentally repaired.

The open question is the maintainer's: should the accept set admit the plugin-registered types (policy, data, package, kind) so GET /meta/types and the write door agree, or should those types stop being advertised as writable? Both keep the invariant the failing objectql fixture guards.

Generated by Claude Code

…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
…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

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 15, 2026 1:18am

Request Review

@github-actionsgithub-actionsBot 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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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/mainab8b10f82.

packageat f59a135f0 (PR head)at ab8b10f82 (control)
@objectstack/objectqlTests 3 failed | 3626 passed (3629)Tests 3629 passed (3629)
@objectstack/runtimeTests 1 failed | 2390 passed (2391)Tests 2391 passed (2391)
@objectstack/restTests 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 · testrefusedclassification
1objectqloverlay-precedence.test.ts — accepts brand-new triggertriggerspecimen in a runtime-creatable loop
2objectqlprotocol-meta.test.ts — accepts brand-new trigger and seedtriggeras above
3objectqlprotocol-meta.test.ts — accepts brand-new plugin-registered typepolicysee below
4runtimemeta-field-overlay-lock.test.ts#7894 POSITIVE CONTROLmy_plugin_kinddeliberate positive control
5restmeta-published-overlay.test.ts — §6 COMPOUND arityleadcollateral: 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 notpolicy, 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 onlyTest Files summary (line 1913).
  • @objectstack/restalso 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

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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:

testtype
objectql overlay-precedence — accepts brand-new triggertrigger
objectql protocol-meta — accepts brand-new trigger and seedtrigger
objectql protocol-meta — accepts brand-new plugin-registered typepolicy
runtime meta-field-overlay-lock#7894 POSITIVE CONTROLmy_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 / kindis 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

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

Labels

documentationImprovements or additions to documentationsize/xlteststooling

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

@hotlong@claude