Repository navigation
Commit 6a6a4d3
fix(durability): a refused syncObjectSchema logs error at the messaging, datasource and publish seams; the gate reads .call/.apply/.bind and declares it (#23052)
Fixes #22985
Clause-②: no (log levels and an existing gate's vocabulary; no accepted
shape and no published name changes)
## What this changes
`check:durability-log-level` now sees a refused `syncObjectSchema`, and
the four places where one was logged below `error` each give AGENTS.md
"Degradation log levels" its measured answer.
| member | site | answer | evidence |
|---|---|---|---|
| 5, the gate | `scripts/check-durability-degradation-log-level.mjs` |
the guarded-side read resolves `.call` / `.apply` / `.bind(recv)(…)` to
the function they invoke, through a scope-aware `const` alias;
`syncObjectSchema` joins `DURABILITY_CRITICAL_CALLEES` | red on the base
members, green after (below); 5 self-test cases, 3 that must light and 2
that must not, each ablated |
| 1 | `service-messaging` `provisionSystemTables` | `error`, both
halves, once per table per boot, the refusal in meta (mirrors
plugin-audit's line) | the pipeline tables are written as a side effect:
`approval-service.ts` and `audit-writers.ts` (@mention) catch a failed
`emit()` at `warn` and complete, so the action succeeds while its
notification never lands |
| 2 | `service-datasource` `connect()` bound-object loop | `error`, both
halves, once per object per connect | pinned: after the refusal
`connectDeclared` answers `status: 'connected'` and `getConnectionState`
reads `connected` |
| 3 | `metadata-protocol` `ensureObjectStorage` | `console.error`, both
halves | pinned: `publishMetaItem` answers `success: true` while the
object's storage sync was refused |
| 4 | `cli` `measureComposedCoverage` | the caller is told: declared in
`FAILURE_PROPAGATION_SITES` (`failures.set`, `effect`); the cli source
is unchanged | not a durability seam (see below) |
### The gate (member 5)
`calleeName()` is untouched; the read-seam rule and helper following
still use it. A new `guardedCriticalCallee()` is used only by
`collectGuardedCalls`. It reads the invoked operation first and the
plain `calleeName()` second, so reach is only added. Scope rules for the
alias: the nearest enclosing declaration wins, and a parameter, a
`catch` binding, a loop variable or a function declaration of the same
name shadows it. Only a `const` is an alias, because a `let` can be
reassigned. A bare `.bind(recv)` invokes nothing, so it is not a guarded
call. No new rule and no new gate.
Measured on the tree, in order:
| state | seams | red |
|---|---|---|
| base `edecd4485` | 46 | 0 |
| unwrap only, vocabulary unchanged | 46 | 0 (no existing seam lost or
gained) |
| `syncObjectSchema` entry only, no unwrap (scratch copy) | 49 | 3:
members 2, 3, 4. Member 1 and the audit site are invisible because
`sync.call` reads as `call` |
| gate change `a91725f9e` (unwrap + entry), members unfixed | 51 | 4:
members 1–4. The audit site is a seam and loud |
| merged `7d7de7be4`, members answered | 51 | 0 (7 propagating,
declared) |
Red run at `a91725f9e`, `--list` filtered to the five `syncObjectSchema`
seams:
```
packages/cli/src/utils/schema-migration-plugins.ts:2401 guards syncObjectSchema()@2399 → SILENT
packages/metadata-protocol/src/protocol.ts:19405 guards syncObjectSchema()@19404 → QUIET (warn@19406)
packages/plugins/plugin-audit/src/audit-plugin.ts:653 guards syncObjectSchema()@652 → loud (error@660)
packages/services/service-datasource/src/datasource-connection-service.ts:658 guards syncObjectSchema()@657 → QUIET (warn@659)
packages/services/service-messaging/src/messaging-service-plugin.ts:491 guards syncObjectSchema()@490 → QUIET (warn@492)
✗ 4 durability report(s) degrade quietly
```
Green, on the merged head:
```
✓ durability-degradation log levels: 51 durability-critical catch seam(s), all loud, rethrowing or propagating to the caller (7 propagating, declared); 2 counter-guarded summary report(s) outside a catch, all of them able to print against a sink with no `error` (#9748).
```
### Member 2: why `error`, and what it does not claim
The catch cannot tell a managed object from a federated one, and both
end up registered and served with no working storage while the
datasource reads `connected`. A managed object's DDL never ran. A
federated object's remote-table binding was never installed, so its
reads go to a table named after the object. The loop only runs from
`connectDeclared`: runtime-admin connects pass no objects. Every boot
calls `connectDeclared`, so the fix the line names is "resolve the
refusal, then restart". `os migrate` is not named, because it plans only
the primary datasource and its telemetry sibling (`schema-migrate.ts`).
For a federated object, `ObjectQLPlugin.reconcileFederatedBindings`
re-drives the binding at `kernel:ready` and reports what is still
unbound at `error`. A federated refusal can therefore print twice, and
both lines are true. The logger contract has `warn` required and `error`
optional, so the call is spelled `(logger.error ??
logger.warn).call(logger, …)`, never `error?.(…)`. A pin holds the `{
warn }`-only sink.
### Member 4: delivery shape, measured
- The catch has no `return`. It runs `failed++` and
`failures.set(message, …)`, then falls off the end. After the loop,
every `failures` entry becomes one note of the returned `{ coverage,
notes }`. `os migrate plan` / `apply` print those notes (`plan.ts`,
`apply.ts`) next to `coverage.reasons.failed`. That is the CALL shape,
which AGENTS.md declares through the propagation vocabulary. It is the
same shape as `duplicatePackage`'s `failed.push`.
- The repo's own instrument agrees. With `syncObjectSchema` in the
vocabulary, `measure-return-propagating-durability-seams.mjs --sites`
lists 0 members in "[1] the gate SEES this seam today", so this catch is
not a RETURN-shaped member.
- It is not a durability seam at all. This pass runs only on a boot that
DEFERRED its DDL, so the guarded call registers metadata in memory and
writes nothing.
- The existing pin `schema-migration-plugins.test.ts` › "reports a
REFUSING driver as a shortfall, quoting it" holds `reasons.failed` and
the note.
- No checker change was needed to excuse it, and no ⛔ move was made: no
baseline entry, no `logger.error`, no `DURABILITY_CRITICAL_CALLEES`
entry for the site.
## Pins and ablations
Each `error` member has a forced-refusal test that asserts exactly one
line, naming the subject and the remedy. The line has no newline, and
the refusal travels in meta or as a separate argument. Each also asserts
no quieter duplicate and has a CONTROL in which a clean run logs no
`error`. Every negative pin was ablated through
`scripts/ablation-replace.mjs`. In each leg the anchor hit once, the
blob changed, and the restore was proven: blob equals HEAD and `git diff
HEAD` is empty.
| leg (mutation) | red | at |
|---|---|---|
| messaging `error` → `warn` | 8 of 9 (every object; CONTROL green) |
`8a59d7e0a` |
| datasource fallback → `logger.warn.call` | 2 of 4 | `8a59d7e0a` |
| datasource fallback → `logger.error?.call` | 1 of 4 (the `{ warn
}`-only sink) | `8a59d7e0a` |
| protocol `console.error` → `console.warn` | 1 of 6 (CONTROL and the
four existing cases green) | `d80c6d124` |
| gate: guarded read back to plain `calleeName` | 3 self-test cases
(`sync.call` flags; cast alias is a seam; apply/bind ×3) | `8a59d7e0a` |
| gate: bare `.bind` read as an invocation | the must-not-light bind
case | `8a59d7e0a` |
| gate: parameter no longer shadows | the must-not-light shadow case |
`8a59d7e0a` |
| gate: `let` trusted as alias | the must-not-light shadow case |
`8a59d7e0a` |
| gate: member 4's site key renamed | real tree red at the cli site + a
stale-site report | `8a59d7e0a` |
## Files beyond the claim's surface, and why
-
`packages/metadata-protocol/src/protocol.record-package-commit-durability.test.ts`:
its stub engine gains `syncObjectSchema`. `HostEngine` declares it, and
an object publish calls it. Without it the publish threw a TypeError,
which the protocol now reports at `error`, so the file's "logs nothing"
control went red (7 failures, measured). The alternative, a `typeof`
guard in `ensureObjectStorage`, would have been consumer-side tolerance
of an off-contract engine.
-
`packages/metadata-protocol/src/protocol.publish-side-effects-canonical-type.test.ts`:
member 3's pin lives here. Its stub already carries the publish path and
is already in both double ledgers. A copied stub in a new file reddened
`check:engine-double-contract` (3 RETAINED) and
`check:objectql-double-limit` (1 BLIND). The stub's `syncObjectSchema`
now takes a set of refused names.
- `scripts/measure-durability-swallow-family.mjs`: a by-value copy of
the gate vocabulary, self-tested against it.
`check:swallow-census-controls` reds on the drift until the copy gains
`syncObjectSchema`.
- `ensureObjectStorage`'s docblock gained two sentences, beside its
catch.
## Verification
Checks run at `7d7de7be4` (`origin/main` merged once, at `7d7de7be4`;
full `turbo run build` after the merge):
- `pnpm --filter @objectstack/service-messaging test`: 48 files, 549
passed; `typecheck` green.
- `pnpm --filter @objectstack/service-datasource test`: 43 files, 769
passed; `typecheck` green.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run`: 227
files passed, 3 skipped; 28192 passed; `typecheck` green.
- `tsc --listFiles` includes each touched test file in its package's
program.
- Gates: the 89 commands `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derives for this change set (it
includes everything the dispatch listed except `check:i18n-coverage` and
`check:i18n-walk-parity`, which were run as well), plus
`measure-return-propagating-durability-seams.mjs --self-test` and
`measure-durability-swallow-family.mjs --self-test`. All 93 exited 0,
and the exit code was captured before any pipe. `dispatch-gates --ran`
with those codes reports `89 derived famil(ies) accounted for — 89 run,
0 NOT-MEASURED (a DERIVED zero — all 89 recorded an exit code and none
of them is 3)`. This includes `check:durability-log-level` (self-test 68
+ 57 cases), `check:swallow-census-controls`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:dual-build-cjs-loads` and `check:i18n`. The last two read built
output, so they ran after the post-merge full build.
- Lint, narrowed and proven: `eslint --no-inline-config --format json`
over the 8 changed `.ts`/`.mjs` files. The JSON lists all 8 as linted
and none as ignored: 0 errors, 0 warnings. The population is all of the
changed code files, read from eslint's own output. The narrowing cannot
move any other file's verdict, because `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`, no typed rules; its own
header says so).
- NOT MEASURED:
- cli tests: no cli source changed, so the `unit` layer is not owed and
`integration` is left to CI.
- Downstream suites that drive the real protocol (`objectql`, `rest`,
`runtime`): not run locally. A static read of every test there that
spies `console.error` found no count-based assertion over an object
publish whose engine lacks `syncObjectSchema`.
- Member 1 at a public door after the fix (`os serve`): not run. The
base-side reading by the #22913 dev stands, and the unit pin holds the
level.
## Acceptance notes
- **Observed, not filed (reach not measured):** under
`OS_SKIP_SCHEMA_SYNC=1`, member 2's loop still issues DDL for a managed
object bound to a declared datasource. `connectDeclared` is called
unconditionally from `AppPlugin.start()`, and `syncObjectSchema` is
ungated. This is the shape #22984 repaired at the two provisioning
sites. It is code reading only, with no public-door measurement.
- **Doc drift:** `ObjectQLPlugin.reconcileFederatedBindings`' docblock
says `DatasourceConnectionService` re-drives "not at all when
`OS_SKIP_SCHEMA_SYNC` is set". No such gate exists in `AppPlugin` or the
connection service.
- The two census instruments
(`measure-return-propagating-durability-seams.mjs`,
`measure-durability-swallow-family.mjs`) keep their own guarded-call
readers, which do not resolve `.call`. They read the gate's vocabulary
but are not the gate.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e49aaec commit 6a6a4d3
10 files changed
Lines changed: 718 additions & 15 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- services
- service-datasource/src
- __tests__
- service-messaging/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | | - | |
| 106 | + | |
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
| |||
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
| 186 | + | |
186 | 187 | | |
| 188 | + | |
187 | 189 | | |
188 | 190 | | |
189 | 191 | | |
| |||
200 | 202 | | |
201 | 203 | | |
202 | 204 | | |
203 | | - | |
204 | | - | |
| 205 | + | |
| 206 | + | |
205 | 207 | | |
206 | 208 | | |
207 | 209 | | |
| |||
327 | 329 | | |
328 | 330 | | |
329 | 331 | | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
176 | 181 | | |
177 | 182 | | |
178 | 183 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19387 | 19387 | | |
19388 | 19388 | | |
19389 | 19389 | | |
19390 | | - | |
| 19390 | + | |
| 19391 | + | |
| 19392 | + | |
| 19393 | + | |
19391 | 19394 | | |
19392 | 19395 | | |
19393 | 19396 | | |
| |||
19403 | 19406 | | |
19404 | 19407 | | |
19405 | 19408 | | |
19406 | | - | |
| 19409 | + | |
| 19410 | + | |
| 19411 | + | |
| 19412 | + | |
| 19413 | + | |
| 19414 | + | |
| 19415 | + | |
| 19416 | + | |
| 19417 | + | |
| 19418 | + | |
| 19419 | + | |
| 19420 | + | |
| 19421 | + | |
| 19422 | + | |
| 19423 | + | |
| 19424 | + | |
19407 | 19425 | | |
19408 | 19426 | | |
19409 | 19427 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
0 commit comments