Skip to content

fix(core,metadata,objectql): enforce the #7378 three-cell IMetadataService register ruling in every shipped implementation - #8349

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-7378-metadata-roundtrip-rulings
Aug 13, 2026
Merged

fix(core,metadata,objectql): enforce the #7378 three-cell IMetadataService register ruling in every shipped implementation#8349
os-zhuang merged 5 commits into
mainfrom
claude/issue-7378-metadata-roundtrip-rulings

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #7378 — the implementation half of the maintainer's 2026-08-12 three-cell ruling. #7378 remains open: the spec-side half is out of this PR's reach by dispatch fence (any change under packages/spec/src/** stops the card) and transfers to the domain:spec seat — see "The spec fork" below.

The ruling (2026-08-12, 裁定人:维护者 huangyiirene — verbatim, untranslated)

三格分别裁定:

  1. Row 1(key 归属)= (c) 响亮拒绝。 register(type, name, data)name 参数与 data.name 不一致时,所有实现统一拒绝并报错定位 —— 不一致几乎必是作者 bug,任一方向的静默解决都可能把条目放错位置。这是四个实现都要改的行为变化,按卡内自己的论证,它是唯一不可能静默放错的答案。
  2. Row 2(objects/object 别名)= 所有实现一个答案,与 check:meta-type-normalized 收敛。 类型名归一化是契约级规则,不是各实现自留的民俗;实现者先读该闸门的既有方向再落,两处裁定必须互相引用而不是各说各话。
  3. Row 3(非对象 data 静默丢弃)= 响亮拒绝(throw)。 接受后丢失、且任何成员都读不回来,无可辩护;拒绝一个实现无法键控的 data 与契约同样一致。修的是「接受再丢」,不强求「必须存下」。
  • 三格的 // DIVERGENCE pin 在裁定 PR 内同步更新(该测试文件设计意图即如此)。

Implementations changed, with their packages

One shared guard — assertMetadataRegisterContract (rows 1/3) and canonicalMetadataServiceType (row 2), new module packages/core/src/metadata-service-contract.ts, exported from @objectstack/core (the lowest common dependency) — called by:

Implementation Package Rows 1/3 (refusals) Row 2 (canonical fold)
createMemoryMetadata @objectstack/core (src/fallbacks/memory-metadata.ts) register refuses fold on the single getTypeMap accessor (all members)
MetadataManager (both conformance subjects: registry-only and writable datasource: loader) @objectstack/metadata (src/metadata-manager.ts) register refuses before any store or loader write fold at entry of register, registerInMemory, get, getDiagnosed, list, listDiagnosed, unregister, exists, listNames, subscribe
MetadataFacade @objectstack/objectql (src/metadata-facade.ts) toKeyedDefinition now asserts, then keys — the interim { name, content } boxing and the silent reconcile are both gone fold at entry of every member
contract reference double packages/spec/src/contracts ⛔ NOT changed — spec fence; transfers with the spec fork ⛔ same

Boundary, stated: registerInMemory shares the row-2 fold (a store key is a store fact) but not the rows-1/3 refusals — the ruling names register, and registerInMemory is the boot-time seeding primitive; the guard's header records the boundary.

Cell 2 cross-reference: check:meta-type-normalized (两处裁定互相引用)

The gate's existing direction, read before implementing as the ruling instructs. From scripts/check-meta-type-normalized.mjs's own header: its job is to refuse "the REST layer making a DECISION on the un-normalized value" — the ruled fix for #3984 was structural: "normalize once at the top of each handler, let every gate read that", after three authorization bypasses (#3984, #5881, #6241) came from raw-:type comparisons; plural→singular folding is owned by canonicalMetaType (#4432) via PLURAL_TO_SINGULAR (@objectstack/spec/shared). The gate's scan surface is packages/rest/src; what this PR converges with is its DIRECTION: canonicalMetadataServiceType folds through the same PLURAL_TO_SINGULAR map, once, at each member's entry, so every store key reads the normalized value — and the guard module's header quotes the ruling back at the gate, closing the mutual reference. Result: register('objects', n, d) and get('object', n) address one store on every implementation, in both directions (a driver-local pin covers the reverse read too).

Conformance pins — updated in this ruling PR, as the ruling requires

packages/objectql/src/metadata-service-roundtrip-conformance.test.ts: the // DIVERGENCE era ends. RULED_CONTRACT_ANSWERS overrides the (spec-side, still pre-ruling) table for the five ruled rows: key-is-the-name-argument-object / -nonobject, primitive-data-roundtrips, array-data-roundtrips → refused; plural-objects-type-is-its-own-store → readable. The refusal path asserts, per subject: the rejection's code (VALIDATION_ERROR) AND status (400); the message locates (names the type, the argument, and on mismatch rows BOTH disagreeing spellings); and nothing was stored — absent under the argument key and under the document's own name. Driver-local pins add what the table cannot yet carry: no-name documents still register (absence is not a disagreement), agreeing data.name still registers, and the row-2 fold holds in the reverse direction. Note: arrays are refused with primitives — an array cannot carry document identity, and { ...[a, b] } is { 0: a, 1: b } corruption; the ruling's ban on coercion-into-storability decides the array row with the primitive one.

