@teamscala/service-boot 0.23.523 → 0.23.525

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +1 -9
  2. package/src/agreement-gate/agreement-lookup.ts +0 -7
  3. package/src/agreement-gate/authorized-emails.ts +0 -14
  4. package/src/agreement-gate/placeholder-strip.ts +0 -10
  5. package/src/agreement-gate/resolution-caches.ts +0 -5
  6. package/src/agreement-gate/settings.ts +0 -22
  7. package/src/agreement-gate/signing-session.ts +0 -8
  8. package/src/agreement-gate/signing-shell.ts +0 -9
  9. package/src/agreement-gate.ts +0 -52
  10. package/src/apply-logger-config.ts +1 -9
  11. package/src/asset-cache-key.ts +1 -5
  12. package/src/asset-optimize.ts +1 -7
  13. package/src/asset-proxy.ts +1 -10
  14. package/src/asset-social-card.ts +1 -9
  15. package/src/attach-agreement-magic.ts +0 -14
  16. package/src/attach-portal-members.ts +0 -17
  17. package/src/attach-proposal-accept.ts +1 -21
  18. package/src/attach-proposal-magic.ts +1 -13
  19. package/src/attach-transfers-upload.ts +0 -21
  20. package/src/auth/sign-out.ts +1 -44
  21. package/src/auth-gate/public-paths.ts +1 -96
  22. package/src/auth-gate.ts +1 -41
  23. package/src/bind-page-handler.ts +1 -83
  24. package/src/build-client-assets/build-lock.ts +1 -11
  25. package/src/build-client-assets/hashed-output.ts +1 -11
  26. package/src/build-client-assets/hydrate-bundle.ts +1 -137
  27. package/src/build-client-assets/manifest.ts +1 -30
  28. package/src/build-client-assets/source-candidates.ts +0 -51
  29. package/src/build-client-assets/sourcemaps-upload.ts +1 -52
  30. package/src/build-client-assets/styles-build.ts +1 -71
  31. package/src/build-client-assets.ts +1 -30
  32. package/src/capability-slug.ts +1 -45
  33. package/src/compose-page-title.ts +1 -34
  34. package/src/content-gate/access.ts +0 -34
  35. package/src/content-gate/catalog.ts +0 -52
  36. package/src/content-gate/config-types.ts +3 -90
  37. package/src/content-gate/gate-entry-clears.ts +0 -18
  38. package/src/content-gate/gate.ts +0 -27
  39. package/src/content-gate/parse-cookie.ts +0 -4
  40. package/src/content-page-floor.ts +0 -28
  41. package/src/csrf-protection.ts +1 -28
  42. package/src/error-page-logo.ts +2 -35
  43. package/src/expand-roster.ts +1 -25
  44. package/src/gate-ttls.ts +0 -12
  45. package/src/gpt-gate.ts +1 -19
  46. package/src/hono-error-handler.ts +0 -19
  47. package/src/install-harness.ts +1 -12
  48. package/src/install-logging.ts +1 -47
  49. package/src/load-page-manifest.ts +1 -29
  50. package/src/load-service-install-row.ts +42 -10
  51. package/src/module-not-found.ts +0 -17
  52. package/src/mount-orpc-router.ts +1 -94
  53. package/src/mount-server-packages.ts +1 -37
  54. package/src/mount.ts +1 -4
  55. package/src/mutable-app-ref.ts +0 -21
  56. package/src/orpc-contract-rows.json +289 -0
  57. package/src/orpc-openapi-document.ts +1 -19
  58. package/src/orpc-openapi-gate.ts +1 -23
  59. package/src/page-route-shell-types.ts +1 -8
  60. package/src/page-server-loader-graph.ts +0 -10
  61. package/src/page-server-orpc/server-client.ts +1 -23
  62. package/src/pg-boss-handler-registry.ts +1 -24
  63. package/src/pg-boss-mount.ts +1 -109
  64. package/src/proposal-authorized-emails.ts +0 -32
  65. package/src/proposal-gate.ts +1 -52
  66. package/src/quiz-gate.ts +1 -31
  67. package/src/read-html-template.ts +1 -49
  68. package/src/readiness-gate.ts +0 -8
  69. package/src/register-api-routes.ts +1 -11
  70. package/src/register-crash-handlers.ts +1 -6
  71. package/src/register-page-routes.ts +1 -12
  72. package/src/register-page-server-client.ts +1 -66
  73. package/src/register-registry-page-route/ai-chat-prefetch.ts +1 -24
  74. package/src/register-registry-page-route/apply-page-branding.ts +1 -100
  75. package/src/register-registry-page-route/captured-loader-config.ts +1 -12
  76. package/src/register-registry-page-route/graph-cache.ts +1 -38
  77. package/src/register-registry-page-route/live-loader-config.ts +1 -22
  78. package/src/register-registry-page-route/meta-title.ts +1 -36
  79. package/src/register-registry-page-route/page-data-shared.ts +1 -18
  80. package/src/register-registry-page-route/prefetch-org-id.ts +1 -24
  81. package/src/register-registry-page-route/proposal-approval-email-token.ts +1 -22
  82. package/src/register-registry-page-route/proposal-brand-stamping.ts +1 -31
  83. package/src/register-registry-page-route/public-shell-page-data.ts +1 -34
  84. package/src/register-registry-page-route/region-slots-page-data.ts +1 -8
  85. package/src/register-registry-page-route/resolve-page-data.ts +1 -10
  86. package/src/register-registry-page-route/resolve-panel-page.ts +1 -10
  87. package/src/register-registry-page-route/resolve-request-graph.ts +1 -68
  88. package/src/register-registry-page-route/run-content-gates.ts +3 -64
  89. package/src/register-registry-page-route/single-page-data.ts +1 -9
  90. package/src/register-registry-page-route/stamp-session-context.ts +1 -10
  91. package/src/register-registry-page-route/warn-uncovered-blocks.ts +1 -18
  92. package/src/register-registry-page-route.ts +1 -93
  93. package/src/register-static-assets.ts +1 -53
  94. package/src/register-ws-routes.ts +1 -21
  95. package/src/resolve-brand-favicon.ts +0 -22
  96. package/src/resolve-service-port.ts +1 -61
  97. package/src/resolve-theme-mode.ts +1 -30
  98. package/src/resolvers/app-header.ts +1 -20
  99. package/src/resolvers/fetch-org-brand.ts +1 -11
  100. package/src/resolvers/portal-route-paths.ts +1 -29
  101. package/src/resolvers/website-by-host-map.ts +1 -7
  102. package/src/resource-not-found.ts +0 -48
  103. package/src/run-boot-hook-list.ts +1 -13
  104. package/src/run-boot-hooks.ts +1 -19
  105. package/src/scala-tools-currency-watchdog.ts +1 -7
  106. package/src/security-headers.ts +1 -7
  107. package/src/select-service-routes.ts +1 -31
  108. package/src/server-error-page.ts +0 -18
  109. package/src/theme-fonts-link.ts +1 -21
  110. package/src/transfers/presigned-upload.ts +1 -27
  111. package/src/two-phase-full-app.ts +0 -82
  112. package/src/two-phase-minimal-app.ts +1 -111
  113. package/src/two-phase-readiness.ts +0 -6
  114. package/src/types.ts +0 -17
  115. package/src/ui-service.ts +2 -48
  116. package/src/ws-boot-self-test.ts +1 -14
  117. package/src/content-gate/gate-entry-marker.test.ts +0 -111
  118. package/src/content-gate/token.segmentation.test.ts +0 -76
  119. package/src/content-gate/token.test.ts +0 -65
  120. package/src/content-gate/token.ts +0 -219
  121. package/src/is-react-element-type.test.ts +0 -45
  122. package/src/is-react-element-type.ts +0 -20
  123. package/src/page-server-orpc/hub-mcp-client.ts +0 -20
  124. package/src/pg-boss-mount.test.ts +0 -101
  125. package/src/resource-chat-inject.page-assistant.test.ts +0 -145
  126. package/src/resource-chat-inject.test.ts +0 -240
  127. package/src/resource-chat-inject.ts +0 -374
