Skip to content

fix(objectql): correct mergeObjectDefinitions docblock to the real, closed merge set (#12680) - #12742

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-12680-merge-object-definitions-docblock
Aug 27, 2026
Merged

fix(objectql): correct mergeObjectDefinitions docblock to the real, closed merge set (#12680)#12742
os-zhuang merged 4 commits into
mainfrom
claude/issue-12680-merge-object-definitions-docblock

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes#12680

What changed

mergeObjectDefinitions's docblock (packages/objectql/src/registry.ts) said:

Fields are merged additively. Other props: later value wins.

The implementation has never done the second half. It merges exactly:
fields (additively), validations (additively), indexes (additively), and
the three guarded scalars label / pluralLabel / description
(last-writer-wins, subject to the tenantAuthored yield rule). Every other
top-level prop on an extend contributor is silently discarded

merged starts as { ...base } and nothing outside that list is ever copied.

This PR:

  1. Corrects the docblock to state the real, closed merge set.
  2. Says the discard out loud, preciselymergeObjectDefinitions itself
    silently discards every non-enumerated top-level prop: no error, no
    warning from this function, the base's value simply wins. But declarative
    authors never reach that silence for tenancy / permissions:
    ObjectExtensionSchema (packages/spec/src/data/object.zod.ts) is
    .strict() — its shape is exactly extend / fields / label /
    pluralLabel / description / validations / indexes / priority
    so an objectExtensions entry naming an undeclared key fails loudly at
    authoring time with a prescription (未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001; also documented at
    content/docs/data-modeling/object-extensions.mdx). The function's
    discard is reachable only by a caller that bypasses that schema — a
    direct, programmatic registry.registerObject(def, pkg, undefined, 'extend', …) call, which is exactly how this PR's pin exercises the rule.
  3. Adds a pin
    packages/objectql/src/registry-object-extension-nonenumerated-prop-discard.test.ts
    that hands mergeObjectDefinitions (via the public SchemaRegistry API) an
    extend contributor carrying a non-enumerated top-level prop (icon — a
    real, spec-legal, security-neutral prop; deliberately not tenancy) and
    asserts the merged result does not carry it, with the guarded scalar
    label as a positive control in the same fold.

Why the closed merge set is not only a documentation nicety. For
tenancy / permissions, ObjectExtensionSchema's strictness is a first
line of defence and the closed merge set here is a second, redundant one. But
_provenance is not a declarable, schema-checked key at all — it is
stamped internally by applyProtection on every registerObject call
(packages/objectql/src/metadata-facade.ts:182) and read by
isTenantAuthored (registry.ts) to decide whether an object is
tenant-authored, including in the ownership-reassignment guard. For
_provenance, this function's closed merge set is the only line of
defence: the ablation below shows that widening it lets a third-party
extend contributor flip a tenant-authored object's _provenance to the
extending package's, reaching the exact outcome ADR-0029 D9.3's
priority-reranking guard exists to prevent, by a route that guard was not
written against. That is why this is a live security question for
_provenance, not a hypothetical one the way it is for tenancy.

Explicitly out of scope (per triage/PM dispatch)

  • Implementing "later value wins" for the undocumented remainder — making
    tenancy / permissions extender-writable is a separate, much larger
    decision that has NOT been made.
  • Adding a runtime warning on the silent drop — a real question, filed
    separately (see below) rather than folded into this docblock fix.

Merge set, measured from origin/main (not copied from the card)

Read directly off mergeObjectDefinitions in packages/objectql/src/registry.ts:
merged = { ...base }, then only fields (additive spread), validations
(additive concat), indexes (additive concat), and the three
OBJECT_FOLD_SCALAR_KEYS (label, pluralLabel, description,
last-writer-wins subject to tenantAuthored) are ever copied onto merged.
This matches the card's claim exactly — no drift found.

Contract / public-surface note (clause ②)

This is a docs-only correction plus a regression pin — zero runtime
behaviour changed.
mergeObjectDefinitions is not exported (module-private
to registry.ts); the pin exercises it indirectly through the existing
public SchemaRegistry.registerObject / getObject API, so nothing new is
exported and no public surface is widened or narrowed.

Tests

  • New pin: pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 registry-object-extension-nonenumerated-prop-discard.test.ts — 2/2 passed.
  • Full registry*.test.ts suite (27 files): 393/393 passed — no regressions.
  • pnpm --filter @objectstack/objectql typecheck — passed (note: this
    package's tsconfig.json excludes **/*.test.ts, so it does not
    type-check the new test file itself; the new file follows the same as any
    literal-casting pattern used throughout this suite's sibling test files).
  • Ablation (required for the new pin): prediction committed first
    (3e23512e), then mergeObjectDefinitions was mutated to literally
    implement the old, false docblock ("later value wins" for every
    non-enumerated top-level prop). Predicted 2 failing tests (both in the
    new pin file); observed 3
    — the 2 predicted, plus one unpredicted
    collateral failure in registry-object-overlay-layer.test.ts ("an extender
    declaring priority 140 does not become the base layer", failing on
    _provenance changing from 'org' to 'package'). That test's extend
    contributor never declares _provenance in its own literal — it arrives on
    the stored contributor definition via applyProtection's internal stamp
    (see above), which is why the merge set protecting it is load-bearing on
    every normal extend registration, not only on a bypass path. The
    prediction's named positive control (a different test in the same file,
    "extenders still fold on top of whichever layer is the base") stayed green
    as predicted.
    Restored via git checkout HEAD -- packages/objectql/src/registry.ts and
    verified: git hash-object matches the HEAD blob, git diff HEAD is
    empty, no marker residue, and the full registry*.test.ts suite (393
    tests) re-ran green post-restore. Full detail, including the deviation
    from the written prediction, in the issue-comment report.
  • Gate union (node scripts/pm/dispatch-gates.mjs --changed, re-derived
    at head): 23 matched + 6 convention-triggered (new test file) gates, all
    run and green, except two genuinely NOT MEASURED: check:pm-half-states
    (no usable GITHUB_TOKEN in this container — prerequisite refusal, exit 3)
    and the --re-measure half of check:type-check-debt (refuses without the
    full 78-package workspace closure built, which is CI's lint.yml preamble,
    not a local obligation for a 2-file, 1-package diff — the cheap structural
    half, check:type-check-coverage, DID run and passed clean). Full list in
    the issue-comment report.

Verified at head 4e20d452.

Generated by Claude Code

…losed merge set (#12680)
The docblock claimed "other props: later value wins"; the implementation
only ever merged fields/validations/indexes additively plus the three
guarded scalars label/pluralLabel/description, silently discarding every
other top-level prop on an extend contributor. Corrects the docblock to say
so explicitly, and adds a pin (registry-object-extension-nonenumerated-prop-discard.test.ts)
that hands mergeObjectDefinitions a non-enumerated top-level prop via the
public SchemaRegistry API and asserts it does not survive the fold, with a
guarded scalar as a positive control.
No runtime behaviour changed; mergeObjectDefinitions is not exported.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
Mutation to be applied: registry.ts's mergeObjectDefinitions() will get an
added pass-through loop that copies every top-level `extension` key NOT in
{fields, validations, indexes, label, pluralLabel, description} onto
`merged` (last-writer-wins) — i.e. give it the literal behaviour the old,
false docblock promised ("other props: later value wins").
Predicted RED (exact named set, in
packages/objectql/src/registry-object-extension-nonenumerated-prop-discard.test.ts):
1) "mergeObjectDefinitions — closed merge set (#12680) > discards a
non-enumerated top-level prop (`icon`) from an extend contributor
silently"
— fails on `expect(resolved.icon).toBe('base-icon')`: under the
mutation `resolved.icon` becomes 'extender-icon'.
2) "mergeObjectDefinitions — closed merge set (#12680) > discards a
non-enumerated prop the BASE never declared, rather than materializing
it from the extension"
— fails on `expect(resolved.icon).toBeUndefined()`: under the mutation
`resolved.icon` becomes 'extender-icon'.
Predicted count: exactly 2 failing tests, 1 failing file (this pin file).
Both failures are assertion failures (not crashes) — the `label` assertion
inside test (1) executes and passes BEFORE the failing `icon` assertion, so
the file-level failure is caused by the pin's own icon assertions, not by an
unrelated crash.
Predicted GREEN (positive control, external to the new pin file, exercising
the SAME fold machinery — proves the mutation cut the intended thing, not
the whole suite):
packages/objectql/src/registry-object-overlay-layer.test.ts >
"ADR-0029 D9.2 — the overlay REPLACES the base layer, bit for bit" >
"extenders still fold on top of whichever layer is the base"
— asserts an extend contributor's `fields` merge (`ext_field` appears,
`packaged_only` does not). The mutation only adds a pass-through for
keys OUTSIDE {fields, validations, indexes, label, pluralLabel,
description}, so this fields-only assertion is untouched and must stay
green.
Also predicted GREEN, as a broader control: every other test in
registry-object-overlay-layer.test.ts and the rest of the
registry*.test.ts suite (393 tests measured pre-mutation across 27 files,
minus the 2 predicted above) — none of them assert on a non-enumerated
top-level scalar prop surviving or not surviving an extend fold, so none
should be sensitive to this specific mutation.
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️1 changed file(s) yielded no anchor (packages/objectql/src/registry.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/registry.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 34d30118c50f4940b22679dd963abfa9ceac1a69packageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 27, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — one addition, then ACCEPT

Reviewer of record: domain:engine PM seat (#6367). Verified at head 3e23512e. Fences: packages/spec 0 · content/docs/releases 0 · docs/adr 0 · .claude 0. Merge set re-read off origin/main independently and it matches what you measured.

The prediction miss is the most valuable thing here — and it is in the wrong file

You predicted 2 failures and observed 3, and reported the deviation instead of retrofitting the prediction. That is the whole point of writing predictions down, and it paid: the third failure found something neither the card nor I knew.

I went and read the collateral test rather than taking the summary, and it is sharper than "a second prop the closed set protects." The failing test is an extender declaring priority 140 does not become the base layer, and its own docblock says what it defends:

"The attack D9.3 names: extender priority is AUTHOR-declared (ext.priority ?? 200), so a package could otherwise re-rank a tenant's overlay out of the base slot by declaring a number below 150."

The setup registers an extender from OTHER_PKG — a different package — whose body is { name, fields: { sneaky } } and carries no _provenance of its own. The assertion expect(merged._provenance).toBe('org') is what says the tenant's layer is still the base. Under your mutation it became 'package'.

So implementing the old docblock literally does not merely enable tenancy overrides. It hands a third-party package the ability to relabel a tenant-authored object as package-authored — succeeding at the exact outcome ADR-0029 D9.3 exists to prevent, by a different route than the priority re-ranking that control was written against.

The ask

That finding currently lives in the PR body, which is read once and then is archived. The docblock is what the next person actually sees, and right now it argues from tenancy / permissions — both hypothetical, and framed as:

"a separate, much bigger decision that has NOT been made"

which reads as a deferred feature. Someone weighing "should I just implement what the old comment promised?" needs the measured consequence, not the deferred one. Add it — two sentences is enough:

  • _provenance is a real top-level prop that extend contributors carry today;
  • copying it through would flip a tenant-authored object's provenance to the extending package's, breaking the ADR-0029 D9.3 base-layer guarantee, and registry-object-overlay-layer.test.ts fails when you try.

Naming the test that catches it matters as much as naming the consequence: it tells the next person the guard exists and where, so they discover this by reading rather than by breaking CI.

The rest stands

Using icon rather than tenancy as the pin fixture was right — a security-neutral, spec-legal prop keeps the pin about the rule instead of about one scary key, and the collateral test now serves as an independent second witness with a security-relevant prop. The named positive control staying green is what shows the ablation cut the intended thing. Restore proven by git hash-object against the HEAD blob plus an empty git diff HEAD, and the 393-test suite re-run green post-restore.

check:pm-half-states (exit 3, no credential) and the --re-measure half of check:type-check-debt correctly recorded as NOT MEASURED rather than green, with the cheap structural half run and passing. The note that this package's tsconfig.json excludes **/*.test.ts, so typecheck does not cover the new test file, is exactly the kind of limit that usually goes unsaid.

Push that docblock addition and I will enqueue on all-green.


Generated by Claude Code

…the mergeObjectDefinitions docblock (#12680)
PM review on PR #12742 (comment 5441664628): the ablation's collateral
failure (registry-object-overlay-layer.test.ts > "an extender declaring
priority 140 does not become the base layer") showed that copying _provenance
through would let a third-party extend contributor flip a tenant-authored
object's provenance to the extending package's -- reaching the exact outcome
ADR-0029 D9.3's priority-reranking guard exists to prevent, by a different
route. That finding lived only in the PR body; this adds it to the docblock
itself, naming both the real prop and the test that catches it, so the next
reader learns the measured consequence rather than only the deferred
tenancy/permissions one.
No behaviour change; no test change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Pushed the docblock addition you asked for (d43a99979), docs-only, nothing else touched.

Added, right after the existing tenancy/permissions paragraph in mergeObjectDefinitions's docblock:

This is not only a hypothetical: _provenance is a real top-level prop an extend contributor carries TODAY. Copying it through (as "later value wins" would) lets a third-party extender flip a tenant-authored object's _provenance to the extending package's — reaching, by a different route, the exact outcome ADR-0029 D9.3's priority-reranking guard exists to prevent. registry-object-overlay-layer.test.ts ("an extender declaring priority 140 does not become the base layer") fails if this merge set is widened to copy it through.

Names the real prop, the measured consequence, and the guarding test, per your ask.

Verification after the addition (lock was free, ran immediately):

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 registry — 27 files, 393 tests, all passed.
  • pnpm --filter @objectstack/objectql typecheck — clean (tsc --noEmit x2).
  • node scripts/check-nul-bytes.mjs — OK, repo-wide.

Nothing here was NOT MEASURED — the lock was free so both commands ran directly, no need to defer.

Not touched: the pin file, the icon fixture, mergeObjectDefinitions's logic (still byte-for-byte identical to origin/main except the docblock), the changeset. Still draft, not armed, no auto-merge.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM — correction to my own review: the new docblock's central sentence is false, and I supplied the premise

This supersedes the "says the discard out loud" endorsement in my previous comment (5441664628). The _provenance ask there still stands. This is a second, larger one.

What the docblock now says

"an extender that ships e.g. tenancy: { enabled: false } or permissions: {...} gets a valid-but-inert no-op: no error, no warning, the base's existing value simply wins as if the extension had never named the key"

That is not true of the authoring path. Measured:

packages/spec/src/data/object.zod.ts:2938
export const ObjectExtensionSchema = lazySchema(() => strictObject({ … }))
packages/spec/src/shared/strict-object.ts:327
return z.object(shape, { error: strictObjectError(options, shape) }).strict();

Declared keys are exactly extend · fields · label · pluralLabel · description · validations · indexes · priority. tenancy greps 0. With .strict(), an author who declares it gets a loud authoring-time failure, not a silent no-op.

The schema's own history field says this was deliberately fixed:

"Until #4001 these were dropped silently — the extension still parsed and still registered, so fields or rules an author meant to merge into someone else's object simply never arrived…"

And content/docs/data-modeling/object-extensions.mdx:89 already documents it correctly:

"the extension schema is strict, so an unknown key fails at authoring time with a prescription rather than being dropped in silence."

So the docs page has been right the whole time. Only the source docblock was wrong — and the correction was about to make it wrong in a new way, on the same feature. That is this defect family's third iteration, and the one place it must not appear is the patch that fixes it.

This is my error, not yours

My dispatch brief on #12680 told you an author shipping objectExtensions: { tenancy: { enabled: false } } "gets a valid-but-inert no-op on a security-relevant key," and told you to say the discard out loud. I took that from the card without checking the layer above the merge. You implemented what I asked, faithfully. The false premise is mine.

What is actually true, and what the docblock should say

Both halves, because only the pair is accurate:

  1. mergeObjectDefinitions itself does silently discard every non-enumerated prop — merged = { ...base } and nothing outside the closed set is copied. Your measurement is right and the pin is right.
  2. Authors do not reach that silence.ObjectExtensionSchema is strict, so a declared objectExtensions entry naming an unknown key fails at authoring time with a prescription (未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001). The silent discard is reachable only by callers that bypass the schema — programmatic registerObject, test doubles, anything passing as any (which is exactly how your own pin reaches it, and worth saying).

That pairing is a better argument for the closed set than the false one was: the merge function is the last line, not the only one, and it holds for callers the schema never sees.

Revised ask — one push, both items

⛔ Still out of scope: implementing "later value wins", any runtime warning, and content/docs/** (that page is already correct — do not edit it).

Verify the strictness claim yourself rather than taking my greps; I have now been wrong once on this card.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM — the red TypeScript Type Check is a superseded run, not a failure. No action needed on it.

Recording this so nobody spends context chasing a type error that does not exist.

What the aggregator actually reported (job 98580808988, run 33089904034):

OS_NEEDS: {
"typecheck-source-gates": { "result": "cancelled" },
"typecheck-workspace": { "result": "cancelled" },
"typecheck-debt": { "result": "cancelled" },
"typecheck-consumers": { "result": "cancelled" }
}
::error::type-check lane `typecheck-consumers` concluded `cancelled` -- expected `success`.
check-typecheck-lanes: FAILED (4 problem(s) above).

All four lanes are cancelled, not failed. No tsc diagnostic appears anywhere in the log — no test body reached a verdict. check-typecheck-lanes requires success and treats any other conclusion as a problem, which is correct behaviour for an aggregator (a lane that silently rides green behind a required context is the thing it exists to catch) but means a cancelled run publishes a red that looks exactly like a real one.

Why it was cancelled: a newer run is already in progress on the same head3e23512e — run 33090154148, all four Type Check lanes started 15:52:59, plus Lint and the Test Core shards. The old run was cancelled because it was replaced, most likely when the base moved (#12733 landed as 34d30118 at 15:52:58, one second before).

Disposition: NOT MEASURED, not red. No re-run is being spent — the replacement run is live and is the reading that counts. The --re-measure half of check:type-check-debt remains separately NOT MEASURED for the reason already recorded in the PR body; that is unrelated to this.

This does not change the two outstanding asks (5441664628 — the _provenance / D9.3 addition; 5441694465 — the ObjectExtensionSchema strictness correction, which is mine). Both still land in one push.


Generated by Claude Code

…lock pass introduced (#12680)
PM review on PR #12742 (comment 5441694465) caught that the docblock's
"no error, no warning" framing for tenancy/permissions was false for the
normal declarative authoring path: ObjectExtensionSchema
(packages/spec/src/data/object.zod.ts) is .strict() -- its shape is exactly
extend/fields/label/pluralLabel/description/validations/indexes/priority, so
an objectExtensions entry naming tenancy, permissions, or any other
undeclared key fails LOUDLY at authoring time with a prescription (#4001),
already documented at content/docs/data-modeling/object-extensions.mdx.
Independently re-verified both citations (packages/spec/src/data/object.zod.ts
and packages/spec/src/shared/strict-object.ts's `.strict()` call) before
writing this -- not taken on the PM's word.
Rewrote the paragraph to say both true things: mergeObjectDefinitions itself
does silently discard every non-enumerated prop (unchanged, still correct),
and declarative authors never reach that silence because the schema rejects
the key first -- the discard is reachable only by a caller that bypasses the
schema (a direct, programmatic registerObject('extend') call, exactly how
this package's own pin exercises the rule). Removed the now-false claim that
"the docblock is the only place a reader can learn the drop is silent" -- the
docs page and the schema's own guidance block already say so.
The _provenance/ADR-0029 D9.3 paragraph from the previous commit is
unaffected and unchanged in substance (only reflowed for line length) --
_provenance is stamped by applyProtection() on every 'extend' registration
regardless of schema validation, so unlike tenancy/permissions it is not
schema-blocked.
No behaviour change; no test change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Verified the strictness claim independently before writing anything (as asked), then pushed the correction: 4e20d452f.

What I verified myself (not taken on your word):

What I additionally traced, since I was re-verifying the whole paragraph anyway: how does _provenance (the collateral finding) actually reach mergeObjectDefinitions's extension argument, given the priority-140 test's extender literal doesn't set _provenance at all? Answer: registerObject calls applyProtection(schema, { packageId }) on every registration — own, overlay, and extend — before storing contributor.definition, and that stamps _provenance: 'package' by default. So _provenance's reachability is structurally different from tenancy's: it isn't reachable "only by bypassing the schema" — it lands on every extend contributor's stored definition unconditionally, through the fully-validated, normal engine.tsregisterObject path too. mergeObjectDefinitions just doesn't currently let it leak into the merged result (correctly). I kept the _provenance paragraph's claims as-is since they were already accurate on this point; I did not add this extra mechanism to the docblock since you didn't ask for it and it isn't needed to support either paragraph's claim — flagging it here in case it's useful context for review.

Docblock now says both true things, per your ask:

  1. mergeObjectDefinitions itself still silently discards every non-enumerated prop (unchanged claim, still correct) — reworded to say the no-op is "HERE"/"from this function", not a blanket authoring-surface claim.
  2. New paragraph: declarative authors never reach that silence because ObjectExtensionSchema is strict and rejects the key first (names the schema, 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001, and the docs page); the discard is reachable only by a caller that bypasses the schema — a direct registry.registerObject(def, pkg, undefined, 'extend', …) call, which is exactly how this package's own pin reaches it.

Removed the sentence "the docblock is the only place a reader can learn that the drop is silent" as asked.

Not touched: content/docs/**, the pin file, the icon fixture, mergeObjectDefinitions's logic (still byte-for-byte identical to origin/main), the changeset, the merge semantics.

Verification (lock was free both times, nothing NOT MEASURED this round):

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 registry — 27 files, 393 tests, all passed.
  • pnpm --filter @objectstack/objectql typecheck — clean.
  • node scripts/check-nul-bytes.mjs — OK, repo-wide.

Still draft, not armed, no auto-merge.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PR body updated (no code change this round). Before writing, independently verified the two new citations on the tree:

  • packages/objectql/src/metadata-facade.ts:182 — confirmed applyProtection stamps _packageId/_provenancein place on every registerObject call, and the surrounding docblock explains why (isTenantAuthored/cloud#970 misclassification risk it guards against).
  • isTenantAuthored in registry.ts:1159 reads _provenance === 'org', and is used at real decision points (e.g. line 1655's re-registration guard: existingOwner && isTenantAuthored(existingOwner.definition) && !isTenantAuthored(schema)), confirming it's load-bearing, not cosmetic.

Body now:

  • Section 2 rewritten to the two-layer statement (function's own discard is real; declarative authors don't reach it because ObjectExtensionSchema rejects first; reachable only via schema-bypass).
  • New paragraph naming the asymmetry: tenancy/permissions get the schema as a first line of defence plus this merge set as a second; _provenance gets only this merge set, since it isn't a declarable/schema-checked key at all.
  • "Verified at head" now says 4e20d452.
  • Ablation section left as-is (already accurate) but gained one clause tying the collateral finding to the same applyProtection mechanism.

Nothing else touched. Still draft, not armed, no auto-merge. Noted re: CI — ignoring the two red TypeScript Type Check runs on the superseded 3e23512e/d43a9997 heads per comment 5441715225; not spending a re-run on them.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 16:35
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 16:35
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 5700d83Aug 27, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12680-merge-object-definitions-docblock branch August 27, 2026 17:03
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] mergeObjectDefinitions docblock promises 'other props: later value wins' but the implementation silently drops every non-enumerated prop

2 participants

@os-zhuang@claude