Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/init-service-declared-enforced.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
---
'@objectstack/metadata': patch
'@objectstack/rest': patch
'@objectstack/cli': patch
'@objectstack/service-analytics': patch
'@objectstack/service-datasource': patch
'@objectstack/service-job': patch
'@objectstack/service-knowledge': patch
'@objectstack/service-queue': patch
'@objectstack/service-settings': patch
'@objectstack/service-storage': patch
---

Init-time service consumption is now declared everywhere, and the declaration is enforced (#4471, ADR-0116). A new CI gate (`check:init-service-contract`) walks every plugin's `init()` call graph — including private helpers, the shape that shipped #4420 — and errors on any init-reachable `getService('X')` of a workspace-provided service that is not covered by `dependencies`, `optionalDependencies`, or `requiresServices`. Eleven previously undeclared init-time consumers (metadata, rest, cli serve plugins, and seven services) now declare `optionalDependencies` on their providers, so the kernel orders them deterministically instead of by registration luck; each still degrades on purpose when the provider is not composed. Plugin authors: a best-effort init-time `getService` must declare its provider in `optionalDependencies` (declared tolerance) — the checker never exempts it.
15 changes: 15 additions & 0 deletions .github/workflows/lint.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -173,6 +173,21 @@ jobs:
- name: Wildcard fall-through guard
run: pnpm check:wildcard-fallthrough

# Init-service declaration guard (#4471, ADR-0116). The kernel's ordering
# contract (dependencies / optionalDependencies / requiresServices /
# providesServices) was complete but VOLUNTARY: a plugin that resolves
# getService('X') during init() and declares nothing fails only under
# unlucky composition orders, usually inside a best-effort try/catch that
# downgrades the miss to a warn. That silence shipped #4085 and #4420 (the
# latter losing every in-flight approval on restart). This scan walks each
# plugin's init() call graph from the AST — the #4420 call sat in a private
# helper, not init()'s own body — and errors on any init-reachable
# getService of a workspace-provided service that no declaration covers.
# Declared tolerance stays in the plugin (optionalDependencies), never in a
# checker-side ledger. Runs its own --self-test first.
- name: Init-service declaration guard
run: pnpm check:init-service-contract

# Release-notes drift guard: the platform is one version-locked train, so
# every released @objectstack/spec major must have a curated, navigable
# release page at content/docs/releases/v<major>.mdx. Catches the gap that
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,7 @@
"check:route-envelope": "node scripts/check-route-envelope.mjs --self-test && node scripts/check-route-envelope.mjs",
"check:error-code-casing": "node scripts/check-error-code-casing.mjs --self-test && node scripts/check-error-code-casing.mjs",
"check:wildcard-fallthrough": "node scripts/check-wildcard-fallthrough.mjs --self-test && node scripts/check-wildcard-fallthrough.mjs",
"check:init-service-contract": "node scripts/check-init-service-contract.mjs --self-test && node scripts/check-init-service-contract.mjs",
"check:console-sha": "node scripts/check-console-sha.mjs",
"check:release-notes": "node scripts/check-release-notes.mjs",
"check:node-version": "node scripts/check-node-version.mjs",
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/commands/serve.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1270,6 +1270,11 @@ export default class Serve extends Command {
const guardPlugin: any = {
name: 'com.objectstack.cli.unknown-hostname-guard',
version: '1.0.0',
// init() resolves the `http.server` service the hono server plugin
// provides — order-if-present so the middleware install is
// deterministic (ADR-0116, #4471). Soft: without a server plugin the
// guard degrades on purpose (warn + not installed).
optionalDependencies: ['com.objectstack.server.hono'],
init: async (ctx: any) => {
try {
const httpServer: any = ctx.getService?.('http.server') ?? ctx.getService?.('http-server');
Expand DownExpand Up@@ -2375,6 +2380,11 @@ export default class Serve extends Command {
const adminRoutePlugin: any = {
name: 'com.objectstack.cli.datasource-admin-routes',
version: '1.0.0',
// init() resolves the `http.server` service the hono server plugin
// provides — order-if-present so route registration is
// deterministic (ADR-0116, #4471). Soft: without a server plugin
// the routes degrade on purpose (warn + not installed).
optionalDependencies: ['com.objectstack.server.hono'],
init: async (ctx: any) => {
try {
const httpServer: any =
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/plugin-order.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,16 @@
* Declare only unconditional registrations: a conditional service (e.g.
* one gated behind an option) would indict this plugin for orderings it
* cannot actually satisfy.
*
* Declaring is NOT voluntary (#4471). Everything above can only enforce what
* a plugin declares — a plugin that resolves `getService('X')` during init()
* and declares nothing was invisible to all of it, failing only under
* unlucky composition orders (#4085, and #4420 at data-consistency cost).
* `scripts/check-init-service-contract.mjs` (CI: `check:init-service-contract`)
* closes that gap: it walks every plugin's init() call graph and errors on
* any init-reachable getService of a workspace-provided service that no
* declaration covers. Best-effort tolerance is declared IN the plugin via
* `optionalDependencies`, never exempted in the checker.
*/

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/metadata/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -206,6 +206,14 @@ export class MetadataPlugin implements Plugin {
* initializes.
*/
providesServices = ['metadata'];
/**
* init() registers the metadata system objects through the `manifest`
* service ObjectQLPlugin provides — order-if-present so that
* registration is deterministic instead of "whichever init ran first"
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
* degrades on purpose (objects are discovered via the legacy fallback).
*/
optionalDependencies = ['com.objectstack.engine.objectql'];

private manager: NodeMetadataManager;
private options: MetadataPluginOptions;
Expand Down
9 changes: 8 additions & 1 deletion packages/rest/src/rest-api-plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,14 @@ export function createRestApiPlugin(config: RestApiPluginConfig = {}): Plugin {
return {
name: 'com.objectstack.rest.api',
version: '1.0.0',

/**
* init() registers sys_import_job through the `manifest` service
* ObjectQLPlugin provides — order-if-present so the registration is
* deterministic (ADR-0116, #4471). Soft, not hard: on an engine-less
* kernel the plugin degrades on purpose (warn + no import-job object).
*/
optionalDependencies: ['com.objectstack.engine.objectql'],

init: async (ctx: PluginContext) => {
// Register the async-import job object so its state/progress/history
// is queryable in Studio and readable by the import-job routes.
Expand Down
8 changes: 8 additions & 0 deletions packages/services/service-analytics/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -161,6 +161,14 @@ export class AnalyticsServicePlugin implements Plugin {
version = '1.0.0';
type = 'standard' as const;
dependencies: string[] = [];
/**
* init() probes the `data` engine ObjectQLPlugin provides for the
* auto-bridge — order-if-present so the probe verdict is deterministic
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
* degrades on purpose (per-query lazy resolution / explicit
* `executeAggregate`).
*/
optionalDependencies: string[] = ['com.objectstack.engine.objectql'];

private service?: AnalyticsService;
private readonly options: AnalyticsServicePluginOptions;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,13 @@ export class DatasourceAdminServicePlugin implements Plugin {
version = '1.0.0';
type = 'standard' as const;
dependencies: string[] = [];
/**
* init() contributes the Setup-app nav entry through the `manifest`
* service ObjectQLPlugin provides — order-if-present so the contribution
* is deterministic (ADR-0116, #4471). Soft, not hard: without an engine
* the plugin degrades on purpose (no nav entry, admin service still up).
*/
optionalDependencies: string[] = ['com.objectstack.engine.objectql'];

private service?: DatasourceAdminService;
private config?: DatasourceAdminServiceConfig;
Expand Down
8 changes: 8 additions & 0 deletions packages/services/service-job/src/job-service-plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,6 +48,14 @@ export class JobServicePlugin implements Plugin {
* kernel name this plugin when a consumer requires one before it inits.
*/
providesServices = ['job'];
/**
* init() registers sys_job/sys_job_run through the `manifest` service
* ObjectQLPlugin provides, and probes the `cluster` service for the cron
* adapter's leader election — order-if-present so both resolutions are
* deterministic (ADR-0116, #4471). Soft, not hard: without either the
* plugin degrades on purpose (in-memory adapter, single-node cron).
*/
optionalDependencies = ['com.objectstack.engine.objectql', 'com.objectstack.service.cluster'];
version = '1.1.0';
type = 'standard';

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -59,6 +59,13 @@ export class KnowledgeServicePlugin implements Plugin {
name = 'com.objectstack.service.knowledge';
version = '0.1.0';
type = 'standard';
/**
* init() resolves the `objectql` engine for RLS re-checks —
* order-if-present so the resolution is deterministic (ADR-0116, #4471).
* Soft, not hard: without an engine the service degrades on purpose
* (pure-search mode with conservative RLS).
*/
optionalDependencies = ['com.objectstack.engine.objectql'];

private service: KnowledgeService | null = null;
private subscriptionId: string | undefined;
Expand Down
7 changes: 7 additions & 0 deletions packages/services/service-queue/src/queue-service-plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,6 +41,13 @@ export class QueueServicePlugin implements Plugin {
* kernel name this plugin when a consumer requires one before it inits.
*/
providesServices = ['queue'];
/**
* init() registers sys_job_queue through the `manifest` service
* ObjectQLPlugin provides — order-if-present so the registration is
* deterministic (ADR-0116, #4471). Soft, not hard: without an engine the
* plugin degrades on purpose (in-memory queue adapter).
*/
optionalDependencies = ['com.objectstack.engine.objectql'];
version = '1.1.0';
type = 'standard';

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,6 +78,13 @@ export class SettingsServicePlugin implements Plugin {
* kernel name this plugin when a consumer requires one before it inits.
*/
providesServices = ['settings'];
/**
* init() registers the settings K/V object through the `manifest` service
* ObjectQLPlugin provides — order-if-present so the registration is
* deterministic (ADR-0116, #4471). Soft, not hard: lean test kernels
* without an engine degrade on purpose (no sys table, service still up).
*/
optionalDependencies = ['com.objectstack.engine.objectql'];
version = SETTINGS_PLUGIN_VERSION;
type = 'standard' as const;

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -121,6 +121,13 @@ export class StorageServicePlugin implements Plugin {
* kernel name this plugin when a consumer requires one before it inits.
*/
providesServices = ['file-storage'];
/**
* init() registers sys_file / sys_upload_session / sys_attachment through
* the `manifest` service ObjectQLPlugin provides — order-if-present so the
* registration is deterministic (ADR-0116, #4471). Soft, not hard: without
* an engine the plugin degrades on purpose (storage service still up).
*/
optionalDependencies = ['com.objectstack.engine.objectql'];
version = '1.0.0';
type = 'standard';

Expand Down
Loading
Loading