@@ -1,28 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Loads the service's GENERATED `src/page-manifest.ts` — the codegen output of
7
- * the `page-manifest` transform, emitted from `block_schema` rows. That file is
8
- * the single artefact describing which Puck components this service renders.
9
- *
10
- * This REPLACES `discover-blocks.ts`, a hand-written runtime scan that queried
11
- * block_schema at boot and dynamic-imported each row. Deleting it is the point.
12
- * The scan was introduced to survive one dead row, and it bought that by moving
13
- * a build-time, deterministic, generated fact into a runtime path with four
14
- * independent ways to silently resolve nothing: a registry read that answers
15
- * with an empty array instead of failing, an install probe that reports "not
16
- * installed" for installed packages, a `node_modules` lookup relative to
17
- * `process.cwd()`, and a per-block try/catch that swallows each failure into a
18
- * string nobody reads when the boot dies first. Measured 2026-09-03: 91 active
19
- * block_schema rows, a generated manifest carrying 63 blocks sitting on disk,
20
- * and the scan loading ZERO — for hours, across every UI service.
21
- *
22
- * Generation is the platform's answer to exactly this (`one-generator-pattern`,
23
- * `business-logic-is-rows-plus-executors`): a bad row fails codegen loudly, at
24
- * build time, with the file in front of you. It cannot present as a service
25
- * that boots and renders nothing.
26
4
  */