The spec fork (⛔ NOT in this PR — hard fence; transfers to domain:spec)

  • packages/spec/src/contracts/metadata-service-roundtrip-conformance.ts — the five rows' expected still state pre-ruling reference semantics (and plural-objects-type-is-its-own-store is now a misnomer).
  • packages/spec/src/contracts/metadata-service-roundtrip-conformance.test.ts — the reference double must refuse rows 1/3 and fold row 2.
  • packages/spec/src/contracts/metadata-service.tsregister/get TSDoc still states the superseded 2026-08-11 option-(a) ruling, including a "(c) PARKED for v18 — do not implement" paragraph that the 2026-08-12 ruling overrides.

Until that lands, the objectql driver's overrides carry the ruled answers, and the wiring test "the table still lags the 2026-08-12 ruling" goes red the moment the table is updated — the mechanical handoff signal to delete each override.

Verification

  • Suites (post origin/main merge @ d5031f6a1): @objectstack/core 33 files / 786 tests; @objectstack/metadata 31 / 603; @objectstack/objectql 196 / 3491 — all green. Conformance file alone: 78/78 across all four subjects, refusal paths exercised.
  • Downstream consumer check (direction: downstream, prefix form — the full ...@objectstack/core closure is the whole repo and is CI's half): @objectstack/runtime, the nearest consumer of createMemoryMetadata (kernel fallback, /meta domain), 150 files / 2306 tests green after building its dependency closure.
  • Reverse verification, direction predicted BEFORE running: restoring the four pre-ruling implementation files (from a committed state, no stash) should redden exactly 22 rows — 16 = four refused cases × four subjects, 3 = plural table row on the raw-string subjects with MetadataFacade staying green (its SchemaRegistry read alias pre-dated the ruling), 3 = the driver-local both-directions pin on the same three. Measured: Tests 22 failed | 56 passed (78), distribution exactly as predicted, then restored and re-run green.
  • Production callers swept: no in-tree register call passes a primitive/array, and both callers whose data carries a name derive the argument from it (objectql/plugin.ts, service-datasource/datasource-admin-plugin.ts) — no behavior change for any in-tree caller.
  • Fixture triage: 17 @objectstack/metadata test files mocked @objectstack/core naming only createLogger; converted to the repo's spread-actual precedent so the next core export cannot break them again.
  • Gates: check:durability-log-level, check:engine-double-contract, check-engine-split-ratio, check:query-options-erasure, check:type-check-coverage, check:error-code-casing, check:nul-bytes all green; re-derived via scripts/pm/dispatch-gates.mjs against the real diff, which surfaced three families the dispatch list missed — check:init-service-contract, check:kernel-hook-pairs, check:test-source-alias — all run, all green.
  • Error vocabulary: refusals use the standard catalog's VALIDATION_ERROR + status 400 (the ledger's own guidance: no synonym registration for a generic validation condition), so no ERROR_CODE_LEDGER change was needed — which also keeps this PR clear of the spec fence.

