@@ -63,6 +63,17 @@ import {
6363} from '../utils/port-contract.js' ;
6464import { BootLogCapture , isVerboseBootLevel } from '../utils/boot-log-capture.js' ;
6565import { graftAuthoredRuntimeMembers , isAppPluginLike } from '../utils/graft-runtime-hooks.js' ;
66+ // [ADR-0130 D4 / option B, #15006] Every read below that keys off a
67+ // PACKAGE-OWNED collection goes through this seam, so an option-B artifact
68+ // (flattened top level gone, `packages[]` carrying everything once) reaches
69+ // the same decision — and so the acceptance probe can CALL the decision
70+ // instead of re-implementing it.
71+ import {
72+ shouldAutoRegisterObjectQL ,
73+ shouldAutoRegisterStorageDriver ,
74+ stackDeclaresMetadata ,
75+ bundleDeclaresTranslations ,
76+ } from '../utils/stack-collections.js' ;
6677import { redactConnectionUrl , describeDriverConnection } from '../utils/connection-display.js' ;
6778// The posture prose `os serve` and `os doctor` BOTH print, declared once
6879// (#12492) — and, since #12579, the multi-org runtime SPELLING those two
@@ -2628,8 +2639,12 @@ export default class Serve extends Command {
26282639 }
26292640
26302641 // 1. Auto-register ObjectQL Plugin if objects define but plugins missing
2631- const hasObjectQL = plugins . some ( ( p : any ) => p . name ?. includes ( 'objectql' ) || p . constructor ?. name ?. includes ( 'ObjectQL' ) ) ;
2632- if ( config . objects && ! hasObjectQL ) {
2642+ // [#15006] The whole gate — the `objects` read AND the already-composed
2643+ // check — is `shouldAutoRegisterObjectQL`. It answers exactly as the two
2644+ // inline expressions did for every stack that boots today, and resolves
2645+ // `packages[]` when the flattened top level is absent, which is the shape
2646+ // that used to boot with NO QUERY ENGINE and throw nothing.
2647+ if ( shouldAutoRegisterObjectQL ( config , plugins ) ) {
26332648 try {
26342649 const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
26352650 await kernel . use ( new ObjectQLPlugin ( ) ) ;
@@ -2661,12 +2676,9 @@ export default class Serve extends Command {
26612676 // at boot through the datasource connection service, so building a
26622677 // storage driver here would construct a duplicate pool the engine then
26632678 // discards as already-registered.
2664- const hasDriver = plugins . some ( ( p : any ) =>
2665- p . name ?. includes ( 'driver' ) ||
2666- p . constructor ?. name ?. includes ( 'Driver' ) ||
2667- p . name === 'com.objectstack.runtime.default-datasource' ||
2668- p . constructor ?. name === 'DefaultDatasourcePlugin' ) ;
2669- if ( ! hasDriver && config . objects ) {
2679+ // [#15006] Same seam, same reason — see the ObjectQL gate above. The
2680+ // driver-provider duck-typing moved into it with the read it guards.
2681+ if ( shouldAutoRegisterStorageDriver ( config , plugins ) ) {
26702682 const databaseUrl = process . env . OS_DATABASE_URL ;
26712683 const driverType = resolveDriverType ( process . env . OS_DATABASE_DRIVER , databaseUrl ) ;
26722684 // libSQL/Turso's credential is the only one that does NOT ride inside the
@@ -2799,9 +2811,10 @@ export default class Serve extends Command {
27992811 // already holds an AppPlugin instance — and never on a named app, so it
28002812 // is checked structurally below.
28012813 const hasAppPluginAlready = plugins . some ( isAppPluginLike ) ;
2802- const configHasMetadata = ! ! (
2803- config . objects || config . manifest || config . apps || config . flows || config . apis
2804- ) ;
2814+ // [#15006] The same predicate `schema-migration-plugins.ts` runs after its
2815+ // own second `loadConfig` (B4) — folded into one seam rather than left as
2816+ // two copies whose comment already said they were the same.
2817+ const configHasMetadata = stackDeclaresMetadata ( config ) ;
28052818
28062819 // ── Decide the dev-only artifact door BEFORE the wrap (#14397) ────
28072820 // On a HOST config `os dev` composes TWO writers over ONE stack: the
@@ -2973,24 +2986,19 @@ export default class Serve extends Command {
29732986 // `plugins` array — a host/aggregator config may define no translations
29742987 // of its own and instead compose several `new AppPlugin(...)` entries,
29752988 // each carrying its own. Keyed on that shape, not on a named app.
2976- const pluginBundleHasTranslations = ( bundle : any ) : boolean => {
2977- if ( ! bundle || typeof bundle !== 'object' ) return false ;
2978- if ( Array . isArray ( bundle . translations ) && bundle . translations . length > 0 ) return true ;
2979- if ( bundle . i18n ) return true ;
2980- if ( bundle . manifest && (
2981- ( Array . isArray ( bundle . manifest . translations ) && bundle . manifest . translations . length > 0 )
2982- || bundle . manifest . i18n
2983- ) ) return true ;
2984- return false ;
2985- } ;
2989+ // [#15006] `bundleDeclaresTranslations` is that same shape-keyed check plus
2990+ // the `packages[]` leg: `translations` is package-owned and `i18n` is an
2991+ // envelope key a translations-only stack never sets, so an option-B artifact
2992+ // reached this gate with neither and the REST i18n routes silently did not
2993+ // exist. MEASURED on the acceptance probe, not inferred.
29862994 const anyAppPluginHasTranslations = plugins . some ( ( p : any ) => {
29872995 if ( ! p ) return false ;
29882996 // AppPlugin instances expose their bundle on `.bundle`
2989- if ( p . bundle && pluginBundleHasTranslations ( p . bundle ) ) return true ;
2997+ if ( p . bundle && bundleDeclaresTranslations ( p . bundle ) ) return true ;
29902998 return false ;
29912999 } ) ;
29923000 const configHasTranslations = (
2993- pluginBundleHasTranslations ( config )
3001+ bundleDeclaresTranslations ( config )
29943002 || anyAppPluginHasTranslations
29953003 ) ;
29963004 if ( ! hasI18nPlugin && configHasTranslations && tierEnabled ( 'i18n' ) ) {
0 commit comments