Repository navigation
fix(durability): a refused syncObjectSchema logs error at the messaging, datasource and publish seams; the gate reads .call/.apply/.bind and declares it - #23052
Conversation
…e guarded side; declare syncObjectSchema The guarded-side callee read used the property a call is spelled through, so `sync.call(engine, name)` read as `call`. It now resolves the adapter to the function it invokes, through a scope-aware const alias, the way the log side has read the same adapters for a long time. The plain reading is still consulted, so reach is only added. `syncObjectSchema` joins the vocabulary. On this commit the checker is RED on the four members it now sees (service-messaging, service-datasource, metadata-protocol, cli); the plugin-audit site is seen and already loud. The members are answered in the next commits. Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
…d fix; the migrate-plan catch is declared as delivering - service-messaging provisionSystemTables: one `error` per refused object per boot, consequence + fix, the refusal in meta (mirrors plugin-audit's line). - service-datasource connect: one `error` per refused bound object per connect, spelled `(logger.error ?? logger.warn).call(...)` against the warn-required sink contract. - metadata-protocol ensureObjectStorage: `console.error` with consequence + fix after a publish that already answered success. - cli measureComposedCoverage: not a durability seam (deferred DDL); its catch delivers each refusal into the returned plan notes through `failures.set`, declared in FAILURE_PROPAGATION_SITES. check:durability-log-level: 51 seams, all loud, rethrowing or propagating. Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
… at all Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
…yncObjectSchema `HostEngine` declares it and an object publish calls it. Without it the publish's storage sync threw a TypeError, which the protocol now reports at `error`, and the file's "logs nothing" control read that as a commit-store line. Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
…essaging, service-datasource and metadata-protocol Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
…stub that is already ledgered; census copy learns syncObjectSchema The member-3 pin moves into protocol.publish-side-effects-canonical-type.test.ts, whose stub already carries the publish path and is already in the engine-double and limit ledgers, instead of a copied stub that both gates would have to learn. The stub's syncObjectSchema takes a set of refused names. measure-durability-swallow-family.mjs copies the gate vocabulary by value and self-tests the copy, so it gains syncObjectSchema with the gate. Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce Co-authored-by: Claude <noreply@anthropic.com>
…hema-sync-durability-family
📓 Docs Drift CheckThis PR changes 3 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b151e7850f9283e0c900392679a7a412f58664c5 && git checkout b151e7850f9283e0c900392679a7a412f58664c5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df7a137923be503bf6fea15a6fc944d3ba06df6f 7d7de7be40859fd84e17f025f047d6a760116793 && git checkout -B drift-repro df7a137923be503bf6fea15a6fc944d3ba06df6f && git merge --no-ff 7d7de7be40859fd84e17f025f047d6a760116793
node scripts/docs-audit/affected-docs.mjs --json df7a137923be503bf6fea15a6fc944d3ba06df6f
|
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-levelnow sees a refusedsyncObjectSchema, and the four places where one was logged belowerroreach give AGENTS.md "Degradation log levels" its measured answer.scripts/check-durability-degradation-log-level.mjs.call/.apply/.bind(recv)(…)to the function they invoke, through a scope-awareconstalias;syncObjectSchemajoinsDURABILITY_CRITICAL_CALLEESservice-messagingprovisionSystemTableserror, both halves, once per table per boot, the refusal in meta (mirrors plugin-audit's line)approval-service.tsandaudit-writers.ts(@mention) catch a failedemit()atwarnand complete, so the action succeeds while its notification never landsservice-datasourceconnect()bound-object looperror, both halves, once per object per connectconnectDeclaredanswersstatus: 'connected'andgetConnectionStatereadsconnectedmetadata-protocolensureObjectStorageconsole.error, both halvespublishMetaItemanswerssuccess: truewhile the object's storage sync was refusedclimeasureComposedCoverageFAILURE_PROPAGATION_SITES(failures.set,effect); the cli source is unchangedThe gate (member 5)
calleeName()is untouched; the read-seam rule and helper following still use it. A newguardedCriticalCallee()is used only bycollectGuardedCalls. It reads the invoked operation first and the plaincalleeName()second, so reach is only added. Scope rules for the alias: the nearest enclosing declaration wins, and a parameter, acatchbinding, a loop variable or a function declaration of the same name shadows it. Only aconstis an alias, because aletcan 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:
edecd4485syncObjectSchemaentry only, no unwrap (scratch copy)sync.callreads ascalla91725f9e(unwrap + entry), members unfixed7d7de7be4, members answeredRed run at
a91725f9e,--listfiltered to the fivesyncObjectSchemaseams:Green, on the merged head:
Member 2: why
error, and what it does not claimThe 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 fromconnectDeclared: runtime-admin connects pass no objects. Every boot callsconnectDeclared, so the fix the line names is "resolve the refusal, then restart".os migrateis not named, because it plans only the primary datasource and its telemetry sibling (schema-migrate.ts). For a federated object,ObjectQLPlugin.reconcileFederatedBindingsre-drives the binding atkernel:readyand reports what is still unbound aterror. A federated refusal can therefore print twice, and both lines are true. The logger contract haswarnrequired anderroroptional, so the call is spelled(logger.error ?? logger.warn).call(logger, …), nevererror?.(…). A pin holds the{ warn }-only sink.Member 4: delivery shape, measured
return. It runsfailed++andfailures.set(message, …), then falls off the end. After the loop, everyfailuresentry becomes one note of the returned{ coverage, notes }.os migrate plan/applyprint those notes (plan.ts,apply.ts) next tocoverage.reasons.failed. That is the CALL shape, which AGENTS.md declares through the propagation vocabulary. It is the same shape asduplicatePackage'sfailed.push.syncObjectSchemain the vocabulary,measure-return-propagating-durability-seams.mjs --siteslists 0 members in "[1] the gate SEES this seam today", so this catch is not a RETURN-shaped member.schema-migration-plugins.test.ts› "reports a REFUSING driver as a shortfall, quoting it" holdsreasons.failedand the note.logger.error, noDURABILITY_CRITICAL_CALLEESentry for the site.Pins and ablations
Each
errormember 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 noerror. Every negative pin was ablated throughscripts/ablation-replace.mjs. In each leg the anchor hit once, the blob changed, and the restore was proven: blob equals HEAD andgit diff HEADis empty.error→warn8a59d7e0alogger.warn.call8a59d7e0alogger.error?.call{ warn }-only sink)8a59d7e0aconsole.error→console.warnd80c6d124calleeNamesync.callflags; cast alias is a seam; apply/bind ×3)8a59d7e0a.bindread as an invocation8a59d7e0a8a59d7e0alettrusted as alias8a59d7e0a8a59d7e0aFiles beyond the claim's surface, and why
packages/metadata-protocol/src/protocol.record-package-commit-durability.test.ts: its stub engine gainssyncObjectSchema.HostEnginedeclares it, and an object publish calls it. Without it the publish threw a TypeError, which the protocol now reports aterror, so the file's "logs nothing" control went red (7 failures, measured). The alternative, atypeofguard inensureObjectStorage, 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 reddenedcheck:engine-double-contract(3 RETAINED) andcheck:objectql-double-limit(1 BLIND). The stub'ssyncObjectSchemanow 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-controlsreds on the drift until the copy gainssyncObjectSchema.ensureObjectStorage's docblock gained two sentences, beside its catch.Verification
Checks run at
7d7de7be4(origin/mainmerged once, at7d7de7be4; fullturbo run buildafter the merge):pnpm --filter @objectstack/service-messaging test: 48 files, 549 passed;typecheckgreen.pnpm --filter @objectstack/service-datasource test: 43 files, 769 passed;typecheckgreen.pnpm --filter @objectstack/metadata-protocol exec vitest run: 227 files passed, 3 skipped; 28192 passed;typecheckgreen.tsc --listFilesincludes each touched test file in its package's program.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives for this change set (it includes everything the dispatch listed exceptcheck:i18n-coverageandcheck:i18n-walk-parity, which were run as well), plusmeasure-return-propagating-durability-seams.mjs --self-testandmeasure-durability-swallow-family.mjs --self-test. All 93 exited 0, and the exit code was captured before any pipe.dispatch-gates --ranwith those codes reports89 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 includescheck:durability-log-level(self-test 68 + 57 cases),check:swallow-census-controls,check:engine-double-contract,check:objectql-double-limit,check:dual-build-cjs-loadsandcheck:i18n. The last two read built output, so they ran after the post-merge full build.eslint --no-inline-config --format jsonover the 8 changed.ts/.mjsfiles. 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, becauseeslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules; its own header says so).unitlayer is not owed andintegrationis left to CI.objectql,rest,runtime): not run locally. A static read of every test there that spiesconsole.errorfound no count-based assertion over an object publish whose engine lackssyncObjectSchema.os serve): not run. The base-side reading by the plugin-audit: a failed DDL for an audit table at boot is logged at warn and the boot stays green #22913 dev stands, and the unit pin holds the level.Acceptance notes
OS_SKIP_SCHEMA_SYNC=1, member 2's loop still issues DDL for a managed object bound to a declared datasource.connectDeclaredis called unconditionally fromAppPlugin.start(), andsyncObjectSchemais ungated. This is the shape plugin-audit: provisionSystemTables creates tables under OS_SKIP_SCHEMA_SYNC=1, while objectql's boot says it registered every schema WITHOUT DDL #22984 repaired at the two provisioning sites. It is code reading only, with no public-door measurement.ObjectQLPlugin.reconcileFederatedBindings' docblock saysDatasourceConnectionServicere-drives "not at all whenOS_SKIP_SCHEMA_SYNCis set". No such gate exists inAppPluginor the connection service.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