Skip to content

[finding] Nothing requires the settings ordering declaration — check:init-service-contract walks init() only, and the settings bind window is a kernel:ready fact #11045

Description

@os-warren

Found while implementing #10250 (PR #11044). Filed separately rather than widening that card: the ruling there names three specific plugins and their declarations, and building a gate is a different, larger piece of work. Filed unassigned.

Observation

#10250's fix makes plugin-email, service-sms and service-storage each declare

optionalDependencies=['com.objectstack.service.settings'];

so the kernel orders SettingsServicePlugin's engine bind ahead of their kernel:ready settings reads. That closes the three measured readers.

Nothing checks that a fourth one declares it. A plugin added tomorrow that acquires a settings handle in a kernel:ready hook and forgets the declaration is back in the pre-bind window, and the only thing that will say so is the runtime warn PR #11044 adds — which requires someone to be reading boot logs of a deployment whose composition order happens to be wrong.

Why the existing gate does not cover it

scripts/check-init-service-contract.mjs (check:init-service-contract) is exactly the right shape — ADR-0116 / #4471, "declaring is NOT voluntary" — but its population is init()-reachable getService calls. The settings bind window is not an init() fact:

  • SettingsServicePlugin registers its bind hook from start(), not init();
  • the readers acquire their handle inside a kernel:ready hook registered from start();
  • so both sides of the ordering constraint are invisible to a walk rooted at init().

Measured on origin/main at 2866d5f97: check:init-service-contract reports 34 declared / 1 self-provided / 3 without a workspace provider (68 plugin unit(s) scanned) and is green both before and after the three declarations land — i.e. it is indifferent to the property this card is about.

The population, measured

Shipped settings-handle acquisitions (getService[^(]*\(\s*['"]settings['"], tests and dist/ excluded; the regex was proved on three known readers before its silence was read anywhere) — 11 real acquisitions across 13 hits, the other 2 being string literals inside service-settings comments:

sitein an always-on provider?declares the edge after PR #11044?
packages/plugins/plugin-email/src/email-plugin.ts:388yes (email)yes
packages/services/service-sms/src/sms-plugin.ts:189yes (sms)yes
packages/services/service-storage/src/storage-service-plugin.ts:406yes (storage)yes
packages/plugins/plugin-auth/src/auth-plugin.ts:703nono — but defends itself via resolved.source !== 'default'
packages/plugins/plugin-auth/src/auth-plugin.ts:1182nono — not analysed
packages/plugins/plugin-audit/src/audit-plugin.ts:200nono — not analysed
packages/mcp/src/plugin.ts:301nono — not analysed
packages/rest/src/rest-api-plugin.ts:299nono — not analysed
packages/objectql/src/plugin.ts:436, :509nono — not analysed
packages/runtime/src/security/resolve-execution-context.ts:204norequest-time, not boot-time

⚠️ The five not analysed rows are the honest part of this card: whether each of them reaches its getService('settings') from a hook registered before SettingsServicePlugin.start() has not been established. #10250's census established it only for the three always-on providers. That analysis is the first step here, and it may well come back "none of them" — which would make this a hygiene card rather than a defect card.

What a gate would have to do

Walk each plugin's start()-registered kernel:ready hook body (transitively, like the init walk does) for a getService('settings') and require com.objectstack.service.settings in that plugin's dependencies / optionalDependencies.

Two known hazards for whoever picks this up, both measured on the #10250 census:

  • a name-based call-graph walker's transitive arm is the part that fails quietly. That census's walker hit 4 of 6 known readers, every hit at depth 0, and an earlier revision mis-parsed every (ctx as any).hook(...) site and returned 1-line bodies for 8 of 16 hooks — "a zero that looked completely clean". A gate resting on that walk needs a --self-test that pins the transitive arm, not just depth 0.
  • multi-line function signatures drop out of a naive definition index (bootAutoEnqueue( was the measured case).

Adjacent, not duplicates

#4471 / #4131 established the init()-phase contract and produced the existing gate; this is the same argument one lifecycle phase later. #10250 is the defect this generalises from and is being fixed narrowly, by declaration, on the three measured readers.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions