@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,43 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The two SWR loader-graph caches (static-shell + parameterised routes) for the
7
- * registry page registrar, plus readCachedGraph — the testable stale-while-
8
- * revalidate read. Extracted from register-registry-page-route.ts so the SWR
9
- * decision is unit-testable independent of the Hono/DB/graph machinery.
10
- *
11
- * The SWR contract rides @teamscala/cache directly (the hand-rolled `at` timestamp,
12
- * FRESH_MS window, and graphRefreshInflight single-flight Set are gone): each
13
- * cache is configured ttlMs = the fresh window (serve, no refresh) + staleTtlMs =
14
- * the rest (serve stale + fire ONE background refresh). readCachedGraph uses
15
- * getWithMeta for the fresh/stale/miss decision and cache.refreshInBackground
16
- * for the fire-and-forget refresh — deduped against the cache's own in-flight map
17
- * (so delete/clear/disable drop its flights, and a refresh in flight across an
18
- * invalidation can no longer resurrect the dropped entry).
19
- *
20
- * Graphs deep-clone on BOTH set and get at the caller (registerRegistryPageRoute):
21
- * gateAgreementContent + downstream block resolution MUTATE the object in place,
22
- * so handing a cached tree out twice would ship request A's resolved data to
23
- * request B (ssr-first-tick-parity). Clone-on-get stays local to the caller.
24
- */
25
- /**
26
- * Stale-while-revalidate read of a cached loader graph.
27
- *
28
- * - Fresh hit (within ttlMs): return the cached graph, no refresh.
29
- * - Stale hit (within ttlMs + staleTtlMs): return the cached graph AND fire ONE
30
- * background refresh via cache.refreshInBackground — single-flight (deduped
31
- * against the cache's in-flight map), never rejects, leaves the stale entry in
32
- * place on failure. The caller deep-clones the returned value before use.
33
- * - Miss (past staleTtlMs, or never cached): return undefined — the caller
34
- * resolves synchronously and cache.set()s the clone.
35
- *
36
- * The refresh callback must return a graph safe to store. To SKIP caching a
37
- * refresh result (e.g. a primary-missing / dead-link graph that must stay
38
- * per-request so a just-created record appears immediately), the callback THROWS
39
- * — refreshInBackground catches it, leaves the stale entry, and logs one line
40
- * (an observable dead-link signal, not a hard error).
41
4
  */
42
5
 
43
6
  import { createCache } from "@teamscala/cache/create-cache";
@@ -1,27 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * live-loader-config.ts — the per-request resource_loader read.
7
- *
8
- * The constitution's deploy-time bake list (installer-and-runtime) names
9
- * service_install, service_manifest, block_schema, page_route, api_route and
10
- * scheduled_job — resource_loader is NOT among it, so a loader row is runtime
11
- * data: a registry edit must reach the running service on a subsequent request
12
- * without a restart. This module is that contract: `readLiveLoaderConfig`
13
- * re-reads the row through `loadRegistryConfig` on EVERY request (its SWR
14
- * cache makes the fresh-window hit a memory read, not a DB round trip) and
15
- * FAILS CLOSED — a row that vanished or stopped validating mid-flight throws
16
- * into the branded error path rather than silently serving the boot-time
17
- * snapshot of a row that no longer exists.
18
- *
19
- * `fingerprintLoaderConfig` gives the per-request graph cache a key component
20
- * derived from the LIVE config, so a row edit invalidates cached graphs for
21
- * that route immediately (the graph cache keys on route+host+path alone would
22
- * otherwise serve a stale graph until its SWR window lapsed — right cache,
23
- * wrong key: the graph is a function of the config, so the config belongs in
24
- * the key).
25
4
  */
26
5
 
27
6
  import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
@@ -1,36 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Meta/title helpers for the route registrar: interpolate the route's
7
- * meta_context against the resolved graph. Extracted from
8
- * register-registry-page-route.ts.
9
- *
10
- * resolveTemplateTitle (the page_meta_template title reader) was DELETED
11
- * 2026-08-17: it only ever applied when NO resource entity resolved, and on a
12
- * resource page an entity ALWAYS resolves — the curated template was discarded
13
- * by construction, dead on exactly the pages the type exists for (Valencia's
14
- * finding). composePageTitle (+ the org-prefix flag) owns titles end to end.
15
- */
16
- /**
17
- * THE ONE SUBJECT DERIVATION. What a page is ABOUT — the entity whose name the
18
- * browser tab should carry.
19
- *
20
- * This existed as THREE hand-written `||` chains, one per render branch in
21
- * resolve-page-data, and they had already diverged: two ordered form before
22
- * proposal, the third ordered proposal first. So which resource named the page
23
- * depended on which branch happened to render it, silently.
24
- *
25
- * The divergence is the cheap half of the cost. The expensive half is that a
26
- * correction has to be applied three times to work, and applying it twice looks
27
- * exactly like applying it correctly — measured 2026-08-17, when a fix to the
28
- * quiz subject was shipped, installed and verified present in the bundle while
29
- * the page kept its old title, because the branch that rendered it held the
30
- * second copy.
31
- *
32
- * One chain, one precedence, one place to change. Adding a resource type is an
33
- * edit here rather than a fourth branch in three files.
34
4
  */
35
5
 
36
6
  import { interpolateMeta } from "@teamscala/resolver-engine/loader-config/resolve-from-loader-config";
@@ -43,11 +13,6 @@ export function buildMetaFromContext(
43
13
  return interpolateMeta(metaContext as Record<string, unknown>, resolved);
44
14
  }
45
15
 
46
- /**
47
- * The page's own meta_title row (loaderData.page first, resolved.page second)
48
- * — used only on /sign-in, where the org's sign-in page title overrides the
49
- * derived one. Moved from register-registry-page-route.ts.
50
- */
51
16
  export function resolvePageMetaTitle(
52
17
  resolved: Record<string, unknown>,
53
18
  ): unknown {
@@ -1,27 +1,11 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Shared Puck-page-data helpers for the resolved-graph → pageData conversion:
7
- * the PuckData alias of the canonical boundary Data, JSON extraction with an
8
- * honest null on
9
- * malformed input, layout-default application, region root-prop picking, the
10
- * RegionEntry shape, and the unresolved-region-slot warn. Plus the brand-token
11
- * prologue shared by all three render branches.
12
- *
13
- * Split out of resolve-page-data.ts (547 lines) — byte-for-byte moves; only
14
- * closure variables became explicit parameters.
15
4
  */
16
5
 
17
6
  import { getAppLogger } from "@teamscala/logger/app-loggers";
18
7
  import type { Data } from "@teamscala/puck-boundary/data";
19
8
 
20
- /**
21
- * The canonical identity from the generated boundary (the platform's one
22
- * declaration site for framework shapes) — never a hand-spelled structural
23
- * duplicate of it.
24
- */
25
9
  export type PuckData = Data;
26
10
 
27
11
  export const EMPTY_PUCK_DATA: PuckData = { root: { props: {} }, content: [] };
@@ -97,7 +81,6 @@ export function warnUnresolvedRegionSlot(slot: string): void {
97
81
  );
98
82
  }
99
83
 
100
- /** The resolved brand tokens every branch reads (the old file's shared prologue). */
101
84
  export function readBrandTokens(resolved: Record<string, unknown>): {
102
85
  brand: Record<string, unknown>;
103
86
  primaryColor: string;
@@ -1,31 +1,8 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Resolves the organisation id for the background AI-chat session prefetch
7
- * from the request path. A canonical portal URL is /<websiteId>/<orgId>/
8
- * <path> — the org is segment 1, NOT segment 0. Moved from
9
- * register-registry-page-route.ts.
10
4
  */
11
5
 
12
- /**
13
- * Prefer segment 1; fall back to segment 0 only when segment 1 is not a
14
- * UUID (the single-UUID shape, which is live and supported — NOT
15
- * transitional). The fallback is deliberately kept
16
- * UNCHANGED-BEHAVIOUR rather than "fixed": a UUID in segment 0 is
17
- * genuinely ambiguous — of the 40 UUID-first page paths in builda_pages,
18
- * ZERO are websites and ZERO are organisations, so segment 0 can also be
19
- * an unrelated record id. Position alone cannot resolve that; only
20
- * validating the id against organisation_profile can, and that is a DB
21
- * read this hot prefetch path should not take without caching
22
- * (`hot-path-resolutions-are-cached`). Preferring segment 1 is strictly
23
- * better for the canonical shape and no worse for any other. Reading
24
- * segment 0 first captured the WEBSITE id and passed it as an
25
- * organisation id, which failed silently because both are well-formed
26
- * UUIDs: the prefetch simply warmed a session against the wrong tenant
27
- * (found 2026-07-21).
28
- */
29
6
 
30
7
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
31
8
 
@@ -1,27 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Resolve the `{{approvalEmail}}` token on a proposal page, after pageData is
7
- * built.
8
- *
9
- * Split from proposal-post-processors.ts, which single-purpose-file correctly
10
- * flagged as two unrelated export groups. This unit is an async ORPC lookup
11
- * that rewrites button props; its former co-tenants are synchronous brand
12
- * stamping that touch no network. Sharing a file made an I/O-bound resolver
13
- * look like a pure formatter.
14
- */
15
- /**
16
- * Resolve the `{{approvalEmail}}` placeholder in a proposal page's button props
17
- * (e.g. the Approve CTA) in place. Routing: the organisation's registered
18
- * company email FIRST (organisation_profile.contact_email, already resolved
19
- * into pageData.brand.contactEmail); the proposal creator's user email only as
20
- * a fallback when the org has none — so a cloned proposal can never carry a
21
- * stale hard-coded address. The creator lookup is null-safe and adds no user
22
- * exposure: it resolves only where the in-process ORPC client exposes `user`
23
- * (super-admin), degrading to the org email elsewhere. Called only for proposal
24
- * pages; other registry pages are untouched.
25
4
  */
26
5
 
27
6
  import { getServerClient } from "../page-server-orpc/server-client.ts";
@@ -1,32 +1,8 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Stamp partner and client brand assets onto a proposal page's pageData, after
7
- * it is built.
8
- * Split from proposal-post-processors.ts (single-purpose-file: two unrelated
9
- * export groups). Both helpers here are synchronous, take an already-resolved
10
- * loader-graph org, and only map a logo/colour onto pageData — no network, no
11
- * ORPC. The approval-email token resolver they used to share a file with is an
12
- * async ORPC lookup, which is the distinction that was invisible while they sat
13
- * together.
14
4
  */
15
5
 
16
- /**
17
- * Stash a proposal's connected partner logo/colour on `pageData` from the
18
- * `partnerOrg` loader source (`crm_organisations` loaded by the proposal's
19
- * `partner_id`, via the SAME proven ORPC loader path that loads the proposal
20
- * itself). resolve-page-brand (service-runtime) prefers `pageData.partnerLogo`
21
- * over any inline body-Hero prop and fans it into the shared `/header` so a
22
- * `source: "partner"` OrgLogo fills automatically — the partner logo lives on
23
- * the CRM record, never duplicated per proposal. No partner → partnerOrg is
24
- * null → nothing set → the partner logo block renders blank. Reading the
25
- * already-resolved loader value (not a second hand-rolled getServerClient call)
26
- * is what makes this reliable: the earlier direct `crm_organisations.findFirst`
27
- * with a Prisma-style object `select` resolved intermittently/never against the
28
- * generated ORPC, leaving the header partner blank (2026-06-12).
29
- */
30
6
  export function applyPartnerBrand(
31
7
  pageData: Record<string, unknown>,
32
8
  partnerOrg:
@@ -40,12 +16,6 @@ export function applyPartnerBrand(
40
16
  pageData.partnerBrandColor = partnerOrg.brand_primary_color;
41
17
  }
42
18
 
43
- /**
44
- * Auto-resolve the client's company logo from the `clientOrg` loader source
45
- * (contact_id → company_id → crm_organisations.logo_url). Injects into the
46
- * Hero block's clientLogo prop when it's empty — no manual logo URL needed
47
- * per proposal when the contact's company profile has a logo.
48
- */
49
19
  export function applyClientLogo(
50
20
  pageData: Record<string, unknown>,
51
21
  clientOrg:
@@ -1,13 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Branch 2 of the resolved-graph → pageData conversion: the public shell
7
- * (header/body/footer regions from header/footer sources).
8
- *
9
- * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
10
- * the closure variables became explicit parameters.
11
4
  */
12
5
 
13
6
  import { resolveThemeMode } from "../resolve-theme-mode.ts";
@@ -95,12 +88,6 @@ export function publicShellPageData(
95
88
  const overrideHeading = pickRootProp<string>(regions, "headingFont");
96
89
  const overrideBody = pickRootProp<string>(regions, "bodyFont");
97
90
 
98
- // The page's own screenMode, threaded the same way Branch 1 threads it
99
- // (single-page-data.ts). Dropping it here is not neutral: build-page-tree
100
- // falls back to `fullScreen: renderMode === "public" || "gpts"`, and for a
101
- // shell page that resolves false — which wraps the body in a centred card
102
- // and puts the site header INSIDE that card instead of across the top of
103
- // the page. A page that declares "full-screen" was rendering as a form.
104
91
  const screenMode = pickRootProp<string>(regions, "screenMode");
105
92
 
106
93
  const pageName = (page.name as string) || null;
@@ -115,22 +102,11 @@ export function publicShellPageData(
115
102
  return {
116
103
  puckData: { root: { props: {} }, content: [] } as PuckData,
117
104
  regions,
118
- // Region-shell blocks (e.g. the header Navigation deriving the body's
119
- // document sections) read the resolved entity graph from
120
- // pageData.loaderData — the same surface Branch 1 exposes. Without it a
121
- // header/footer block serverLoader gets ctx.loaderData=undefined and
122
- // cannot see the body page, so a server-derived nav produced no items
123
- // and the menu was client-only (vanished on stale tabs / narrow views).
124
105
  loaderData: resolved,
125
106
  website: {
126
107
  slug: website.slug as string,
127
108
  name: website.name as string,
128
109
  organisation_id: website.organisation_id as string | null,
129
- // header_settings carries the DB-driven header config (e.g.
130
- // backdropBlur) the public shell applies — PublicTemplate reads
131
- // pageData.website.header_settings.backdropBlur. Dropping it here
132
- // silently kills the header blur even though the DB switch is set
133
- // (the 2026-06-17 regression). Pass it through verbatim.
134
110
  header_settings:
135
111
  (website.header_settings as Record<string, unknown> | null) ?? null,
136
112
  },
@@ -139,10 +115,6 @@ export function publicShellPageData(
139
115
  null,
140
116
  brand: {
141
117
  primaryColor: overrideColor ?? primaryColor,
142
- // Thread the org brand's dark surface (brand_background_color /
143
- // brand_dark_color) when the page sets no explicit override — so
144
- // dark-mode pages use the brand's real dark colour, not a
145
- // primary-derived tint. Uses the org brand's real dark colour.
146
118
  backgroundColor:
147
119
  overrideBg ?? (brand.backgroundColor as string | null) ?? null,
148
120
  lightSurface: (brand.lightSurface as string | null) ?? null,
@@ -166,11 +138,6 @@ export function publicShellPageData(
166
138
  pageMetaTitle: bodyMetaTitle,
167
139
  templateTitle: (meta.title as string) || undefined,
168
140
  websiteName: (website.name as string) || undefined,
169
- // The FRIENDLY name is what the public sees — the trading brand,
170
- // not the legal entity. Reading companyName here put "GMG APAC"
171
- // in the title of every launcha page, which the operator has
172
- // corrected repeatedly; the friendly name is "launcha". Falls
173
- // back to companyName only where no friendly name is set.
174
141
  organisationName:
175
142
  (brand.companyFriendlyName as string) ||
176
143
  (brand.companyName as string) ||
@@ -1,13 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Branch 1 of the resolved-graph → pageData conversion: arbitrary region-slots
7
- * (portal/agents/docs) driven by resource_loader.region_slots.
8
- *
9
- * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
10
- * the closure variables became explicit parameters.
11
4
  */
12
5
 
13
6
  import { resolveThemeMode } from "../resolve-theme-mode.ts";
@@ -1,15 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Converts the resolved loader-graph into the pageData shape the renderer
7
- * consumes — three branches: arbitrary region-slots (portal/utility), the
8
- * header/body/footer public shell, and the single-page fallback. Extracted
9
- * from register-registry-page-route.ts (single-purpose-file split); the three
10
- * branches + shared helpers were split out of THIS file at 547 lines
11
- * (region-slots-page-data.ts, public-shell-page-data.ts, single-page-data.ts,
12
- * page-data-shared.ts).
13
4
  */
14
5
 
15
6
  import { regionSlotsPageData } from "./region-slots-page-data.ts";
@@ -1,15 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * ?panel=<slug> resolution: reads the panel_view row, follows its
7
- * content.page_slug to the builda_pages row, and stashes the panel page
8
- * data + active panel slug on pageData.
9
- *
10
- * Split out of register-registry-page-route.ts (924 lines) — the loadData
11
- * closure segment moved byte-identical; only the closure variables became
12
- * explicit parameters.
13
4
  */
14
5
 
15
6
  import { getServerClient } from "../page-server-orpc/server-client.ts";
@@ -1,16 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Per-request loader-graph resolution for a registered page route: SWR cache
7
- * read (static-shell vs param-route cache), clone-on-get, defensive clone
8
- * with logged fallbacks, primary-resource-missing dead-link 404s, and the
9
- * content-page floor.
10
- *
11
- * Split out of register-registry-page-route.ts (924 lines) — the loadData
12
- * closure segments moved byte-identical; only the closure variables became
13
- * explicit parameters.
14
4
  */
15
5
 
16
6
  import type { PageRouteConfig } from "../register-registry-page-route.ts";
@@ -71,8 +61,6 @@ function cloneForCache(
71
61
  `[scala-install:${serviceSlug}] graph-cache structuredClone failed for ${graphKey} — falling back to JSON-clone (non-cloneable value in loader graph): ${err instanceof Error ? err.message : err}`,
72
62
  );
73
63
  try {
74
- // JSON-clone preserves the data graph, dropping the functions and
75
- // undefined a loader graph should not be carrying anyway.
76
64
  return JSON.parse(JSON.stringify(graph)) as unknown;
77
65
  } catch (err2) {
78
66
  log.warn(
@@ -88,19 +76,7 @@ export async function resolveRequestGraph(
88
76
  ): Promise<Record<string, unknown>> {
89
77
  const { pattern, routeSlug, routeConfig, loaderConfig, params, isStaticShell, serviceSlug, configFingerprint } = args;
90
78
 
91
- // A bare root ("/") that declares a resource_type is a UUID-driven
92
- // public site (proposals/forms/quiz) with no resource selected — it
93
- // must render the branded holding/not-found page ("no <resource> at
94
- // this link"), never fall through the shared public loader to the
95
- // marketing site. PrimaryResourceMissingError is the existing signal
96
- // the page binder maps to buildResourceNotFoundResponse. A "/" route
97
- // WITHOUT a resource_type (e.g. scala-public's marketing root) is
98
- // unaffected and still renders its content.
99
79
  if (pattern === "/" && routeConfig.resource_type) {
100
- // Status 200: the bare root is a valid "no <resource> selected"
101
- // landing, not an error — and kind:ui services must answer GET /
102
- // with 200 (verify-prod-persistent-slot). An invalid /:id keeps
103
- // the default 404 (genuinely not found).
104
80
  throw new PrimaryResourceMissingError(routeConfig.resource_type, 200);
105
81
  }
106
82
  const graphKey = `graph:${routeSlug}:${params.host}:${params.path}${configFingerprint ? `:${configFingerprint}` : ""}`;
@@ -121,13 +97,6 @@ export async function resolveRequestGraph(
121
97
  >;
122
98
  };
123
99
  let resolved: Record<string, unknown>;
124
- // SWR read over @teamscala/cache: fresh hit serves the cached graph, stale
125
- // hit serves it AND fires ONE background refresh (refreshInBackground,
126
- // single-flight via the cache's in-flight map), miss returns undefined.
127
- // The refresh callback re-resolves + stores a clone; a primary-missing
128
- // (dead-link) graph THROWS so the cache is not updated — a dead link must
129
- // stay per-request so a just-created record appears immediately
130
- // (refreshInBackground catches, leaves the stale entry, logs one line).
131
100
  const cachedGraph = readCachedGraph(
132
101
  graphCache,
133
102
  graphKey,
@@ -143,16 +112,6 @@ export async function resolveRequestGraph(
143
112
  `primary resource "${primaryType}" missing — skip cache update for ${graphKey}`,
144
113
  );
145
114
  }
146
- // SAME DEFENSIVE CLONE AS THE COLD PATH. This used to call
147
- // structuredClone bare, so a non-cloneable value in the loader
148
- // graph threw on EVERY background refresh: refreshInBackground
149
- // caught it, kept the stale entry, and logged one line — so the
150
- // cache could never update, expired at staleTtlMs, and the next
151
- // request re-resolved the whole graph in-request. Measured
152
- // 2026-09-03 on the live proposal page: "The object can not be
153
- // cloned" every ~13 minutes on the same graphKey, indefinitely.
154
- // The cold path was hardened for exactly this and the refresh
155
- // path was not — one defect, two clone sites, one protected.
156
115
  const refreshed = cloneForCache(freshGraph, graphKey, serviceSlug);
157
116
  if (refreshed === undefined) {
158
117
  throw new Error(
@@ -164,47 +123,21 @@ export async function resolveRequestGraph(
164
123
  ),
165
124
  );
166
125
  if (cachedGraph !== undefined) {
167
- // Clone-on-get: gateAgreementContent + block resolution MUTATE the
168
- // graph in place, so each request gets its own copy.
169
126
  resolved = structuredClone(cachedGraph) as Record<string, unknown>;
170
127
  } else {
171
128
  resolved = await resolveFromLoaderConfig(typedLoaderConfig, params);
172
129
  const primaryMissing =
173
130
  hasPrimarySource && primaryType && resolved[primaryType] == null;
174
131
  if (!primaryMissing) {
175
- // Defensive clone: structuredClone throws on non-cloneable loader
176
- // graphs (functions/Proxies/Prisma instances). A swallowed throw
177
- // here used to SILENTLY disable the graph cache for this key
178
- // (fail-open-silent — forbidden by protective-layers-fail-explicitly),
179
- // which left affected routes re-resolving the loader graph every
180
- // request (measured ~1.2s on the projects utility shell). JSON-clone
181
- // preserves the data graph (dropping functions/undefined a loader
182
- // graph shouldn't carry) so the cache stays effective; both fallbacks
183
- // are LOGGED so the offending non-cloneable source is observable +
184
- // fixable, never silently disabling the cache again.
185
132
  const cloned = cloneForCache(resolved, graphKey, serviceSlug);
186
133
  if (cloned !== undefined) {
187
134
  graphCache.set(graphKey, cloned);
188
135
  }
189
136
  }
190
137
  }
191
- // A route that declares resource_type is meaningless without that
192
- // entity: a dead link (wrong id, deleted row) must 404 with the
193
- // branded not-found page — never fall through to default content
194
- // (the "Vision Toolbar" leak, operator-observed 2026-06-05).
195
138
  if (hasPrimarySource && primaryType && resolved[primaryType] == null) {
196
139
  throw new PrimaryResourceMissingError(primaryType);
197
140
  }
198
- // A CONTENT route (no resource_type) whose loader resolves a `page`
199
- // body source must not synthesize an empty page for an unmatched path
200
- // and serve a cacheable 200 — the generalized "Vision Toolbar" leak
201
- // (operator-reported 5×: a deleted page, e.g. /roadmap, returned 200
202
- // with an empty body instead of not-found). No page row → dead link →
203
- // branded 404 floor. A page row that exists but has zero blocks → a
204
- // content gap → the floor's empty-page surface (200). Both carry the
205
- // "Take me home" action. The pure decision lives in content-page-floor.ts
206
- // (unit-tested without the Hono/DB/graph machinery). Region-slot
207
- // (utility/portal) shells without a `page` source are untouched.
208
141
  const contentFloorError = decideContentPageFloor({
209
142
  hasResourceType: !!primaryType,
210
143
  hasPageContentSource: !!(loaderConfig as { sources?: { key: string }[] }).sources?.some(
@@ -1,78 +1,17 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Runs every registered content gate in catalog order (sequential, never
7
- * parallel — gates depend on prior gates' graph mutations) and applies the
8
- * page's declared assistant where authorized.
9
- *
10
- * Split out of register-registry-page-route.ts (924 lines) — the loadData
11
- * closure segment moved byte-identical; only the closure variables became
12
- * explicit parameters.
13
4
  */
14
5
 
15
- import type { PageRouteConfig } from "../register-registry-page-route.ts";
16
6
  import { runContentGate } from "../content-gate/gate.ts";
17
- import type { ContentGateOutcome } from "../content-gate/gate.ts";
18
- import { CONTENT_GATES, withholdsContent } from "../content-gate/catalog.ts";
19
- import { injectResourceChatLauncher } from "../resource-chat-inject.ts";
7
+ import { CONTENT_GATES } from "../content-gate/catalog.ts";
20
8
 
21
9
  export async function runContentGatesForPage(
22
10
  resolved: Record<string, unknown>,
23
11
  gateCookie: string | null,
24
12
  gateNow: number,
25
- assistant: PageRouteConfig["assistant"],
26
- regionSlots: string[] | undefined,
27
13
  ): Promise<void> {
28
- // EVERY REGISTERED GATE, IN CATALOG ORDER (content-gate/catalog.ts).
29
- // This used to name proposalGateConfig, agreementGateConfig and
30
- // gptGateConfig by hand, so a fourth gated resource type meant editing
31
- // this file AND the second hand-written list in two-phase-boot.ts.
32
- //
33
- // SEQUENTIAL, NEVER PARALLEL. An authorized proposal viewer has their
34
- // gate block removed from the resolved graph, and that removal is what
35
- // makes the agreement gate find no resource and no-op on a page that
36
- // carries both. Awaiting these together would race that dependency.
37
- const gateOutcomes: ContentGateOutcome[] = [];
38
14
  for (const entry of CONTENT_GATES) {
39
- const outcome = await runContentGate(
40
- resolved,
41
- gateCookie,
42
- gateNow,
43
- entry.gate,
44
- );
45
- // A seed-only gate (open capture — the public GPT identity gate)
46
- // withholds nothing, so its verdict is not evidence about whether
47
- // this viewer may see the page and must not reach the assistant
48
- // decision below. See withholdsContent, which derives that from the
49
- // access config rather than restating it here.
50
- if (withholdsContent(entry)) gateOutcomes.push(outcome);
51
- }
52
-
53
- // THE ASSISTANT IS A ROW, AND THIS IS THE ONE PLACE IT IS APPLIED. It used to be a call inside each gate's onAuthorized plus an entry
54
- // in the hardcoded gate sequence above, so a third resource type meant editing this file. Now any page declaring `assistant` gets
55
- // one and no page without the key does.
56
- //
57
- // GATED PAGES ONLY WHEN AUTHORIZED. The launcher's runtime context is
58
- // the whole document serialised, so injecting past a closed gate
59
- // would hand withheld bytes to the chat. A page no gate governs is
60
- // public and gets the assistant unconditionally -- that is a
61
- // different state from an unauthorized viewer, which is why the gate
62
- // reports `applies` separately from `authorized`.
63
- if (assistant) {
64
- const governing = gateOutcomes.filter((outcome) => outcome.applies);
65
- const authorized = governing.find((outcome) => outcome.authorized);
66
- if (governing.length === 0 || authorized) {
67
- await injectResourceChatLauncher(resolved, authorized?.email ?? "", {
68
- resourceKey: assistant.resource_key,
69
- agentConfigSlug: assistant.agent_config_slug as never,
70
- launchLabel: assistant.launch_label,
71
- placeholder: assistant.placeholder,
72
- // From the loader row, so the injector never has to guess
73
- // which resolved keys hold rendered content.
74
- regionSlots,
75
- });
76
- }
15
+ await runContentGate(resolved, gateCookie, gateNow, entry.gate);
77
16
  }
78
17
  }
@@ -1,14 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Branch 3 of the resolved-graph → pageData conversion: the single-page
7
- * fallback (form/quiz/proposal/gpt pages with no region slots and no
8
- * header/footer sources).
9
- *
10
- * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
11
- * the closure variables became explicit parameters.
12
4
  */
13
5
 
14
6
  import { resolveThemeMode } from "../resolve-theme-mode.ts";
@@ -1,15 +1,6 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Stamps the resolved-in-progress loader graph with the request's session
7
- * identity: sessionVisitor, portalContext (user/orgId/pane widths from the
8
- * portal cookie and the user's saved preferences). Anonymous when no session.
9
- *
10
- * Split out of register-registry-page-route.ts (924 lines) — the loadData
11
- * closure segment moved byte-identical; only the closure variables became
12
- * explicit parameters.
13
4
  */
14
5
 
15
6
  import { getServerClient } from "../page-server-orpc/server-client.ts";