27
5
 
28
6
  import { join } from "node:path";
@@ -32,12 +10,6 @@ interface PageManifestModule {
32
10
  components?: Record<string, RendererComponentEntry>;
33
11
  }
34
12
 
35
- /**
36
- * Reads the generated manifest's `components` export. Throws when the file is
37
- * absent or exports nothing — an empty manifest means codegen has not run for
38
- * this service, which is a build problem the operator must see named, not a
39
- * degraded runtime state to paper over.
40
- */
41
13
  export async function loadPageManifest(
42
14
  serviceSlug: string,
43
15
  ): Promise<Record<string, RendererComponentEntry>> {
@@ -1,11 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Loads the service_install registry row for a given service slug.
7
- * Uses registry.require() per Constitution Rule 11.
8
- * Schema is owned by @teamscala/db-validation (Rule 7).
9
4
  */
10
5
 
11
6
  import { getCache } from "@teamscala/db/registry/loader/cache/get-cache";
@@ -14,7 +9,6 @@ import {
14
9
  RegistryValidationError,
15
10
  } from "@teamscala/db/registry/require";
16
11
  import { normalizeRegistryConfigObject } from "@teamscala/db/registry/validation";
17
- import type { ServiceInstallConfig } from "@teamscala/db-validation/registry-schemas/service-install";
18
12
  import {
19
13
  enforceExplicitRenderMode,
20
14
  ServiceInstallConfigSchema,
@@ -22,8 +16,32 @@ import {
22
16
  } from "@teamscala/db-validation/registry-schemas/service-install";
23
17
  import * as v from "valibot";
24
18
 
25
- export { ServiceInstallConfigSchema } from "@teamscala/db-validation/registry-schemas/service-install";
26
- export type { ServiceInstallConfig };
19
+ const BOOT_ROW_KEYS = [
20
+ "access",
21
+ "api_routes",
22
+ "app_port",
23
+ "auth",
24
+ "block_packages",
25
+ "boot_hooks",
26
+ "brand_org",
27
+ "home_path",
28
+ "kind",
29
+ "orpc_router",
30
+ "portal_scoped",
31
+ "port",
32
+ "providers",
33
+ "render_mode",
34
+ "server_packages",
35
+ "websocket_routes",
36
+ ] as const;
37
+
38
+ export const ServiceInstallBootSchema = v.object(
39
+ v.pick(ServiceInstallConfigSchema.pipe[0], BOOT_ROW_KEYS).entries,
40
+ );
41
+
42
+ export type ServiceInstallConfig = v.InferOutput<
43
+ typeof ServiceInstallBootSchema
44
+ >;
27
45
 
28
46
  export interface LoadServiceInstallRowResult {
29
47
  success: true;
@@ -54,9 +72,23 @@ export async function loadServiceInstallRow(
54
72
  `service_install:${serviceSlug}`,
55
73
  ) as Record<string, unknown>;
56
74
  enforceExplicitRenderMode(serviceSlug, rawConfig);
57
- const config = v.parse(ServiceInstallConfigSchema, rawConfig);
75
+ const config = v.parse(ServiceInstallBootSchema, rawConfig);
58
76
  return { success: true, config };
59
77
  } catch (err) {
78
+ if (err instanceof v.ValiError) {
79
+ const issues = err.issues
80
+ .map((issue) => {
81
+ const path = v.getDotPath(issue);
82
+ return path ? `${path}: ${issue.message}` : issue.message;
83
+ })
84
+ .join("; ");
85
+ return {
86
+ success: false,
87
+ error: "validation_failed",
88
+ slug: serviceSlug,
89
+ message: `service_install row for "${serviceSlug}" has a key the boot reads missing or malformed: ${issues}`,
90
+ };
91
+ }
60
92
  if (err instanceof RegistryMissingError) {
61
93
  const sql = `INSERT INTO registry_entries (type, slug, config, is_active) VALUES ('service_install', '${serviceSlug}', '{"kind":"ui","render_mode":"public","port":null,"page_routes":[],"providers":[],"api_routes":[],"block_packages":[],"teamgbg_packages":[],"env_vars":[]}'::jsonb, true);`;
62
94
  return {
@@ -1,30 +1,13 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Classifies a failed dynamic import as "this candidate is absent" vs a real
7
- * error, for the server_package mount candidate chain. Extracted from
8
- * mount-server-packages so the classification is testable: as a closure it
9
- * could not be exercised, and it shipped wrong.
10
4
  */
11
5
 
12
- /** `@scope/name/sub/path.ts` -> `@scope/name`; `name/sub` -> `name`. */
13
6
  export function barePackageOf(specifier: string): string {
14
7
  const parts = specifier.split("/");
15
8
  return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
16
9
  }
17
10
 
18
- /**
19
- * True when the import failed because `requestedPath` itself is absent.
20
- *
21
- * Bun reports an unexported SUBPATH as `Cannot find package '<bare>'`, naming
22
- * the package rather than the subpath, so the bare name of THIS candidate
23
- * counts as not-found. Without that the first candidate's miss is
24
- * misclassified as fatal and the fallbacks never run (measured 2026-08-24:
25
- * scala-agents-ui crash-looped because cli-session exports ./boot/mount.ts and
26
- * not ./mount.ts). A transitive miss naming a DIFFERENT module still surfaces.
27
- */
28
11
  export function isModuleNotFound(err: unknown, requestedPath: string): boolean {
29
12
  const msg =
30
13
  (err as { message?: string })?.message ??
@@ -1,12 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Mounts the composed ORPC app router (generated Prisma CRUD + custom fn routers)
7
- * at /api/rpc/* over HTTP via @orpc/server/fetch's RPCHandler.
8
- * Uses getAppRouter() from @teamscala/orpc/app-router which merges generated
9
- * routers plus the row-composed fn namespace (orpc-bundle from api_route rows).
10
4
  */
11
5
 
12
6
  import { join } from "node:path";
@@ -32,22 +26,12 @@ export async function mountOrpcRouter(
32
26
  serviceSlug: string,
33
27
  ): Promise<OrpcMountResult | null> {
34
28
  const log = getAppLogger();
35
- // registerFnRouters() is retired: the row-composed bundle (orpc-bundle from
36
- // api_route rows) is the sole fn source. The handwritten registration is
37
- // fully shadowed by generated.fn (all families have rows) — keeping it
38
- // would block generated-only procedures (auth, cacheAdmin, regions,
39
- // realtime) from serving.
40
29
  const routerPath = join(
41
30
  process.cwd(),
42
31
  "prisma/generated/orpc/routers/index.ts",
43
32
  );
44
33
  const schemaPath = join(process.cwd(), "prisma/schema.prisma");
45
34
 
46
- // ORPC forward mode: when SCALA_ORPC_FORWARD_URL is set and there is no
47
- // local bundle, /api/rpc/* proxies to the central ORPC surface instead of
48
- // mounting a per-service router. This is the collapse's consolidation path —
49
- // services shed their local bundle and the central surface serves all
50
- // row-composed procedures.
51
35
  const forwardUrl = process.env.SCALA_ORPC_FORWARD_URL;
52
36
  if (!await Bun.file(routerPath).exists() && forwardUrl) {
53
37
  const base = forwardUrl.replace(/\/+$/, "");
@@ -58,7 +42,7 @@ export async function mountOrpcRouter(
58
42
  method: c.req.method,
59
43
  headers: c.req.raw.headers,
60
44
  body: c.req.raw.body,
61
- // @ts-ignore — duplex is required for streaming bodies in Bun
45
+ // @ts-ignore duplex is required for streaming bodies in Bun
62
46
  duplex: "half",
63
47
  });
64
48
  return new Response(res.body, {
@@ -73,14 +57,6 @@ export async function mountOrpcRouter(
73
57
  return { routeCount: 0 };
74
58
  }
75
59
 
76
- // No generated ORPC router is the EXPECTED state for daemon-only / boot-hook
77
- // services (session-picker, dep-sync, etc.) that own no DB schema or
78
- // api_routes — they serve no /api/rpc surface. Skip silently: importing a
79
- // nonexistent module would throw into the catch below and surface a
80
- // misleading "ORPC router not mounted" WARN on every boot. The catch is
81
- // reserved for GENUINE mount failures (router present but fails to load).
82
- // A service that SHOULD have a router but doesn't is a codegen gap caught by
83
- // API verification, not a boot warning.
84
60
  if (!await Bun.file(routerPath).exists()) {
85
61
  if (await Bun.file(schemaPath).exists()) {
86
62
  throw new Error(
@@ -97,12 +73,6 @@ export async function mountOrpcRouter(
97
73
  const router = await getAppRouter();
98
74
  if (!router || Object.keys(router).length === 0) return null;
99
75
 
100
- // The mount error hook: every procedure error crossing this boundary is
101
- // shaped by buildErrorReport (errorId, class, stack head) and recorded
102
- // durably via event-log before ORPC swallows it into a stackless 500.
103
- // The recorder is wired HERE because orpc is primitives tier and cannot
104
- // import @teamscala/event-log across a same-tier edge — the boot layer
105
- // owns that dependency.
106
76
  const { procedureErrorReportInterceptor } = await import(
107
77
  "@teamscala/orpc/procedure-error-report"
108
78
  );
@@ -120,24 +90,12 @@ export async function mountOrpcRouter(
120
90
  interceptors: [procedureErrorReportInterceptor()] as never,
121
91
  });
122
92
 
123
- // The bootloader injects the service Prisma client into ORPC's configured
124
- // primitive. Reading it through that surface keeps this mount on the same
125
- // dependency boundary as the generated procedures.
126
93
  const { getPrisma: getConfiguredPrisma } = await import(
127
94
  "@teamscala/orpc/configure"
128
95
  );
129
96
 
130
- // Coexistence (Prisma 8 migration): create the v8 chained client when
131
- // the shared platform contract ships inside @teamscala/db. Absence is
132
- // the EXPECTED state for services that have not migrated — db8 stays
133
- // undefined and v7 procedures are untouched. One boot-time creation,
134
- // closed over by the request handler (connects lazily on first query).
135
97
  let db8: Db8Client | undefined;
136
98
  try {
137
- // Named-need address (wrong-database guard, 2026-09-27): the tenant
138
- // plane by name through @teamscala/db's resolver, never an env key
139
- // read at this call site. Null = the deploy wrote nothing for this
140
- // plane, and db8 stays undefined on the existing absence path.
141
99
  const databaseUrl = resolveDbUrlOrNull("tenant");
142
100
  if (databaseUrl) {
143
101
  const { createDbClientV8 } = await import(
@@ -147,9 +105,6 @@ export async function mountOrpcRouter(
147
105
  connectionString: databaseUrl,
148
106
  serviceName: serviceSlug,
149
107
  })) as never;
150
- // Register for db-core internals (registry loaders, raw fallbacks)
151
- // routing v8-first via getDb8() — the same coexistence shape as
152
- // the RPC context injection above.
153
108
  const { registerDb8 } = await import("@teamscala/db/db8-registry");
154
109
  registerDb8(db8 as never);
155
110
  }
@@ -160,26 +115,9 @@ export async function mountOrpcRouter(
160
115
  db8 = undefined;
161
116
  }
162
117
 
163
- // Bridge the composed router into tool-executor so in-process tool
164
- // dispatch (chat agents' work_items/meetings/agreements/proposals
165
- // tools, etc.) resolves against this service's ORPC router + Prisma
166
- // client. Without this every ORPC tool call throws
167
- // "app router provider not configured". Parallel to dev-mcp-boot's
168
- // configureToolExecutor. Calls getAppRouter() fresh each time (not a
169
- // boot-time capture) so the tool-executor always resolves the CURRENT
170
- // memoized router — the tool-generator listener may regenerate the
171
- // ORPC router and call clearAppRouterCache() after boot, producing a
172
- // new router object that a captured closure would miss.
173
118
  const { configure: configureToolExecutor } = await import(
174
119
  "@teamscala/tool-executor/configure"
175
120
  );
176
- // The caller-identity and secret collaborators arrive here (the boot
177
- // layer) — tool-executor cannot import its tier-2 siblings statically
178
- // (vertical-dependency-only: a same-tier edge). Resolved eagerly at
179
- // mount so the injected factories stay synchronous. The retired
180
- // @teamscala/host-command-bus invoker injection no longer happens: the
181
- // bus is retired, and host_command tools execute through the native
182
- // in-process coding path inside tool-executor.
183
121
  const [{ getCallerContext }, { decryptSecretConfig }] =
184
122
  await Promise.all([
185
123
  import("@teamscala/mcp-multi-session/caller-context"),
@@ -192,19 +130,12 @@ export async function mountOrpcRouter(
192
130
  decryptSecretConfig: (raw) => decryptSecretConfig(raw as never),
193
131
  });
194
132
 
195
- // The fn capability primitives (foundation tier) cannot import the email
196
- // client, so the boot layer injects the real sender — the same shape the
197
- // bootloader already uses for getPrisma. Until configured, fn.workItems
198
- // notification procedures degrade to { notified: 0 } with an error log
199
- // (never throw).
200
133
  try {
201
134
  const [{ configureWorkItems }, { createEmailClient }] = await Promise.all([
202
135
  import("@teamscala/db/work-items"),
203
136
  import("@teamscala/email-client/client"),
204
137
  ]);
205
138
  configureWorkItems({
206
- // The injected contract is the template-send subset; the real
207
- // client's send agrees at every call site ({to, template, vars}).
208
139
  emailSender: createEmailClient() as never,
209
140
  logger: log,
210
141
  });
@@ -232,17 +163,7 @@ export async function mountOrpcRouter(
232
163
  id: session.user.id,
233
164
  name: session.user.name ?? undefined,
234
165
  email: session.user.email,
235
- // The active organisation lives on the SESSION
236
- // (activeOrganizationId, set at sign-in by scala-otp's
237
- // session.create hook), NOT on the user object — getSession's
238
- // ResolvedSession.user carries no organisationId. Reading the
239
- // user field left organisationId always null, so the generated
240
- // router's org-scope gate 403'd every non-super-admin write
241
- // ("Organization membership required") on the public-HTTP path.
242
166
  organisationId: session.session.activeOrganizationId ?? null,
243
- // better-auth's admin plugin issues role "admin"; the platform
244
- // also uses "super-admin"/"super_admin". All three are platform
245
- // super-admins for ORPC org-scope bypass.
246
167
  isSuperAdmin:
247
168
  role === "super_admin" ||
248
169
  role === "super-admin" ||
@@ -266,10 +187,6 @@ export async function mountOrpcRouter(
266
187
  context: { prisma, db8, headers: c.req.raw.headers, resolveUser },
267
188
  });
268
189
  if (!result?.matched) return c.notFound();
269
- // Response-side signal only (a fleet-wide 500 spike with no
270
- // traceback — measured 2026-09-04). The real error, its class and
271
- // its stack are captured by the mount error hook above; this line
272
- // stays for failures that reach the boundary without a throw.
273
190
  if (result.response.status >= 500) {
274
191
  log.error(
275
192
  `[ORPC] ${c.req.method} ${c.req.path} → ${result.response.status}`,
@@ -279,16 +196,6 @@ export async function mountOrpcRouter(
279
196
  },
280
197
  );
281
198
 
282
- // THE OpenAPI CONTRACT, served BESIDE the RPC — a GET on the same prefix,
283
- // generated from the router object mounted below, so the document and the
284
- // handler cannot describe different surfaces. Order is free: the methods
285
- // never overlap (`app.post` matches POST alone).
286
- //
287
- // Gated by openApiContractAuthorised, which refuses a proxied request as
288
- // well as a wrong key (guard auth-helpers-fail-closed), so only an
289
- // internal caller — the MCP gateway — reaches this. Its own facts come
290
- // from openApiContractOptions; both units carry the reasoning, and
291
- // contract rows state their behaviour.
292
199
  app.get("/api/rpc/openapi.json", async (c) => {
293
200
  const header = (name: string) => c.req.header(name) ?? null;
294
201
  if (
@@ -1,13 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Walks `service_install.server_packages[]`, dynamic-imports each, and calls
7
- * its `mount(ctx)` function. Server packages register handlers / boot hooks /
8
- * scheduled jobs / WS routes via the MountContext exposed in ctx; the
9
- * resulting MountRegistry is consumed by register-api-routes /
10
- * register-ws-routes / run-boot-hook-list / start-scheduled-jobs.
11
4
  */
12
5
 
13
6
  /**
@@ -96,9 +89,6 @@ export async function mountServerPackages(
96
89
  lastNotFound = err;
97
90
  continue;
98
91
  }
99
- // Real error (syntax, runtime, transitive resolution failure inside the
100
- // loaded module) — surface it directly. Swallowing here is what hides
101
- // the true cause behind a misleading "cannot find <pkg>" further down.
102
92
  throw new BootFatalError(
103
93
  "mount-server-packages",
104
94
  serviceSlug,
@@ -129,30 +119,6 @@ export async function mountServerPackages(
129
119
  }
130
120
  } catch (err) {
131
121
  if (err instanceof BootFatalError) throw err;
132
- // A PLUGIN THAT FAILS TO MOUNT IS UNAVAILABLE, NOT FATAL. `crash-isolation`
133
- // states the law directly — "a failure in one package does not crash the
134
- // host" — and names the plugin failure domain: "a broken plugin degrades
135
- // to 'that plugin unavailable'". This arm used to wrap ANY mount error in
136
- // a BootFatalError, so one plugin's missing config killed a whole service.
137
- //
138
- // MEASURED 2026-09-07: scala-agents-ui, the operator's fleet monitoring UI,
139
- // booted through db, auth, routes and every other server_package, then died
140
- // at @teamscala/opencode because OPENCODE_SERVER_URL was absent. systemd
141
- // retried five times and gave up, agents.scala.business served 502, and the
142
- // operator lost sight of his fleet — over one optional integration missing
143
- // one variable, on a page that needs no opencode connection to render.
144
- //
145
- // THE ASYMMETRY DECIDES IT. A service that boots without one plugin still
146
- // serves every route that plugin did not own, and the gap is loud in this
147
- // log line and counted in the summary below. A service that refuses to boot
148
- // serves NOTHING — including the surface that would have explained why.
149
- //
150
- // NOT-FOUND STAYS FATAL, deliberately: that arm is above and untouched. A
151
- // package that LOADS and then throws is a broken plugin, which is this
152
- // case. A package that cannot be resolved at all means the declared
153
- // dependency set was never installed — an install-integrity failure, where
154
- // booting a half-installed service would serve wrong results while looking
155
- // healthy.
156
122
  degraded.push(pkg);
157
123
  log.error(
158
124
  `[scala-install:${serviceSlug}] server_package "${pkg}" UNAVAILABLE — mount failed; ` +
@@ -165,8 +131,6 @@ export async function mountServerPackages(
165
131
  `[scala-install:${serviceSlug}] Registered ${mountRegistry.handlers.size} handler(s), ` +
166
132
  `${mountRegistry.wsRoutes.size} ws route(s), ` +
167
133
  `${mountRegistry.bootHooks.size} boot hook(s)` +
168
- // NAMED, NEVER COUNTED. A bare "1 degraded" sends the reader hunting for
169
- // which one; the name is the whole value of the line.
170
134
  (degraded.length > 0
171
135
  ? `, ${degraded.length} server_package(s) UNAVAILABLE: ${degraded.join(", ")}`
172
136
  : ""),
package/src/mount.ts CHANGED
@@ -1,9 +1,6 @@
1
1
  /**
2
- * @system server-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Server-package mount for opt-in service-boot watchdogs.
7
4
  */
8
5
 
9
6
  import type { MountContext } from "@teamscala/os/runtime-contracts/mount-context";
@@ -1,34 +1,17 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Mutable app reference for two-phase boot. Allows swapping the Hono app
7
- * atomically after building the full app in the background. The initial app
8
- * is a minimal tool-only surface (ORPC, data routes) that starts serving
9
- * immediately; the full app (pages, hydration) is built off-to-the-side and
10
- * swapped in once complete.
11
- *
12
- * Thread-safety: Bun is single-threaded, so a simple module-level variable
13
- * is sufficient. Atomics are not needed.
14
4
  */
15
5
 
16
6
  import type { Hono } from "hono";
17
7
  let currentApp: Hono | null = null;
18
8
  let appLabel = "initial";
19
9
 
20
- /**
21
- * Create a new mutable app reference initialized with the provided app.
22
- * This should be called once at boot time.
23
- */
24
10
  export function initMutableAppRef(initialApp: Hono, label: string): void {
25
11
  currentApp = initialApp;
26
12
  appLabel = label;
27
13
  }
28
14
 
29
- /**
30
- * Get the current app. Throws if the ref hasn't been initialized.
31
- */
32
15
  export function getApp(): Hono {
33
16
  if (!currentApp) {
34
17
  throw new Error("Mutable app reference not initialized");
@@ -36,10 +19,6 @@ export function getApp(): Hono {
36
19
  return currentApp;
37
20
  }
38
21
 
39
- /**
40
- * Atomically swap the app to a new instance. This is called after the
41
- * full app is built in the background. Returns the previous label for logging.
42
- */
43
22
  export function swapApp(newApp: Hono, newLabel: string): string {
44
23
  const oldLabel = appLabel;
45
24
  currentApp = newApp;