Skip to content

Commit 5234daa

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-18605-enable-on-install-one-authority
2 parents d84a846 + a675ad4 commit 5234daa

34 files changed

Lines changed: 3267 additions & 131 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`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`
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/rest": minor
4+
"@objectstack/metadata-protocol": minor
5+
"@objectstack/lint": minor
6+
"@objectstack/verify": minor
7+
---
8+
9+
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.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the `agent.tools` liveness row is `dead` — it claimed `live` on a key the schema tombstoned
6+
7+
`liveness/agent.json` ships inside this package, and its `tools` row read:
8+
9+
```json
10+
"tools": { "status": "live", "evidence": "cloud: packages/service-ai/src/agent-runtime.ts", "note": "legacy direct-tool fallback." }
11+
```
12+
13+
`agent.tools` was removed in protocol 17 (#3894). `src/ai/agent.zod.ts` declares it
14+
`retiredKey(...)`, which types the key `never` and rejects any authored value with the
15+
upgrade prescription, and the ADR-0087 conversion `agent-tools-to-skills` deletes it from
16+
stored rows and built artifacts when the chain is replayed at rehydration. So nothing can
17+
carry a value for the key and no consumer in any repo can read one — while the ledger's own
18+
vocabulary defines `live` as "Has a runtime consumer".
19+
20+
The verdict moves `live` -> `dead` with **no key added or removed**: the classified total
21+
stays at 1094 and the accept set is byte-identical, because a liveness row is a claim about
22+
the schema rather than the schema. `dead` is the status the ledger's own convention already
23+
gives this class — of the 40 tombstoned top-level keys across the 36 governed types, 39
24+
were already `dead` and this was the only outlier — and it is what puts the key on the
25+
ADR-0049 enforce-or-remove worklist it should have been on since protocol 17. `live-elsewhere`
26+
is refused rather than left undeclared: that status needs a genuine foreign enforcer, and a
27+
key nothing can carry a value for has nothing to enforce.
28+
29+
Nothing changes for authors: writing `agent.tools` failed `tsc` and failed the parse before
30+
this change and fails both after it. What changes is that the ledger, which ships in this
31+
tarball and is the input to the retirement worklist, no longer certifies a consumer that does
32+
not exist.
33+
34+
Also in this change: the stale `evidence` pointer is deleted rather than repointed (a `dead`
35+
row's pointer lives in its `note` by the gate's own design), the ledger's own `_note`
36+
sentence saying the row was deliberately left unstamped is corrected to record the landed
37+
re-grade, `liveness/state-counts.md` is regenerated, and a contract test pins the class —
38+
a `[REMOVED]` tombstone's ledger row says `dead`, on a measured population of 40.

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ as an engine when it declares a write door whose first parameter is named
7171
`IDataEngine` door signature. Interfaces that extend one inherit it; aliases that
7272
narrow one (`Partial<Pick<IDataEngine, …>>`) carry it.
7373

74+
**That rule is read off the TYPE, not off the type's name.** The engine type
75+
index is keyed on declaration names, so a receiver whose declared type is an
76+
inline type literal has no name to look up — and for two sites, both writing
77+
under `{ context: { isSystem: true } }`, that alone subtracted a real engine
78+
write from this population while the census printed the type text and said the
79+
subtraction was probably wrong. The same door rule is now read off the type text
80+
itself, so an unnamed type that states a write door places its site. That is
81+
what moved this page's population from 225 to 227; nothing about the two sites
82+
changed, only whether this instrument could see them.
83+
7484
**The expensive failure direction is a keyword.** Sites whose receiver the author
7585
typed `any` have no type to read, and there are 44 of them — just under a fifth
7686
of the population, concentrated in exactly the seed and bootstrap paths this
@@ -86,7 +96,12 @@ Tenancy itself is enabled *by default* — `isTenancyDisabled()` reads
8696
`tenancy.enabled === false` and nothing else — so the object registry only has to
8797
find the opt-outs. Across 117 declared objects — the dated, ⛔ unenforced
8898
corpus-scale figure below — exactly two opt out (`sys_api_key`,
89-
`sys_sso_provider`), and no write call site on this surface targets either.
99+
`sys_sso_provider`), and exactly one write call site on this surface targets
100+
either — `plugin-auth`'s legacy client-secret migration writes
101+
`sys_sso_provider` under an elevated context. That is the
102+
`declares tenancy off` row below reading **1** rather than 0: the object is
103+
outside this control's reach by its own declaration, not by the census failing
104+
to see the write.
90105