Out of scope, untouched: the content ?? item unwrap in MetadataFacade.get/list (the #7519 seam — its fix wanted this PR's row-3 outcome first, which now exists: no box is ever produced).


Generated by Claude Code

claude added 4 commits August 13, 2026 06:19
…ling in every shipped IMetadataService implementation

Maintainer ruling 2026-08-12 (#7378): row 1 — a data.name disagreeing with
the name argument is refused loudly with a locating error; row 2 — type
stores are keyed on the canonical type, converging with
check:meta-type-normalized's enforced plural->singular direction; row 3 —
a non-object data is refused (throw), never accepted-and-dropped and never
coerced into storability.

The shared guard and fold live in @objectstack/core
(metadata-service-contract.ts) — the lowest common dependency — and are
called by createMemoryMetadata, MetadataManager and MetadataFacade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
…cell ruling; spread-actual core mocks

The conformance driver's DIVERGENCE era ends: RULED_CONTRACT_ANSWERS
overrides the (spec-side, still pre-ruling) table for the five ruled rows —
four refusals asserted with code AND status plus locating-message and
nothing-stored probes, one plural row now readable — with a wiring tripwire
that goes red when the spec seat lands the table half, prompting override
deletion. Driver-local pins add the no-name-is-not-a-mismatch and
both-directions-fold cases the table cannot carry yet.

17 metadata test files mocked @objectstack/core naming only createLogger;
they now spread the actual module (the repo's existing precedent shape) so
the next core export cannot break them again.

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

vercel Bot commented Aug 13, 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 13, 2026 7:26am

Request Review

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 8 package(s): @objectstack/core, @objectstack/driver-memory, @objectstack/metadata, @objectstack/objectql, @objectstack/knowledge-memory, @objectstack/knowledge-ragflow, @objectstack/plugin-dev, @objectstack/plugin-hono-server.

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

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core, @objectstack/knowledge-memory, @objectstack/knowledge-ragflow)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata, @objectstack/objectql)
  • content/docs/concepts/north-star.mdx (via packages/core)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-memory)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/driver-memory, @objectstack/objectql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-memory)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server)
  • content/docs/kernel/cluster.mdx (via packages/metadata)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core, packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/driver-memory, @objectstack/metadata, @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/plugin-dev)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/driver-memory, @objectstack/objectql, @objectstack/plugin-hono-server)
  • content/docs/permissions/authorization.mdx (via packages/core)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/driver-memory, @objectstack/objectql, @objectstack/plugin-hono-server)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/driver-memory, @objectstack/metadata, @objectstack/objectql, @objectstack/plugin-dev, @objectstack/plugin-hono-server)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata, @objectstack/plugin-hono-server)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/protocol/knowledge.mdx (via @objectstack/knowledge-memory, @objectstack/knowledge-ragflow)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-memory, packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/driver-memory, @objectstack/objectql, @objectstack/plugin-hono-server)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/metadata)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server)
  • content/docs/releases/v17.mdx (via @objectstack/core, @objectstack/plugin-dev)
  • content/docs/releases/v9.mdx (via @objectstack/metadata)

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.

Copy link
Copy Markdown
Contributor Author

PM: CI red on f66ce208 — diagnosed, patch round in flight. ⛔ Not a flake, ⛔ not a batch verdict.

domain:engine-core seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy. Recording the diagnosis here so the thread carries it rather than a chat window.

Three jobs red on the same head sha, all from one cause:

job failing package
Test Core (1/3) @objectstack/plugin-hono-server
Test Core (3/3) @objectstack/driver-memory
Temporal Conformance (live PG + MySQL) (same run, same sha)

The discriminator, applied

⛔ This is not a merge-queue red, so the batch-verdict reasoning does not apply — it is the PR's own run. The mechanism test (can this diff physically reach that code path?) answers yes: the new module lives in packages/core, which every one of these packages loads transitively.

Shard 1's log, read in full:

Error: ENOTDIR: not a directory, open '.../packages/spec/src/index.ts/shared'
 ❯ ../../core/src/metadata-service-contract.ts:82:1
 ❯ ../../core/src/fallbacks/memory-metadata.ts:3:1

Test Files 16 failed | 2 passed · Tests 29 passed, 0 failed.

⭐ That shape is the diagnosis. Zero failing assertions with sixteen failing files means they never loaded — nothing about the three-cell ruling's semantics is in question. The new guard imports pluralToSingular from the @objectstack/spec/shared subpath, and these consumers' test configs alias @objectstack/spec to the index file, so the subpath resolves to a path under a file.

⚠️ What I verified vs. what I did not: shard 1's signature I confirmed from its log. For driver-memory and Temporal Conformance I have only the failing job, not their error lines — so I am ⛔ not asserting they are the same signature, and the dev is told to confirm rather than inherit my inference.

Why this points at the source, not the consumers

Two independent consumer packages (plus a third job) breaking on one new import is a class, not a misconfigured package — and the module in question deliberately lives in packages/core, the lowest common dependency. Fixing consumers one at a time would leave the rest of the repo waiting to trip. That is my lean, ⛔ stated as a lean and not a ruling: the dev is measuring how many packages carry the file-mapped alias shape before choosing, and reporting the count.

Two things this exposes beyond the fix

  1. ⚠️ check:test-source-alias was GREEN in the dev's run and did not catch this — a gate passing while the exact class of defect it is named for ships is itself a finding. Being characterised and filed separately.
  2. ⚠️ My review missed it the same way the dev did. Both of us swept @objectstack/runtime as the downstream consumer and stopped there. "Ran a downstream consumer" is not "covered the consumer face" — least of all for a change landing in the lowest common dependency. That one is mine, not the dev's.

⛔ Nothing about the implementation review changes: the shared-guard shape, the enumeration with packages, the spec fence held at the right line, Part of #7378, and the 22-red reverse verification matching its pre-declared distribution all stand. The spec half is transferred as #8350.


Generated by Claude Code

