Uh oh!
There was an error while loading. Please reload this page.
fix(lint): three write rules ask anchor provenance before exempting a system column - #8996
Conversation
… system column (#8663) `IMPLICIT_FIELDS` is object-independent, so on an ADR-0015 `external` object it exempted injected anchors the platform never provisioned storage for. Measured end to end: the engine's write-path validator PASSES the anchor (it is in the registered schema) while refusing an undeclared name outright with INVALID_FIELD, so the anchor is the only payload key that reaches the remote database raw — where SQLite answers an untyped `no such column` that aborts the whole statement. Each of the three consumers now emits a separate advisory finding on that path, at `warning`, reusing the read-axis anchor wording. Author-declared columns of the same name stay untouched.
…he hook-bodies page (#8663)
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…s it (#8663) The added line linked ADR-0015 to /docs/protocol/federation, which does not exist — Check Documentation Links caught it. The ADR id now reads as bare text (matching all eight existing ADR-0015 citations in content/docs) and the link moves onto the concept, targeting the page that actually owns federation.
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8663
This was a measure-first card: the filing asserted a structural resemblance to the read-axis gap and explicitly declined to guess the runtime behaviour ("I did not measure either"). The triage fence made the measurement deliverable #1 and said plainly that a no-gap result was the correct delivery. So the measurement came first, and it is reported here in full — including the part that does not match the card's framing.
Deliverable #1 — the measurement
Question. On an ADR-0015
externalobject with an injected (unprovisioned) anchor, does a hook/action/flow body write to that anchor degrade silently, or does the external write path already refuse it downstream?Answer: neither. Measured end to end, on a write-enabled federated object (
external.allowWrites+external.writable) bound to a real remote SQLite table that has noowner_idcolumn.assertWriteAllowed)INVALID_FIELD, on insert and on update; never reaches a driverSQLITE_ERROR: table fact_orders has no column named owner_id— whole statement abortedRaw evidence:
Three things follow, and only the first was in the card:
applySystemFieldsinjects the anchors onto a federated object byte-identically to a local one (A1), so the name is a registered field and every membership test says yes.searchableFieldsis a FIFTH blanket-SYSTEM_FIELDS read site — a declared anchor on an external object becomes a$searchFieldsnarrowing over a column with no storage #8404 constant-false silent degradation. It is the L2 hook body 写集不透明:把「写了目标对象上不存在的字段」从 accepted gap 变成作者时 lint 告警 #4271 driver split — loud on a SQL remote, silent on a schemaless one — which is exactly the harm this rule family already exists to move to author time.INVALID_FIELD), on both verbs, and never reaches a driver at all. The injected anchor is the only payload key that gets past it. So the ugly driver-level failure the rule family was built to prevent is, on this path, reachable only through the blind spot — not "one more case of" it.Note also that the rule's own header states
validateRecord"continues past a key with no field def on update". Measured, update refuses withINVALID_FIELDtoo (A4b). Recorded, not acted on.What changed
IMPLICIT_FIELDS(validate-hook-body-writes.ts:265) is object-independent: it answers "could this name be implicitly writable somewhere", never "did the platform provision a column for it on this object". Three rules consume it —validate-hook-body-writes,validate-action-body-writesandvalidate-flow-node-writes(the third one found during triage, so one set covers three surfaces). All three now ask the second question before going silent, viaindexUnprovisionedAnchors— the per-stack helper #8340 built for exactly this and which no rule in this family had used yet.Each surface gets a new, separately suppressible rule id at
warning, matching #8340'sflow-template-field-unprovisionedprecedent exactly rather than reclassifying the existing finding:hook-body-write-unprovisioned-anchoraction-body-write-unprovisioned-anchorflow-node-write-unprovisioned-anchorThe cause and fix wording is the shared
unprovisionedAnchorCause/unprovisionedAnchorHint; the consequence clause is new, shared across the three write surfaces, and every clause in it is a measured fact from the table above rather than an inference.The flow-node rule advises, it does not gate. Its existence finding stays
error— a literal key against a literal object is a certainty. The provenance finding is a claim about a remote schema the build cannot see, so promoting it toerrorwould convert a currently-silent case straight into a build break, which is the ADR-0072 D1 false positive at the one severity that cannot be ignored.FlowNodeWriteSeveritywidens from'error'to'error' | 'warning'accordingly — the same wideningvalidateFlowTemplatePathsalready carries, and the reference-integrity suite is severity-agnostic by contract.An author-declared column of the same name is never flagged — on a federated object it maps a remote column the author vouches for (#7859's direction), so its provenance is
authorand both findings stay silent.Candidate 2 —
validate-translation-references.ts— measured, NO GAP, untouchedFenced out of scope by the triage, and measured anyway so the disposition rests on evidence rather than on the filing's reasoning. The question is whether a bundle naming an injected anchor in a translation key is drift. It is not:
applyInjectedSystemColumnsadds the anchors to the served/metadocument, ungated by provenance, so the field is served and rendered.The anchor is served with the platform's own
label: "Owner". A bundle translating it is labelling a field the user actually sees, always empty. Warning there would be the ADR-0072 D1 false positive. That set is unchanged in this PR.Verification
Union run at
3ff8c4a95, which is HEAD of this branch.pnpm --filter @objectstack/lint test— 73 files, 2061 tests passed (14 new)pnpm --filter @objectstack/lint typecheck— cleanscripts/pm/dispatch-gates.mjs, all green:check:nul-bytes,check:cross-package-test-inputs(both the lint.yml and ci.yml spellings),check:changeset-gate-self-tests,check:objectui-changeset,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset, plus the test-file convention family —check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage,check:type-check-debt. Re-derived after the docs edit and addedcheck:docs-audit-scopeandcheck:role-word, both green.check:type-check-debtinitially refused (unbuilt closure — a refusal is NOT MEASURED, not "not applicable"), so the full workspace closure was built and it was re-run:33 ledger entries re-measured, none above its recorded number. It reports@objectstack/lintat 19 against a recorded 20, i.e. one below the ceiling. The ledger is deliberately left alone: lowering it would bake a number measured on this branch, and the surplus it names is already tracked in [finding][devx]check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376.3ff8c4a95after the docs-link fix below, not carried over from the earlier head.Docs-link fix (round 2)
Check Documentation Linkswent red on the first head: the added line linked ADR-0015 to/docs/protocol/federation, which does not exist. Fixed by making the ADR id bare text — matching all eight existing ADR-0015 citations incontent/docs— and moving the link onto the concept, pointing at the page that actually owns federation:One correction to the triage of that failure, since it changes what the convention actually is: linked ADR spellings do exist in
content/docs—grep -roh '\[ADR-[0-9]*\]([^)]*)' content/docs/returns ADR-0020, ADR-0039, ADR-0046, ADR-0063, ADR-0064, ADR-0101, ADR-0109 and others. They uniformly target the GitHub ADR source file (https://github.com/.../docs/adr/00NN-*.md), never a docs route. So the corpus has two conventions, not one: bare text for ADR-0015 specifically (8 sites), and GitHub-source links for linked ADRs generally. The spelling above keeps the first and does not introduce a new target class.lycheeis not installed locally, so the link was verified by replicating the gate's own resolution rule (--root-dir content/,--fallback-extensions mdx,md) over every root-relative link on the page — all five resolve.check:doc-anchorswas also re-run green, but it is worth recording that it is the fragment gate and could not have caught this: a page-path link is a different check.Ablation
Behavioural, not import-level, so the red is precise: the four new short-circuits were replaced by the original blanket
continuewhile every export stayed in place.Stated honestly, because this lane has been burned by tests that pass with and without the thing they pin: 4 of the 14 new tests are ablation-proven. The other 10 assert silence — the local twin, the author-declared column, the multi-target per-object branch, the non-anchor rule-local exemptions, and the unchanged existence findings — and by construction they pass in both directions. They are false-positive and regression guards, not evidence for the change. Restored with
git checkout HEAD -- ...from the commit (committed before the ablation, so a restore point existed);git status --porcelainclean afterwards, no staged/unstaged split.Out of scope, deliberately
IMPLICIT_FIELDSset itself, andSYSTEM_FIELDS, are unwidened — the union's origin (lint: five hand-copiedSYSTEM_FIELDSlists in packages/lint should derive from the spec's declarations #4330) is untouched. The fix pairs the membership test with a provenance test; it does not move a name between sets.Generated by Claude Code