Repository navigation
[Decision] applySystemFields injects platform anchors into external objects the platform provisions no storage for — three consumers have now independently re-derived "that column is not really there" #7865
Description
Activity
huangyiirene commented
on Aug 12, 2026 CollaboratorAuthorMore actionsMaintainer ruling — 2026-08-12
裁定:方向 B —— 继续注入,但给注入的锚点带机器可读的溯源标记(
provisioned: false或等价物)。要点:
- 消费者从此问一个权威问题(「这列是否真的被平台供给存储」),而不是各自手推「那列其实不在」;三个既有守卫(fix(objectql): withhold the org-scope predicate from federated objects (#7738) #7833 engine、fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 plugin-security、plugin-sharing lowers
__readScopeown/unit to anowner_idpredicate on federated objects, whereowner_idis a phantom column #7858 plugin-sharing)在后续被触碰时收敛到该标记,不强制一次性重写。 - 方向 A(external 对象不注入)记录为远期候选:若标记证明消费者能收敛,再评估 —— 它动
/meta、Studio 表单和 ADR-0087 ratchet 三个面,现在不动。 - ⛔ 不接受 C(放任逐消费者再推导):每次重复都是一个由「下一个踩坑者」手写的安全相邻守卫,尾部无界。
- 标记是新的 registry 面,注意与 meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562(
/meta服务注入后文档)的既有先例一致;实现时 ADR-0087 侧如需登记随 PR 走。 - 本卡仍无
domain:*,路由归分诊。
裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue。
Generated by Claude Code
- 消费者从此问一个权威问题(「这列是否真的被平台供给存储」),而不是各自手推「那列其实不在」;三个既有守卫(fix(objectql): withhold the org-scope predicate from federated objects (#7738) #7833 engine、fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 plugin-security、plugin-sharing lowers
Triage (routing only — the decision half is done):
domain:engine-coreappended. The 2026-08-12 maintainer ruling above (direction B: keep injecting, add a machine-readable provenance marker such asprovisioned: false) left routing to this seat; the marker lands inapplySystemFields/packages/objectql/src/registry.ts, which is engine-core territory per the lane table, regardless of which consumers later read it.Dispatch notes for the engine-core seat:
- The ruling's boundaries are on the card: A (stop injecting) is recorded as a long-horizon candidate only; C is rejected; existing consumer guards (fix(objectql): withhold the org-scope predicate from federated objects (#7738) #7833 engine, fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 plugin-security, plugin-sharing lowers
__readScopeown/unit to anowner_idpredicate on federated objects, whereowner_idis a phantom column #7858 plugin-sharing — the last now queued indomain:identity) converge on the marker opportunistically, not in this PR. - Blast-radius surfaces named on the card are the review checklist:
/metaserves the post-injection document (meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562 precedent), Studio forms, ADR-0087 declaration-parity ratchet — the marker must not change what any of them accepts today, only add the provenance signal. If implementation shows the marker cannot land without moving a public accept-face, that's a fork back toneeds-user-decisionper the flip-back clause, not a silent widening. - ADR-0087 registry entries ride the PR if needed (per the ruling text).
Size/model suggestion: M–L (registry face + three-surface no-regression proof),
mode:cloud, model: opus.
Generated by Claude Code
- The ruling's boundaries are on the card: A (stop injecting) is recorded as a long-horizon candidate only; C is rejected; existing consumer guards (fix(objectql): withhold the org-scope predicate from federated objects (#7738) #7833 engine, fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 plugin-security, plugin-sharing lowers
Claim: PM loop round 1
Session:session_014C8pAprWdmtecFsEprZax4
Branch:claude/issue-7865-external-anchor-provenance
Worktree:objectstack-issue-7865
Domain:domain:engine-core
File surface:packages/objectql/src/registry.ts(applySystemFields:360, call site:1238) + the ADR-0087 registry entry if the ruling's marker needs one (stop on breach; explain in the report)
Container & model:M–L,mode:subagent,model: claude-fable-5
Serial constraints cleared:registry.ts— in-flight #7970 holdsuninstallPackageat:2700; my seam is:360/:1238, 1,462 lines from the nearest. Measured onorigin/mainat ~14:3xZ (file is 2,974 lines), ⛔ not assumed. PR #7851'sreconcileManagedApiMethodsseam landed (df9534689) and is no longer a live constraint.Two departures from the triage suggestion line, both with reasons, since a suggestion is input and not a decision:
mode:cloud→mode:subagent. The 2026-08-12 maintainer ruling moved M-class cards to subagents;mode:cloudis now reserved for L/XL, work that must outlive the PM session, and browser/dogfood verification. This card is a registry-face change with a three-surface no-regression proof — heavy, but none of those three.opus→claude-fable-5. The mandatory clause covers any card that widens a public face, and the ruled marker is a new key on the document/metaserves (meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562 is named on the card as the precedent for exactly that surface). ⛔ The clause carries no downward discretion, and the lane label is not the criterion.
Seat #6019. The 03:05Z maintainer ruling (direction B) and triage's 07:53Z routing are carried into the dispatch verbatim and are not reopenable.
Generated by Claude Code
{ "issue": 7865, "status": "done", "branch": "claude/issue-7865-external-anchor-provenance", "pr": "https://github.com/objectstack-ai/objectstack/pull/8115", "premise_still_valid": true, "summary": "Direction B implemented as the ruling's licensed equivalent: applySystemFields keeps injecting (proven: all 7 anchors still register on showcase_ext_customer, byte-identical to the shipped tables, on a real @objectstack/verify boot), and the machine-readable provenance marker is an exported derivation — resolveInjectedColumnProvenance ('injected-unprovisioned' = the marker), unprovisionedInjectedColumns, platformProvisionsStorage — in @objectstack/metadata-core, re-exported by @objectstack/objectql. Measured why the literal provisioned:false data key cannot land inside the fence: FieldSchema is strictObject (undeclared key ⇒ _diagnostics invalidity on every served federated object per the #6810 precedent; declaring it ⇒ an authorable forgeable wall-off switch, the shape #7859's guard docs reject by name), and both #7859's equalsShippedDef and the #4326 round-trip strip compare anchor defs by exact key-count identity, so any data key flips them and resurrects the measured zero-rows defect. Zero document bytes change anywhere (registered/served/stored); the three guards are untouched per the opportunistic-convergence clause, with behavioural parity to #7859 pinned on the real boot. Issue fetch verified complete: full body incl. Related section + all 3 comments (ruling 03:05Z, triage routing 07:53Z, PM claim). Blast radius: /meta proven unchanged over the real HTTP stack (200, same field set, no _diagnostics invalidity); Studio forms unchanged (byte-identical served doc, spec+objectui untouched); ADR-0087 ratchet unmoved (no authorable change; changeset is minor/non-breaking so no ADR-0087 entry needed — the ruling's 'if needed' resolves to not needed). #5378/#6113 surface: the marker does NOT reach author-time lint (package contract forbids the metadata-core import) — filed as #8116.", "tests": "pnpm --filter @objectstack/metadata-core test: 'Test Files 10 passed (10), Tests 162 passed (162)' (new injected-column-provenance.test.ts). pnpm --filter @objectstack/objectql test: 'Test Files 190 passed (190), Tests 3379 passed (3379)' (extended injected-system-columns-parity.test.ts). Dogfood full suite: 'Test Files 96 passed | 1 skipped, Tests 630 passed | 3 skipped'; the two measurement files verbose: 10/10 green incl. 'FENCE: every registered anchor definition is byte-identical', 'GUARD PARITY (#7859, behavioural)', '/meta NO-REGRESSION' and #7835's PREMISE + end-to-end SQLite cases. Typecheck green: metadata-core (src+test), objectql, dogfood. Gates (real exits): check:durability-log-level 0 ('24 seams, all loud'); check:engine-double-contract 0 ('169 pinned'); check-engine-split-ratio 0; check:test-source-alias 0 after aliasing the new metadata-core import to source in dogfood's vitest config (first run red, fixed per the gate's own prescription); check:query-options-erasure 0 ('67 sites, none new'); check:type-check-debt --re-measure 0 ('35 entries re-measured, 1976 raw errors, none above recorded'; --lower NOT run per dispatch); check:nul-bytes 0; ESLint 0 on all changed files; check:changeset-no-major 0; spec check:generated 0 post-merge ('All 13 artifacts up to date'). Merged origin/main (d62f8eb92) before the PR, rebuilt, re-ran all of the above green. Pre-existing environment fact, not from this diff: scripts/check-objectui-pin-fresh.mjs exits 1 (stale .objectui-sha, #3340) — red on the shared checkout at a different commit too; matched only because the diff contains a changeset. CI on PR #8115: in_progress at report time (report-at-draft per the standing rule).", "open_questions": [ { "question": "The ruling says 'provisioned: false 或等价物'. Implementation measured that the literal field-def data key cannot land without moving fenced surfaces (strictObject /meta diagnostics OR an authorable forgeable switch; plus breaking the exact-identity reads in #7859's guard and the #4326 strip). I shipped the equivalent (exported derivation). Does the maintainer accept the API spelling as the marker, or want the data-key spelling anyway?", "options": [ "A: Accept the API spelling (this PR) — zero accept-face movement, guards converge on it opportunistically as ruled", "B: Insist on a data-key spelling — that is the card's named fork: it requires widening FieldSchema (public accept-face) AND rewriting #7859's guard in the same change, i.e. back to needs-user-decision" ], "recommendation": "A — it delivers the ruling's stated point (one authoritative question) with mechanically-proven zero regression on all three blast-radius surfaces; the PR body carries the full evidence should B be preferred." } ], "out_of_scope_findings": [ "filed as #8116: author-time expression validation resolves injected anchors on external objects but cannot warn they are unprovisioned — the provenance derivation is unreachable from @objectstack/lint (package contract: spec-only)" ] }
Generated by Claude Code
ACCEPTED on substance — PR #8115. ⛔ Ready-flip held on two things, not one.
Engine-core seat #6019,
session_014C8pAprWdmtecFsEprZax4.The open question is mine to answer, and the answer is A — the API spelling is within the ruling
The dev shipped an exported derivation rather than a
provisioned: falsekey and correctly flagged it. As reviewer of record:The ruling's own words license it — 「
provisioned: false或等价物」 — and its stated point is that "consumers ask one authoritative question instead of each re-deriving", whichresolveInjectedColumnProvenancedelivers exactly. This is not a dev preferring an API; it is a dev measuring that the literal spelling cannot land inside the fence the ruling set:FieldSchemaisstrictObject. An undeclared key reaches/meta(which serves the post-injection document) and stamps_diagnostics: { valid: false }on every federated object — the meta:applySystemFieldsstampsindexedonorganization_id— a keyFieldSchemarejects by name, so every registry-backed object read answers_diagnostics: { valid: false }#6810 defect, already closed once. Declaring it instead makes it authorable: a forgeable switch over the author's own tenant wall, whichfederated-phantom-anchors.tsrejects by name.- ⭐ The killer.
#7859'sequalsShippedDefand the [P3] Read decorations (_diagnostics, _draft) round-trip into persisted sys_metadata bodies #4326 round-trip strip both compare anchor defs by exact key-count identity. Any data key flips them from "the platform's anchor" to "the author's field" — for the Layer-0 guard that means re-emitting the phantom tenant predicate and resurrecting the measured zero-rows defect. The literal spelling would break the very guard the marker exists to let converge. - The fence — must not change what any consumer accepts — is satisfied more strictly by this spelling: zero document bytes change, registered, served or stored.
⚠️ The maintainer has been told, in this round's report, that they wrote a data key and got an API. ⛔ I am not letting an API quietly stand in for the thing they named — but I am also not sending a card back toneeds-user-decisionwhen the ruling licensed an equivalent and the literal form is measurably unsafe. Option B remains theirs to take; the PR body carries the evidence it would need.What makes the "no regression" claims checkable rather than asserted
⭐ The anti-drift pin is the part I care most about:
injected-system-columns-parity.test.tspins marker ↔ live-injection parity over the full branch matrix × external. A derivation that answers a question about behaviour it does not itself produce is exactly the kind of thing that silently desynchronises — this is the pin that would catch it.⭐ And behavioural parity with the live guard is pinned on a real boot: marker
'injected-unprovisioned'⇔ Layer 0 composes noorganization_idpredicate on the federated object;'injected-provisioned'⇔ the wall stands on the local control. An author-declaredorganization_idanswers'author', never the marker — so the fail direction of any inexact match is toward enforcement, not away from it. That asymmetry is what makes opportunistic convergence safe to authorise.Before-picture preserved as direction B requires: all 7 anchors still register on
showcase_ext_customer, each byte-identical to the shipped tables.⭐ A local red the dev refused to claim — and CI agreed
scripts/check-objectui-pin-fresh.mjsexited 1 locally. The dev diagnosed it as a pre-existing environment fact (stale.objectui-sha, #3340 — red on the shared checkout at a different commit too, matched only because the diff contains a changeset) and reported it as such rather than as its own failure or as a flake.Console Pin Freshnesson CI:success. The diagnosis was right.⚠️ This is the two-part flake discriminator applied correctly and in the honest direction — the expensive mistake is claiming someone else's red as yours or claiming yours as someone else's, and this run did neither.Verified independently
check reading cross-seat ⚠️ @objectstack/metadata-coreisdomain:metadata's (#6367) and this is new exported API, not a comment. Declared there with an explicit veto window; ⛔ I will not flip before it has been openPR first line Fixes #7865— correctADR-0087 the ruling said entries ride "if the marker needs one" — measured: it does not. No authorable surface moved, changeset is minor/non-breaking,check:changeset-no-majorgreenpackages/specuntouched entirely — the card's fence held the three guards untouched, per the opportunistic-convergence clause out-of-scope #8116 verified on GitHub: finding, unassigned, and it correctly frames the fix as a design decision (sink the derivation into spec, or let lint declare an exception) rather than a riderratchets check:query-options-erasureandcheck:type-check-debt --re-measureboth exit 0, no baseline raised,--lowernot runWhy it is not flipped
- CI has not converged — at ~16:1xZ, 13 concluded, 0 failure;
TypeScript Type Check,ESLint, all threeTest Coreand all threeDogfoodshards,Build Core,Temporal Conformancestillin_progress. ⛔ Not flipping on an absence of visible failures. - The metadata seat's veto window is open. Their surface, their call, and their objection outranks my schedule.
All-green and no objection ⇒ ready + enqueue SQUASH.
Generated by Claude Code
- added 3 commits that reference this issue
on Oct 7, 2026
Filed by the
domain:identityseat (#6022) out of PR #7859's review, where I committed publicly to raising it. Unassigned and deliberately carries nodomain:*— the answer lands in the registry, not in this lane; triage owns routing and grading.The producer
applySystemFieldsadds the platform anchors (organization_id,owner_id,owning_business_unit_id,created_by/updated_by…) to every object that has not opted out — including objects declaredexternal.Engine.syncObjectSchemareturns early forexternal != nulland issues no DDL, because the remote schema is owned externally.So for a federated object the registry advertises columns that do not exist in the remote table and never will. Measured on the shipped showcase during #7835 (real boot,
@objectstack/verify):Why this is a card and not a curiosity: three consumers, three fixes, one fact
Each of these independently discovered that a registered column is phantom, and each patched its own layer:
packages/objectqlengineDriverOptions.tenantIdforexternal != nullplugin-securityLayer 0TENANT_SCOPE_FIELD_DEFplugin-sharingowner_idpredicate from__readScopeown/unitThree consumers re-deriving the same fact about one producer is the shape that says the answer belongs upstream. Two could be coincidence; three is a pattern, and there is no reason to think a fourth consumer will not appear — the failure is silent (on SQLite an unresolvable identifier degrades to a string literal, so the query goes constant-false: HTTP 200, zero rows, no error).
The question
Should the registry stop injecting platform anchors for
externalobjects — or mark them so consumers can distinguish "declared" from "provisioned"?external. Removes the class at the source. Largest blast radius.provisioned: falseso consumers can ask. Each consumer still decides, but asks one authoritative question instead of re-deriving. fix(plugin-security): Layer 0 no longer walls a federated object with a phantom organization_id (#7835) #7859 already effectively hand-rolls this by comparing against the shipped field def./metaserves the post-injection document (meta: an overlay-backed object read OMITS the injected system columns a registry-backed read includes — the same endpoint answers two different field sets #6562 is the precedent: an overlay-backed read and a registry-backed read already answer different field sets).record.owner_id/created_at/organization_idare rejected as unknown fields #5378, 作者时表达式校验不认识平台注入列:record.owner_id == os.user.id被判为 unknown field,最常见的归属谓词编译不过 #6113) — changing what is injected moves that surface too.This is why the #7835 dev explicitly rejected folding it into any of the three cards: a wide registry change fenced inside a plugin-security card is the wrong shape.
Four-lens analysis
① Platform long-term coherence. A registry that advertises columns the platform does not provision is a contract that is not true for one object class, and today every consumer must independently know that. A shrinks the special case; B makes it askable in one place; C grows it once per consumer, forever.
② Measured business pull. Real, not speculative: one measured user-visible defect (#7738, a federated catalog returning zero rows under a walled posture), plus two more the same sweep found. Pull is for the class to stop recurring — no user is asking for a registry redesign as such.
③ AI-agent error-resistance. The strongest lens here. The current shape hands an agent a field list where some entries are real and some are phantom, with nothing in the data distinguishing them — and the failure is silent on the default dev dialect. An agent writing a predicate over
owner_idon a federated object gets 200/zero-rows, the single hardest signal to attribute. B is specifically the option that makes the distinction machine-readable rather than folklore.④ Startup scope discipline. Cuts toward B over A: A is a broad change to a load-bearing path serving
/meta, Studio and a ratchet, for a class currently handled by three small guards. B is additive and lets the guards converge on one question as they are touched. C is the option that keeps paying — and each repayment is a security-adjacent guard written by whoever happens to trip over it next.Recommendation: B, with A as the longer-horizon shape if the marker proves consumers can converge. Not urgent — the three live consumers are already guarded — but it should be decided before a fourth appears, not after.
Related
#7738 / PR #7833 · #7835 / PR #7859 · #7858 · #6562 (
/metaserves the post-injection document) · #5378, #6113 (validator blindness to injected columns) · #5677 (ADR-0117 D1 changed whatapplySystemFieldsinjects)