…onfigs (#7378 patch round)

Two halves, both measured before choosing:

- packages/core/metadata-service-contract.ts: the '@objectstack/spec/api'
  import becomes TYPE-ONLY (erased at compile time) with the refusal code as
  a typed literal against the closed set — the storage-service.ts precedent.
  The module every consumer loads now makes one runtime subpath demand
  instead of two. '/shared' cannot be erased: pluralToSingular is a runtime
  value with ONE owner (copying the map would be the folk normalization the
  #7378 row-2 ruling forbids).

- Five vitest configs alias the bare '@objectstack/spec' to spec/src/index.ts
  (a FILE, prefix-matched) with per-subpath entries and no '/shared' entry,
  so the guard's import resolved to spec/src/index.ts/shared - ENOTDIR at
  load: driver-memory (23 test files dead) and plugin-hono-server (16 dead)
  in CI, knowledge-ragflow / plugin-dev / knowledge-memory latently. Each
  gains the '/shared' entry in its config's own established pattern.

check:test-source-alias was green throughout - its reachability walk stops
at the package boundary, one hop short of the aliased dependency's own
import surface; filed as #8351.

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

Copy link
Copy Markdown
Contributor Author

Patch round for the CI red (3b84dd6d6) — what was measured, then what was changed.

Measured

  1. Line 81 vs 82: in plugin-hono-server's config, @objectstack/spec/api has its OWN alias entry, so line 81 genuinely resolves — line 82 (/shared) was not merely first-reported. The config maps the bare @objectstack/spec to spec/src/index.ts (a FILE), alias lists match by PREFIX, and /shared was the one subpath in core's src graph with no entry: spec/src/index.ts/shared → ENOTDIR.
  2. driver-memory confirmed same signature (it was not assumed): ENOTDIR ... spec/src/index.ts/shared at metadata-service-contract.ts:82, 23 test files dead at load, 0 failing assertions.
  3. The class, counted: 9 vitest configs carry the file-mapped bare @objectstack/spec alias. Of those, 5 also alias @objectstack/core to src and lacked /shareddriver-memory, plugin-hono-server (both red in CI), knowledge-ragflow, plugin-dev, knowledge-memory (latently identical, green only because their current test graphs don't reach the new module). The other 4 (metadata, driver-sql, runtime, embedder-openai) already carry /shared entries from earlier rounds of this same trap — several configs document it (/security in this very config, the service-settings: alias the remaining three workspace deps to source in vitest.config.ts, shrinking its check-test-source-alias registry entry to empty #8104 saga in service-settings).
  4. The root-import shape is unavailable: neither pluralToSingular nor StandardErrorCode is exported from the @objectstack/spec ROOT (checked src/index.ts and api-surface/root.json), and adding a root export is packages/spec/src/** — fenced.

Changed

  • Core half — minimize what the shared module demands of every resolver: the /api import is now TYPE-ONLY (erased at compile time; zero runtime demand on that subpath) with the refusal code as a typed literal checked against the closed set — the packages/spec/src/contracts/storage-service.ts precedent, and still check:error-code-casing-clean. /shared cannot be erased: pluralToSingular is a runtime value whose map has ONE owner — copying it into core would be exactly the per-implementation folk normalization the MetadataFacade answers three registerget round-trip cases differently from every other shipped IMetadataService #7378 row-2 ruling forbids.
  • Consumer half — fix the class, not the instance: all 5 breaking configs gain the /shared entry in their own established pattern (subpath entry above the bare one, with the mechanism named in a comment), not just the two that went red.

Proven

  • @objectstack/driver-memory: 25 files / 752 tests green (was 23 files dead).
  • @objectstack/plugin-hono-server: 18 files / 211 tests green (was 16 dead; the 6 files still failing after the alias fix were a LOCAL unbuilt-@objectstack/types artifact of my fresh worktree, gone after building its closure — not the CI class).
  • The 3 latently-broken packages: knowledge-ragflow 1/9, plugin-dev 5/54, knowledge-memory 1/8 — green.
  • What the core edit reaches, re-run: @objectstack/core 33 files / 786 tests; the conformance driver 78/78; @objectstack/objectql typecheck exit 0; check:error-code-casing and check:nul-bytes green; ESLint clean on every touched file.

The gate finding

check:test-source-alias was green throughout, and its rule 5 names this exact ENOTDIR class — but its reachability walk collects specifiers from "the test files plus every intra-package file" only. Aliasing @objectstack/core to src imports core's OWN workspace-import surface into the config's resolution domain, and the walk stops one hop short of it, so a specifier the config mangles at runtime is never resolved statically. Filed as #8351 with the invariant, the measured blast radius, and a candidate direction (extend the walk into any dep the config aliases to src).


Generated by Claude Code


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 08:20
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 9319586 Aug 13, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7378-metadata-roundtrip-rulings branch August 13, 2026 08:35
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants