You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`translation-target-unknown` no longer reports the locale keys a package ships for what it CONTRIBUTES into metadata another package owns — `objectExtensions[]`-injected fields and validation rules, and the navigation items it contributes into an app it does not declare (#18441, #18442).
6
+
7
+
Both were `error`, so each one FAILED the run it appeared in, and both carried the orphan remedy — *"Point the key at a declared field, or drop it"*, *"Match the key to an app's `name`, or drop it"* — which deletes a translation the runtime resolves. Measured on the two probe stacks:
8
+
9
+
-`objects: [crm_lead { name }]` + `objectExtensions: [{ extend: 'crm_lead', fields: { sla_tier } }]` + a `zh-CN` key for `sla_tier` produced one `error` at `translations[0]["zh-CN"].objects.crm_lead.fields.sla_tier`. A genuinely undeclared field on the same stack produced a finding identical but for the name, so **a correct author and a real typo were indistinguishable in the output** — an author who extended an object correctly was told their correct key was wrong, in a run that failed.
10
+
- in `os build`'s per-package leg, a contributor package carrying `navigationContributions` and no apps of its own was told app `crm_enterprise` is one *"which this stack does not define"* — whether or not the app's owner was an entry of the same artifact. Declaring that app is the owning package's job; the contributor cannot do it.
11
+
12
+
Both folds widen what a key may RESOLVE against and nothing else, so every genuine orphan still reports at `error` with the rule id intact: a typo on an extended object, a `_validations` name no layer declares, an object neither defined nor extended, an app neither defined nor contributed into, and a contributed navigation id nothing contributes are each pinned as a control beside the case they neighbour.
13
+
14
+
Two bounds worth reading before widening either fold further:
15
+
16
+
-**The extension fold is exactly two rungs wide because `ObjectExtensionSchema` is.** The declared entry keys are `extend`, `priority`, `fields`, `validations`, `indexes`, `label`, `pluralLabel` and `description`; `views`, `listViews`, `actions`, `fieldGroups`, `sections`, `tabs` and `hooks` are refused BY NAME at the extension level with authoring guidance. So `fields.*` and `_validations.*` are the only rungs of this rule an extension can reach, and a `_views` / `_sections` / `_tabs` / `_actions` key on an extended object is an orphan exactly as before. A new pin asserts that surface against the schema, so the sizing cannot silently stop being true.
17
+
-**An extension target this stack does not DEFINE is rung 2b of the cross-package ladder**: the object key resolves — the extension is proof the stack means that name — and the subtree is skipped WHOLLY, for the reason rung 2 skips a registered platform object's. The owner's field set is not visible from a package that only extends it, and judging the subtree against the injected names alone would report the owner's own field keys as orphans, which is the same defect one level up.
18
+
19
+
No schema moved, no export moved, and no accept set moved: this is a lint rule's false-positive set narrowing. `Clause-②: no`
The remaining raw `FieldSchema.reference` readers now **REFUSE** a carrier they cannot read, instead of answering "no target" (#18550). The previous release routed the arbiter (`referenceCarrierOf`) and the lint target readers; these were the measured residue of the same ruling — every reader, not just the arbiter.
10
+
11
+
`FieldSchema.reference` is `z.string().optional()`, so `ObjectSchema.safeParse` refuses an object- or array-valued carrier at the contract door. These reads are the other door: the one a value reaches only when it never went through parse — a hand-built fixture, a raw `registerObject`, a stored row rehydrated past its schema.
12
+
13
+
**`@objectstack/objectql`** — both of the delete cascade's carrier reads (`planCascadeAtomicity` and `cascadeDeleteRelations`). This is the one with a measurable runtime consequence, and it is why the level is not `patch`:
14
+
15
+
```
16
+
before acct=1 task=1
17
+
delete RESOLVED true <- success reported to the caller
18
+
after acct=0 task=1 <- an ORPHANED master_detail row
19
+
```
20
+
21
+
An unreadable carrier made the relation invisible to the cascade, so the parent was deleted, the detail row stayed, and the caller was told the delete succeeded — no `restrict` refusal, no `set_null`, nothing logged. It now refuses before any row is touched.
22
+
23
+
**`@objectstack/rest`** — the public-form lookup picker's field-def fallback. The field def is also hoisted out of the metadata fetch's `catch {}`, so an unreadable carrier is no longer reported as `LOOKUP_TARGET_MISSING`: "no target is declared" and "the declared target cannot be read" want different fixes from whoever owns the metadata.
24
+
25
+
**`@objectstack/metadata-protocol`** — the seed dependency graph, which also retires an `as string` cast that asserted exactly what its truthiness guard had not checked.
26
+
27
+
**`@objectstack/lint`** — the four remaining target readers: `masterDetailCount` (`validate-expressions`), the `displayField` consumer edge (`validate-field-consumers`), the field and action-param targets (`validate-object-references`), and `masterOf` (`validate-sharing-rule-enforceability`).
28
+
29
+
**`@objectstack/verify`** — `relationTarget`, which no longer degrades an unreadable carrier to the generic "has no `reference` target" an object with no relationship metadata at all receives.
30
+
31
+
`null`, `undefined` and `''` are ABSENCE, not a wrong shape, and still answer `undefined` at every one of these sites — a field is allowed to name no target, and `StrictField` declares `reference` nullable. Each site's absence answer is pinned alongside its refusal.
32
+
33
+
Upgrading: nothing conformant changes. A non-string `reference` could not be authored, stored or parsed before this release either; what changes is that one now fails loudly at the read instead of being read as an absent target. If a test asserted the old silence, assert the refusal instead.
@@ -106,7 +121,7 @@ are reported as `undecidable` rather than assumed either way.
106
121
107
122
The same holds twice over for the context. An options argument spelled as a
108
123
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
109
-
forwarding shim cannot, and **67 of the 225 sites are spelled that way**. A
124
+
forwarding shim cannot, and **67 of the 227 sites are spelled that way**. A
110
125
context resolved from an inline literal or a local `const` can be tested for
111
126
`isSystem`; one arriving from a helper call cannot.
112
127
@@ -140,12 +155,28 @@ direction, on the very figure this page tells other cards to cite. `carries` is
140
155
now three-valued, and an unreadable argument can never contribute to the
141
156
provable count.
142
157
143
-
⭐ Both are the same shape as the failure this artefact exists for, wearing
144
-
opposite hats: one scored an unread thing as *nothing to report*, the other
145
-
scored an unread thing as *a finding*. `node
146
-
scripts/check-tenant-audit-census.mjs --self-test` pins all of it — 18 classifier
147
-
cases, 6 of which red against the old reading, driven from the gate's own
148
-
self-test rather than from a flag on the generator.
158
+
**A door rule keyed on names read an unnamed door as no door.** Two receivers
159
+
typed with an inline type literal that spells `insert` / `update` with an
160
+
`object: string` first parameter were subtracted from the population — by the
161
+
same instrument that printed their type text on every run and called the
162
+
subtraction probably wrong. Placing them moved the population **225 → 227** and
163
+
the elevated count **106 → 108**. ⭐ It moved the provable yield surface
164
+
(`9`) **not at all**: both sites thread an elevated context, and one of them
165
+
targets a tenancy-opt-out object. The repair is to the denominator and to what
166
+
this page can be read as having examined — ⛔ not to the count anything cites as
167
+
a violation. It is pinned in **both** directions, because a rule that placed
168
+
every inline literal would be the same failure mirrored: an unnamed type that
169
+
states a write door places its site, one that states none is still a subtraction
170
+
that says why, and an indexed type name beside an inline literal still wins and
171
+
reports itself.
172
+
173
+
⭐ All three are the same shape as the failure this artefact exists for, wearing
174
+
different hats: one scored an unread thing as *nothing to report*, one scored an
175
+
unread thing as *a finding*, and one scored a thing it had read and printed as
176
+
outside its own population. `node
177
+
scripts/check-tenant-audit-census.mjs --self-test` pins the first two — 18
178
+
classifier cases, 6 of which red against the old reading — driven from the
179
+
gate's own self-test rather than from a flag on the generator.
149
180
150
181
## ⭐ Deviations from the carried figures, untrimmed
151
182
@@ -155,10 +186,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
155
186
156
187
| carried figure | where it survives | this census |
157
188
| :--- | :--- | ---: |
158
-
| 175 write call sites | quoted in the merged changeset |**225**|
189
+
| 175 write call sites | quoted in the merged changeset |**227**|
159
190
| 24 carrying no tenant context | quoted in the merged changeset |**9** provable and tenancy-enabled; **32** more whose options argument is unreadable |
160
-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**149 of 225** decidable, **76** undecidable |
161
-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 106 decidably elevated, 0 decidably not, 102 undecidable |
191
+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card |**151 of 227** decidable, **76** undecidable |
192
+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration**|**not reproduced**: 108 decidably elevated, 0 decidably not, 102 undecidable |
162
193
| 141 and 132, two independent re-derivations | the card that filed this work | — |
163
194
164
195
**The differences are not reconciled, and deliberately so.** The old census's
@@ -169,17 +200,17 @@ at any commit.
169
200
170
201
Two structural facts do plausibly widen this reading against any hand or regex
171
202
one, and both are counted in the generated tables below: the 44 sites reached
172
-
through an erased (`any`) receiver, and the 40 that name their object through a
203
+
through an erased (`any`) receiver, and the 41 that name their object through a
173
204
`const` rather than inline. An instrument that read either the way a person does
174
205
would report a smaller number and would not say so.
175
206
176
207
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
177
208
figure has no surviving corroboration anywhere in the tree.** This census reads
178
-
106 of 225 (47%) as decidably elevated, with 102 more whose elevation is a
209
+
108 of 227 (48%) as decidably elevated, with 102 more whose elevation is a
179
210
run-time fact — so the claim is neither confirmed nor refuted, and the honest
180
211
answer is that a static reading cannot settle it.
181
212
182
-
⇒ **Cite `9 / 225`, and say what it is**: the sites whose options argument was
213
+
⇒ **Cite `9 / 227`, and say what it is**: the sites whose options argument was
183
214
READ and holds no tenant context, against a decidably tenancy-enabled object.
184
215
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
185
216
without tenant context" — **32 further sites** have an options argument this
@@ -191,29 +222,29 @@ cannot read, and they are neither in nor out.
191
222
192
223
| what | count |
193
224
| :--- | ---: |
194
-
| write call sites on the application surface |**225**|
195
-
| …whose object name is statically decidable |149|
225
+
| write call sites on the application surface |**227**|
226
+
| …whose object name is statically decidable |151|
196
227
| …whose object name is chosen at run time | 76 |
197
-
| …against an object with tenancy ENABLED |149|
198
-
| …against an object that declares tenancy off |0|
199
-
| threading a tenant context |141|
228
+
| …against an object with tenancy ENABLED |150|
229
+
| …against an object that declares tenancy off |1|
230
+
| threading a tenant context |143|
200
231
| PROVABLY carrying none (options read, no context key) |**17**|
201
232
| …of those, against a decidably tenancy-enabled object |**9**|
202
233
| options argument UNREADABLE — may or may not carry one | 67 |
203
234
| …of those, against a decidably tenancy-enabled object | 32 |
204
-
| threading a decidably ELEVATED (`isSystem`) context |106|
235
+
| threading a decidably ELEVATED (`isSystem`) context |108|
205
236
| threading a context that is decidably NOT elevated | 0 |
206
237
| threading a context whose elevation is a run-time fact | 102 |
207
238
208
239
| how the instrument reached the site | count |
209
240
| :--- | ---: |
210
-
| receiver carried a readable engine type |181|
241
+
| receiver carried a readable engine type |183|
211
242
| receiver erased, placed by the object NAME | 18 |
212
243
| receiver erased, placed by an `object: string` PARAMETER | 15 |
213
244
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 11 |
214
245
215
-
| object name spelled inline |109|
216
-
| object name spelled through a `const`|40|
246
+
| object name spelled inline |110|
247
+
| object name spelled through a `const`|41|
217
248
| object name is an `object: string` parameter | 19 |
218
249
| object name is some other run-time expression | 57 |
219
250
@@ -232,8 +263,14 @@ must not be spelled the same way as «read it, not an engine».
232
263
233
264
| what | count |
234
265
| :--- | ---: |
235
-
| write calls subtracted with no defensible reason |**3**|
236
-
| …whose declared type text states an engine door anyway |**2**|
266
+
| write calls subtracted with no defensible reason |**1**|
267
+
| …whose declared type text states an engine door anyway |**0**|
268
+
269
+
⛔ The second row is **0 by construction**, not a tally that happens to be low.
270
+
An inline type literal stating a write door has no name for the engine type index
271
+
to be keyed on, so the door rule is read off the type text itself and the site is
272
+
PLACED — it is in the population above rather than subtracted here. A non-zero
273
+
value on that row means a door-shaped receiver reached the subtraction anyway.
237
274
238
275
Every one of them is listed, by receiver and by the type text that could not be
239
276
placed, in [`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`](https://github.com/objectstack-ai/objectstack/blob/main/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md).
@@ -253,13 +290,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
253
290
their values are not compared. The reasoning, and the measurement behind it,
0 commit comments