91106
A **declared object** here is a top-level object declaration in a
92107
`*.object.ts(x)` file — `export const X = ObjectSchema.create({ name: … })` —
@@ -106,7 +121,7 @@ are reported as `undecidable` rather than assumed either way.
106121

107122
The same holds twice over for the context. An options argument spelled as a
108123
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
110125
context resolved from an inline literal or a local `const` can be tested for
111126
`isSystem`; one arriving from a helper call cannot.
112127

@@ -140,12 +155,28 @@ direction, on the very figure this page tells other cards to cite. `carries` is
140155
now three-valued, and an unreadable argument can never contribute to the
141156
provable count.
142157

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.
149180

150181
## ⭐ Deviations from the carried figures, untrimmed
151182

@@ -155,10 +186,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
155186

156187
| carried figure | where it survives | this census |
157188
| :--- | :--- | ---: |
158-
| 175 write call sites | quoted in the merged changeset | **225** |
189+
| 175 write call sites | quoted in the merged changeset | **227** |
159190
| 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 |
162193
| 141 and 132, two independent re-derivations | the card that filed this work | — |
163194

164195
**The differences are not reconciled, and deliberately so.** The old census's
@@ -169,17 +200,17 @@ at any commit.
169200

170201
Two structural facts do plausibly widen this reading against any hand or regex
171202
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
173204
`const` rather than inline. An instrument that read either the way a person does
174205
would report a smaller number and would not say so.
175206

176207
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
177208
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
179210
run-time fact — so the claim is neither confirmed nor refuted, and the honest
180211
answer is that a static reading cannot settle it.
181212

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
183214
READ and holds no tenant context, against a decidably tenancy-enabled object.
184215
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
185216
without tenant context" — **32 further sites** have an options argument this
@@ -191,29 +222,29 @@ cannot read, and they are neither in nor out.
191222

192223
| what | count |
193224
| :--- | ---: |
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 |
196227
| …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 |
200231
| PROVABLY carrying none (options read, no context key) | **17** |
201232
| …of those, against a decidably tenancy-enabled object | **9** |
202233
| options argument UNREADABLE — may or may not carry one | 67 |
203234
| …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 |
205236
| threading a context that is decidably NOT elevated | 0 |
206237
| threading a context whose elevation is a run-time fact | 102 |
207238

208239
| how the instrument reached the site | count |
209240
| :--- | ---: |
210-
| receiver carried a readable engine type | 181 |
241+
| receiver carried a readable engine type | 183 |
211242
| receiver erased, placed by the object NAME | 18 |
212243
| receiver erased, placed by an `object: string` PARAMETER | 15 |
213244
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 11 |
214245

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 |
217248
| object name is an `object: string` parameter | 19 |
218249
| object name is some other run-time expression | 57 |
219250

@@ -232,8 +263,14 @@ must not be spelled the same way as «read it, not an engine».
232263

233264
| what | count |
234265
| :--- | ---: |
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.
237274

238275
Every one of them is listed, by receiver and by the type text that could not be
239276
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;
253290
their values are not compared. The reasoning, and the measurement behind it,
254291
are in `scripts/check-tenant-audit-census.mjs`.
255292

256-
Measured on 2026-09-18 at `d4cb05cbf`.
293+
Measured on 2026-09-18 at `30def652e`.
257294

258295
| corpus scale (not enforced) | count |
259296
| :--- | ---: |
260297
| tracked non-test sources scanned | 573 |
261298
| engine-shaped types recognised | 63 |
262299
| declared objects in the registry | 117 |
263-
| same-named calls subtracted as non-engine | 146 |
300+
| same-named calls subtracted as non-engine | 144 |
264301

265302
{/* END GENERATED: tenant-audit-census */}

0 commit comments

Comments
 (0)