@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,32 +1,12 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Tailwind source-candidate scanning for the client CSS build.
7
- *
8
- * Split out of build-client-assets.ts (1221 lines, six concerns). Owns ONE
9
- * concern: turning the resolved `@source` directories into the candidate token
10
- * set Tailwind's `build()` consumes.
11
- *
12
- * Why it exists at all, preserved from the original: Tailwind v4's low-level
13
- * `compile()` does NOT scan content, and @tailwindcss/oxide's native Scanner is
14
- * not a dependency of the bare `tailwindcss` lib — only of the integration
15
- * packages. So the tokens are extracted here. Over-extraction is safe, because
16
- * `build()` ignores any token that is not a valid utility. Dropping this scan is
17
- * what shipped an unstyled page to the operator's tablet on 2026-06-13; it is
18
- * enforced by `css-build-scans-source-candidates`.
19
- *
20
- * Byte-for-byte move — imports adjusted, nothing else.
21
4
  */
22
5
 
23
6
  import { resolve } from "node:path";
24
7
  import { stat } from "node:fs/promises";
25
8
  import { getAppLogger } from "@teamscala/logger/app-loggers";
26
9
 
27
- // Directory-capable existence probe. Bun.file().exists() answers false for
28
- // directories, so the @source roots (directories by construction) were all
29
- // "absent" and the candidate scan returned nothing.
30
10
  async function existsStat(path: string): Promise<boolean> {
31
11
  try {
32
12
  await stat(path);
@@ -36,16 +16,6 @@ async function existsStat(path: string): Promise<boolean> {
36
16
  }
37
17
  }
38
18
 
39
- // Tailwind v4's low-level `compile()` does NOT scan content — `compiler.build( candidates)` only emits utilities for the candidate class names it is GIVEN.
40
- // `build([])` therefore emits ZERO content-driven utilities (only the static theme/base + @utility/@layer + imported package CSS). The workspace's own
41
- // blocks mostly use inline styles + hand-written `blk-*` package classes (no-tailwind-in-blocks / layout-properties-inline), so the missing utility
42
- // layer was invisible — until the Plate document editor (third-party shadcn components using raw `flex`/`p-1`/`bg-background/95`) rendered completely
43
- // unstyled (operator's tablet, 2026-06-13). The CLI we replaced (`b29e369` "in-process Bun compile") scanned content; the replacement dropped
44
- // the scan. This restores it: glob every resolved `@source` dir, extract candidate tokens, and feed them to `build()`. @tailwindcss/oxide's native
45
- // Scanner is NOT a dependency of the bare `tailwindcss` lib (only of the
46
- // integration packages), so we tokenize ourselves — over-extraction is safe
47
- // (build() ignores any token that isn't a valid utility). Enforced by
48
- // `css-build-scans-source-candidates`.
49
19
  const SCAN_EXTS = new Set([
50
20
  ".ts",
51
21
  ".tsx",
@@ -57,9 +27,6 @@ const SCAN_EXTS = new Set([
57
27
  ".md",
58
28
  ".mdx",
59
29
  ]);
60
- // Class-name candidate chars: identifiers, variant `:`, opacity `/`, arbitrary
61
- // values `[]()`, decimals `.`, important `!`, plus `#%,&-` seen in arbitrary
62
- // values and modifiers. Deliberately broad — invalid tokens are dropped by build().
63
30
  const CANDIDATE_RE = /[A-Za-z0-9_:/[\].%#!,&-]+/g;
64
31
 
65
32
  export async function scanSourceCandidates(
@@ -75,23 +42,7 @@ export async function scanSourceCandidates(
75
42
  } catch {
76
43
  continue;
77
44
  }
78
- // A resolved @source root absent on disk is benign: a published block
79
- // package ships compiled output, not its src/ tree, so an `@source` into
80
- // that package resolves to a non-existent dir. Skip silently rather than
81
- // warn on every UI-service boot (the log-flood class — the ENOENT whose
82
- // path rendered with a trailing null byte). Real errors below still warn.
83
- // The probe MUST be a stat, never Bun.file(root).exists(): root is a
84
- // DIRECTORY and Bun.file() on a directory reports exists() === false
85
- // (the same defect discover-blocks.ts measured 2026-09-01). That probe
86
- // skipped EVERY source, collapsed the candidate set to zero, and
87
- // `build([])` stripped every Tailwind utility from every kind:ui
88
- // service's stylesheet at once — the operator's "the css is totally
89
- // fucked" with a stylesheet that loads and 15k package rules intact.
90
45
  if (!await existsStat(root)) continue;
91
- // A root INSIDE node_modules is an explicit `@source` into an installed
92
- // block package (the app-css transform emits one per package) — scan it
93
- // fully. A root OUTSIDE node_modules is the service's own tree — scan it
94
- // but never descend into node_modules (the entire dependency graph).
95
46
  const rootInNodeModules = root.includes("/node_modules/");
96
47
  try {
97
48
  const glob = new Bun.Glob("**/*");
@@ -114,8 +65,6 @@ export async function scanSourceCandidates(
114
65
  if (matched) for (const t of matched) candidates.add(t);
115
66
  }
116
67
  } catch (err) {
117
- // A bad source dir never aborts the whole CSS build (the other
118
- // sources still contribute their utilities).
119
68
  log.warn(
120
69
  `[build-client-assets] candidate scan skipped ${root}: ${
121
70
  err instanceof Error ? err.message : String(err)
@@ -1,44 +1,8 @@
1
1
  /**
2
- * @system server-handlers
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Upload source maps to GlitchTip via the official `glitchtip-cli` debug-ID
7
- * flow, then RETAIN the maps outside the served root so a failed upload is
8
- * retryable on a later boot.
9
- *
10
- * Split out of build-client-assets.ts (962 lines) alongside its build
11
- * siblings. Byte-for-byte move from the parent — imports adjusted, nothing
12
- * else.
13
4
  */
14
5
 
15
- /**
16
- * Upload source maps to GlitchTip via the official `glitchtip-cli` debug-ID
17
- * flow (the documented first-class mechanism — `sourcemaps inject` then
18
- * `sourcemaps upload --release`; see telemetry.md `client-errors-report-to-glitchtip`
19
- * + https://glitchtip.com/documentation/cli). Debug IDs injected into the served
20
- * bundles + uploaded maps are the matching mechanism — robust vs URL-prefix
21
- * guessing.
22
- *
23
- * THE MAPS OUTLIVE THE UPLOAD ATTEMPT: `.map` files are MOVED out of the
24
- * served root (`dist/client/` — /assets/* would serve source publicly) into
25
- * the retained cache `dist/sourcemaps/<release>/` BEFORE anything else, and
26
- * the upload runs from the cache. A failed or skipped upload therefore
27
- * leaves the maps on disk for the next boot's retry (the fingerprint-skip
28
- * path in build-client-assets.ts re-runs the upload whenever the manifest
29
- * does not record `uploaded`) instead of destroying the only copy of a
30
- * release's maps on an unverified claim (`a-component-may-not-report-a-
31
- * state-it-has-not-verified`; measured: the pre-2026-09 flow deleted the
32
- * maps after an upload whose responses were never checked, so a missing
33
- * token or a 4xx permanently lost a release's maps while the boot log kept
34
- * saying "uploaded").
35
- *
36
- * Every upstream response is verified: the release-create call must return
37
- * 2xx/208 and each file upload must return 2xx, else the failure is named
38
- * (status + body head) in the result. Best-effort at the SERVICE level — a
39
- * failed upload never blocks boot — but never at the CLAIM level: the
40
- * result says what actually happened and the manifest records it.
41
- */
42
6
 
43
7
  import { mkdir, readdir, rename, rm } from "node:fs/promises";
44
8
  import { join } from "node:path";
@@ -50,35 +14,22 @@ export type SourceMapUploadStatus = "uploaded" | "failed" | "skipped";
50
14
 
51
15
  export interface SourceMapUploadResult {
52
16
  status: SourceMapUploadStatus;
53
- /** Maps verified accepted by GlitchTip (2xx), never assumed. */
54
17
  uploaded: number;
55
- /** Maps present for this upload pass (moved to the retained cache). */
56
18
  total: number;
57
- /** One named failure per non-accepted upload (status + body head). */
58
19
  failures: string[];
59
20
  at: string;
60
21
  }
61
22
 
62
23
  const UPLOAD_TIMEOUT_MS = 30_000;
63
24
 
64
- /** The retained, never-served home of a release's source maps. */
65
25
  export function sourceMapsCacheDir(cwd: string, release: string): string {
66
26
  return join(cwd, "dist", "sourcemaps", release);
67
27
  }
68
28
 
69
- /** The release (bare content hash) a manifest's hydratePath names. */
70
29
  export function releaseFromHydratePath(hydratePath: string): string | null {
71
30
  return /hydrate-([a-z0-9]+)\.js/.exec(hydratePath)?.[1] ?? null;
72
31
  }
73
32
 
74
- /**
75
- * Move every `.map` in `outDir` whose base filename is in `currentOutputs`
76
- * into the retained cache for `release`, then DELETE every other `.map` left
77
- * in `outDir` (stale maps from an older build whose release cache should
78
- * already hold a copy — or, on Railway ephemeral disk, was lost; either
79
- * way the served root must never hold one). Returns the names that were
80
- * moved into THIS release's cache.
81
- */
82
33
  async function moveMapsToCache(
83
34
  cwd: string,
84
35
  outDir: string,
@@ -121,7 +72,6 @@ function authEnv(): { token: string; url: string } | null {
121
72
  return { token, url };
122
73
  }
123
74
 
124
- /** Upload the maps ALREADY RETAINED in the cache for `release`. */
125
75
  export async function uploadCachedSourceMaps(
126
76
  cwd: string,
127
77
  release: string,
@@ -153,7 +103,6 @@ export async function uploadCachedSourceMaps(
153
103
  return uploadMapsFromCache(cacheDir, names, release, auth, log, at);
154
104
  }
155
105
 
156
- /** Upload source maps for the bundle just built into `outDir`. */
157
106
  export async function uploadSourceMaps(
158
107
  cwd: string,
159
108
  outDir: string,
@@ -1,26 +1,6 @@
1
1
  /**
2
- * @system server-handlers
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The Tailwind stylesheet unit of the client-asset build: Bun-native
7
- * stylesheet resolution returning RAW content, stripping of remote
8
- * (CSP-forbidden) @imports, and the content-hashed styles-<hash>.css
9
- * build.
10
- *
11
- * NESTED @IMPORTS ARE THE SUPPLIER'S JOB: tailwind 4.3.3's compile()
12
- * re-invokes loadStylesheet for every @import INSIDE loaded content and
13
- * splices the loaded nodes into the tree (verified in the installed
14
- * dist source — the dr() recursion, depth-capped at 100), so a present
15
- * nested leaf is inlined by the supplier and a missing one throws at
16
- * compile; the suite pins both. The 2026-07-05 dropped-leaf outage was
17
- * guarded by an in-process flattener here; with the supplier recursion
18
- * verified the flattener is a second owner of the same fact and is
19
- * deleted.
20
- *
21
- * Split out of build-client-assets.ts (962 lines) alongside its build
22
- * siblings. Byte-for-byte move from the parent — imports adjusted, nothing
23
- * else.
24
4
  */
25
5
 
26
6
  import { dirname, join, resolve } from "node:path";
@@ -42,9 +22,6 @@ export async function bunResolveStylesheet(
42
22
  return { path: r, base: dirname(r), content: await Bun.file(r).text() };
43
23
  }
44
24
  if (id.startsWith(".")) {
45
- // Relative import: resolve against the importing file's directory and
46
- // REQUIRE existence — a missing leaf is a build-breaking error naming
47
- // the specifier, never a silently-dropped subtree.
48
25
  const p = resolve(base, id);
49
26
  if (!(await Bun.file(p).exists())) {
50
27
  throw new Error(
@@ -53,28 +30,11 @@ export async function bunResolveStylesheet(
53
30
  }
54
31
  return { path: p, base: dirname(p), content: await Bun.file(p).text() };
55
32
  }
56
- // A BARE SPECIFIER IS A PACKAGE NAME PLUS AN OPTIONAL SUBPATH, and the two
57
- // must be separated before anything is looked up. `@teamscala/ui-foundation`
58
- // is a package; `@teamscala/ui-foundation/theme/styles.css` is that same
59
- // package plus an explicit file inside it. Treating the whole string as a
60
- // package name looks for node_modules/@teamscala/ui-foundation/theme/
61
- // styles.css/package.json, which cannot exist — so EVERY subpath import
62
- // fails and the styles bundle dies, taking the theme layer with it.
63
- //
64
- // A scoped name is the FIRST TWO segments (@scope/name); an unscoped name
65
- // is the first. Everything after that is a path inside the package and is
66
- // resolved directly — it names a file, so there is nothing to negotiate and
67
- // no style entry to consult.
68
33
  const segments = id.split("/");
69
34
  const nameLength = id.startsWith("@") ? 2 : 1;
70
35
  const packageName = segments.slice(0, nameLength).join("/");
71
36
  const subpath = segments.slice(nameLength).join("/");
72
37
 
73
- // Find node_modules/<packageName>/package.json by walking upward from the
74
- // importing file's dir then cwd. Bun.resolve is not used: it hands back
75
- // whatever the package's main is — for a normal JS package a .js entry,
76
- // not the declared stylesheet — and an `exports` allow-list blocks the
77
- // package.json subpath on exactly the CSS-only packages this must serve.
78
38
  const tried: string[] = [];
79
39
  for (const root of [base, cwd]) {
80
40
  let dir = root;
@@ -84,13 +44,6 @@ export async function bunResolveStylesheet(
84
44
  const pkgDir = dirname(pkgPath);
85
45
  const pkg = await Bun.file(pkgPath).json();
86
46
  if (subpath) {
87
- // THE EXPORTS MAP IS THE AUTHORITY ON WHERE A SUBPATH LIVES,
88
- // and it commonly REMAPS. `@teamscala/ui-foundation` publishes
89
- // "./theme/styles.css" -> "./src/theme/styles.css", so resolving
90
- // the subpath literally against the package root looks for a
91
- // file that has never existed there. Consult exports first and
92
- // fall back to the literal path only for a package that
93
- // declares none.
94
47
  const exported = pkg.exports?.[`./${subpath}`];
95
48
  const mapped: string | undefined =
96
49
  typeof exported === "string"
@@ -107,7 +60,6 @@ export async function bunResolveStylesheet(
107
60
  }
108
61
  return { path: r, base: dirname(r), content: await Bun.file(r).text() };
109
62
  }
110
- // No subpath: the package itself must declare its stylesheet.
111
63
  const entry: string | undefined = pkg.exports?.["."]?.style ?? pkg.style;
112
64
  if (!entry) {
113
65
  throw new Error(
@@ -128,16 +80,6 @@ export async function bunResolveStylesheet(
128
80
  );
129
81
  }
130
82
 
131
- // The app owns its fonts via an explicit, CSP-allowlisted <link> in index.html (Google Fonts — `style-src ... fonts.googleapis.com`, `font-src ...
132
- // fonts.gstatic.com`). Third-party package CSS MUST NOT inject its own remote `@import url(http…)`: @puckeditor/core/dist/index.css opens with
133
- // `@import "https://rsms.me/inter/inter.css"`, an arbitrary CDN our CSP does NOT allow. The Tailwind compile inlines that mid-output, where it was either
134
- // silently dropped (CSS spec: @import must precede all rules) OR — once hoisted —
135
- // blocked by CSP, leaving a console error on every page; either way the font
136
- // never loaded and the site fell back to a system font (scala-docs 2026-06-13).
137
- // So every remote @import is STRIPPED from the compiled bundle: a package can
138
- // never inject a CDN font/stylesheet, and the only font source is the app-owned
139
- // index.html link. Fixed once at the single CSS chokepoint; guarded by
140
- // `css-build-strips-remote-imports`.
141
83
  const REMOTE_IMPORT_RE =
142
84
  /^[ \t]*@import\s+(?:url\(\s*)?["']https?:\/\/[^"')]+["']\s*\)?[^;]*;[ \t]*\n?/gm;
143
85
 
@@ -164,16 +106,6 @@ export async function buildTailwindCss(
164
106
  await mkdir(outDir, { recursive: true });
165
107
 
166
108
  const cssSource = await Bun.file(entry).text();
167
- // base MUST be the ENTRY FILE's directory (src/), never the service root.
168
- // The generated app.css authors its @source dirs relative to itself
169
- // ("../node_modules/@teamscala/<pkg>/src"); Tailwind 4.3 resolves @source
170
- // against THIS base, so passing the service root aimed every scan one
171
- // level too high (workspace/node_modules — empty) and the candidate set
172
- // collapsed to zero: build([]) strips every Tailwind utility and every
173
- // kind:ui service renders with naked buttons/controls while the
174
- // stylesheet still loads with its full package-rule payload (measured
175
- // 2026-09-05, operator-visible fleet-wide). Package-specifier @imports are
176
- // unaffected — they resolve through loadStylesheet's Bun.resolve walk.
177
109
  const compiler = await compile(cssSource, {
178
110
  base: join(cwd, "src"),
179
111
  async loadStylesheet(id, base) {
@@ -186,8 +118,6 @@ export async function buildTailwindCss(
186
118
  },
187
119
  });
188
120
 
189
- // Scan every resolved @source for candidate class names — WITHOUT this the
190
- // build emits no content-driven utilities (the unstyled-editor bug above).
191
121
  const candidates = await scanSourceCandidates(compiler.sources, log);
192
122
  const cssOutput = stripRemoteImports(compiler.build(candidates));
193
123
  log.info(
@@ -1,35 +1,6 @@
1
1
  /**
2
- * @system server-handlers
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Builds the per-service client bundle and stylesheet into `dist/client/`
7
- * at UI-service boot time. Outputs are content-hashed so deployments
8
- * naturally cache-bust:
9
- *
10
- * src/entry-client.ts ──bun build──> dist/client/hydrate-<hash>.js
11
- * src/app.css ──tailwindcss──> dist/client/styles-<hash>.css
12
- * (and) dist/client/asset-manifest.json
13
- *
14
- * The HTML template references `{{stylesPath}}` / `{{hydratePath}}` —
15
- * read-html-template.ts substitutes from the manifest at template-read
16
- * time. Bytes-level changes → new hash → new URL → browser refetch.
17
- * Without this, `cache-control: immutable` on a fixed filename means
18
- * returning visitors keep their stale copy forever.
19
- *
20
- * This module is the ORCHESTRATION unit: host-wide lock → parallel
21
- * hydrate/CSS builds → manifest write (or failure tombstone). The
22
- * build runs on EVERY boot: the content-fingerprint skip cache and its
23
- * closure-walking hashers (input-fingerprint.ts, input-hashing.ts) are
24
- * deleted. A skip is the only path that can serve a stale bundle, and
25
- * it has done so twice (the mtime skip that baked stale bundles until
26
- * the fingerprint replaced it, 2026-06-14) — no skip is the one shape
27
- * that cannot skip wrongly, at the cost of a bounded per-boot build
28
- * serialised by the host-wide lock below. The individual concerns live
29
- * as siblings under ./build-client-assets/
30
- * (source-candidates, manifest, hashed-output, hydrate-bundle,
31
- * sourcemaps-upload, styles-build, build-lock)
32
- * — split when the parent held them all at 962 lines.
33
4
  */
34
5
 
35
6
  // Each build is wrapped in its own catch so a JS bundle failure (e.g. transient cross-package export drift) doesn't take down the service
@@ -1,59 +1,15 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * Deployable slug -> capability slug.
7
- *
8
- * A DEPLOYABLE is named `<capability>-<language>-<tier>`
9
- * (`deployables-carry-language-and-tier`, as amended by the operator ruling of
10
- * 2026-09-08: "so actually the apps are ts, not bun" — the language segment
11
- * spells the LANGUAGE, so Bun-launched services are `ts`): `scala-projects-ts-dev`
12
- * and `scala-projects-ts-prod` are two DEPLOYMENTS of one APPLICATION. The
13
- * ecosystem generator derives that name per lane, so one service arrives here
14
- * under a DIFFERENT identity in each lane.
15
- *
16
- * A `page_route` row describes the APPLICATION url space, which is the same
17
- * in both lanes, so it carries the CAPABILITY name and there is exactly one
18
- * row per route rather than one per lane. Joining the two by raw string
19
- * equality therefore matches at most one lane, and does so SILENTLY:
20
- * measured 2026-09-11, 8 of 10 dev UI services registered ZERO page routes
21
- * and served 404 on every path -- including `/sign-in` -- while booting
22
- * clean and reporting healthy, because the dev lane ran as
23
- * `scala-projects-bun-dev` while every page_route row said `scala-projects`.
24
- *
25
- * Both sides normalize through this one function, so the join is
26
- * tier-agnostic by construction and a row may name either form.
27
- *
28
- * The suffix grammar is a CLOSED SET defined by doctrine and emitted by the
29
- * generator, so this is a parse of a documented contract rather than a
30
- * pattern heuristic. A slug that does not end in one is already a capability
31
- * name and is returned unchanged.
32
- *
33
- * `bun` REMAINS IN THE SET AS RETIRED SPELLING TOLERANCE, mirroring the
34
- * generator side (`ecosystem/deployable_identity.rs capability_of` strips
35
- * `-ts`, `-bun` and `-rust`): the fleet was renamed under `-bun` and rows or
36
- * supervised names still carrying it must converge to the same capability
37
- * rather than fail the join during the migration window.
38
4
  */
39
5
 
40
- /** Language segment of a deployable name. `bun` is the retired spelling. */
41
6
  const LANGUAGES = ["ts", "bun", "rust"] as const;
42
7
 
43
- /** Tier segment of a deployable name. */
44
8
  const TIERS = ["dev", "prod"] as const;
45
9
 
46
10
  type Language = (typeof LANGUAGES)[number];
47
11
  type Tier = (typeof TIERS)[number];
48
12
 
49
- /**
50
- * The capability half of a deployable slug.
51
- *
52
- * `scala-projects-ts-dev` -> `scala-projects`
53
- * `scala-projects-ts-prod` -> `scala-projects`
54
- * `scala-projects-bun-dev` -> `scala-projects` (retired spelling)
55
- * `scala-projects` -> `scala-projects` (already a capability name)
56
- */
57
13
  export function toCapabilitySlug(slug: string): string {
58
14
  const parts = slug.split("-");
59
15
  if (parts.length < 3) return slug;
@@ -1,47 +1,14 @@
1
1
  /**
2
- * @system ui-service-boot
2
+ * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The single canonical page-title composer. Every SSR page handler routes
7
- * through this (portal/region-shell, public shell, single-page resource). A
8
- * page title is DERIVED from data that already exists — the website name +
9
- * the page/entity name — with `builda_pages.meta_title` as an explicit
10
- * per-page OVERRIDE only, never the primary source.
11
- *
12
- * Mirrors the brand-is-org-owned-full-scheme precedent: a value that should
13
- * be DERIVED was instead hand-set per row, and when unset it silently fell
14
- * back to something wrong. For themes the wrong fallback was the generic
15
- * light palette; for titles it was the bare org name — the `default`
16
- * page_meta_template `titleTemplate: "{org.name}"` produced "GBG Indo" with
17
- * no site or page context, and per-row `meta_title` carried stale product
18
- * literals like "Dashboard | scala Hub". The fix is identical in shape:
19
- * derive by default, override explicitly.
20
- *
21
- * Canonical format: `<website.name>: <subject>` (subject = entity title or
22
- * page name), site-first so a browser tab reads the surface then the page.
23
- * A resource surface (forms/quiz/gpt) may supply a curated `templateTitle`
24
- * (e.g. "{agent.name} — {org.name}"); it wins over the default derivation
25
- * but NOT over an explicit per-page override. The composer NEVER returns a
26
- * bare org name — that is the regression this closes, and the
27
- * `page-title-not-bare-org` guard backstops it at the template layer.
28
4
  */
29
5
 
30
6
  export interface TitleContext {
31
- /** Per-page override (builda_pages.meta_title). Wins outright when set. */
32
7
  pageMetaTitle?: string | null;
33
- /** Resolved resource-template string (forms/quiz/gpt). Curated per-surface shape. */
34
8
  templateTitle?: string | null;
35
- /** builda_websites.name — the surface, the legacy site context (orgPrefix off). */
36
9
  websiteName?: string | null;
37
- /** The OWNING ORG's display name (brand.companyName, resolved from the
38
- * org the page's brand_org_path names). The title prefix when orgPrefix is
39
- * on — operator ruling 2026-08-17: a client-facing document's tab carries
40
- * the org's name, never the internal website label ("Public Pages"). */
41
10
  organisationName?: string | null;
42
- /** builda_pages.name — the page (e.g. "Dashboard"). */
43
11
  pageName?: string | null;
44
- /** Resource entity title (proposal/form/quiz/agent name) — preferred subject. */
45
12
  entityTitle?: string | null;
46
13
  }
47
14
 
@@ -1,14 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * The content-gate ACCESS endpoint mounter. One generic POST handler for every
7
- * gate: a plain <form> posts {<recordIdField>, email}; on a match against the
8
- * resource's authorized set it sets the signed access cookie and redirects back,
9
- * so the page loader's gate then releases the content. No bespoke endpoint per
10
- * type — each gate is one AccessConfig. Mounted BEFORE the auth gate so it is
11
- * publicly reachable.
12
4
  */
13
5
 
14
6
  import type { Hono } from "hono";
@@ -38,36 +30,17 @@ export async function attachContentGateAccess(
38
30
  let email = "";
39
31
  let name = "";
40
32
  let phone = "";
41
- // Optional prospect segmentation a capture gate's composed form may post
42
- // (the quiz identity screen: company, LinkedIn, website, team size). Every
43
- // field is optional and every reader treats absence as "" — a gate that
44
- // never collects them (the GPT) is unaffected, and a form that collects
45
- // only some carries the rest as absent. Field names follow the response
46
- // columns the segmentation lands in (linkedin_url / team_size, snake case)
47
- // because the composed form posts exactly what its FormField rows name.
48
33
  let company = "";
49
34
  let linkedinUrl = "";
50
35
  let website = "";
51
36
  let teamSize = "";
52
37
  let bodyFailed = false;
53
- // The resource id may ride on the URL instead of the body. A gate whose
54
- // form is COMPOSED FROM BLOCKS has no place to put a hidden field — the
55
- // generic submit action collects the visible inputs and adds only the
56
- // static fields its row declares, and a row is shared by every page so it
57
- // cannot carry a per-page id. The gate therefore publishes its own access
58
- // URL with the id already on it (see quiz-gate onUnauthorized), which
59
- // keeps the id single-sourced from the resolved graph. A body field still
60
- // wins where one is posted (proposals, agreements, the GPT).
61
38
  const queryResourceId = String(c.req.query(config.recordIdField) ?? "").trim();
62
39
  try {
63
40
  const form = await c.req.parseBody();
64
41
  resourceId = String(form[config.recordIdField] ?? "").trim() || queryResourceId;
65
42
  email = String(form.email ?? "").trim().toLowerCase();
66
- // Optional captured display name (open-capture gates carry it in the
67
- // token; allow-list gates ignore it via signToken).
68
43
  name = String(form.name ?? "").trim();
69
- // Captured alongside the email by an open-capture gate's composed
70
- // form. It rides in the signed token rather than any client store.
71
44
  phone = String(form.phone ?? "").trim();
72
45
  company = String(form.company ?? "").trim();
73
46
  linkedinUrl = String(form.linkedin_url ?? "").trim();
@@ -82,9 +55,6 @@ export async function attachContentGateAccess(
82
55
 
83
56
  let matched = false;
84
57
  if (config.openCapture) {
85
- // Open lead-capture gate (the public GPT): any non-empty email is a
86
- // valid visitor — there is no allow-list to authorize against. We
87
- // already required a non-empty email above, so this is a match.
88
58
  matched = true;
89
59
  } else {
90
60
  try {
@@ -120,10 +90,6 @@ export async function attachContentGateAccess(
120
90
  `${config.cookieName}=${encodeURIComponent(token)}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`,
121
91
  );
122
92
  if (config.entryCookieName) {
123
- // The ENTRY MARKER: it authorizes exactly the redirect this sets it
124
- // on — the gate requires it and the render it consumed clears it.
125
- // Max-Age matches the token only as a safety net; consumption is
126
- // what ends its life, never the clock.
127
93
  c.header(
128
94
  "Set-Cookie",
129
95
  `${config.entryCookieName}=1; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`,
@@ -1,58 +1,9 @@
1
1
  /**
2
2
  * @system service-boot
3
3
  * @status handwritten
4
- * @edit edit directly
5
- *
6
- * THE CATALOG OF GATED CONTENT TYPES — the one place a gate is registered.
7
- *
8
- * The gate ENGINE (gate.ts) and the access MOUNTER (access.ts) were already
9
- * generic and config-driven, but the SEQUENCE was not: `register-registry-page-
10
- * route.ts` named proposalGateConfig + agreementGateConfig + gptGateConfig in a
11
- * hand-written array, and `two-phase-boot.ts` named the same three AccessConfigs
12
- * in a second hand-written array. Two lists, one fact, and adding a fourth gated
13
- * resource meant editing both — the same drift as the assistant, one layer down
14
- * (`information-exists-in-one-location`).
15
- *
16
- * Now both call sites iterate THIS. A gate is registered once, as a pair, and
17
- * the SSR gate and its public access endpoint cannot disagree about which gates
18
- * exist — the shape doctrine asks for when behaviour genuinely is code:
19
- * `business-logic-is-rows-plus-executors`, "extend the executor catalog".
20
- *
21
- * WHY A CATALOG AND NOT A REGISTRY ROW. A gate is not data — `verifyToken`,
22
- * `resolveAuthorizedEmails` and the withholding rules are executable behaviour,
23
- * which the rule places in a typed executor, never in a row. What belongs in a
24
- * row is the SELECTION: which gate governs a given page. That is a `page_route`
25
- * field, and a row can only select from a catalog, so this file is the thing it
26
- * will select from — not a step on the way to it.
27
4
  */
28
5
 
29
- /**
30
- * Registered gates, in evaluation order.
31
- *
32
- * ORDER IS LOAD-BEARING AND SO IT IS DECLARED, NOT INCIDENTAL. Proposals run
33
- * before agreements: an authorized proposal viewer has their gate block removed
34
- * from the resolved graph, after which the agreement gate finds no resource and
35
- * no-ops. Reversing these two changes behaviour on a page carrying both.
36
- *
37
- * Each gate still self-selects — `getResourceId` returns "" when the page is not
38
- * its kind — so listing a gate here never makes it apply to a page it does not
39
- * govern.
40
- */
41
6
 
42
- /**
43
- * Does this gate WITHHOLD content, or only identify the visitor?
44
- *
45
- * DERIVED FROM THE ACCESS CONFIG, NEVER DECLARED TWICE. `openCapture` already
46
- * states that any email is a valid visitor, which is precisely the case where
47
- * nothing is being withheld — the GPT page body is public and only its identity
48
- * is gated (see gptGateConfig.onUnauthorized, a documented no-op). A separate
49
- * `withholdsContent` flag would be a second copy of that fact, free to drift
50
- * from the one the access endpoint actually enforces (`offered-is-derived`).
51
- *
52
- * The distinction matters to the assistant: a seed-only gate must not count as
53
- * "governing" this page, or an anonymous visitor to a public GPT page would be
54
- * read as an unauthorized viewer and lose an assistant they are entitled to.
55
- */
56
7
 
57
8
  import type { AccessConfig, ContentGateConfig } from "./config-types.ts";
58
9
  import { agreementAccessConfig, agreementGateConfig } from "../agreement-gate.ts";
@@ -60,9 +11,7 @@ import { gptAccessConfig, gptGateConfig } from "../gpt-gate.ts";
60
11
  import { proposalAccessConfig, proposalGateConfig } from "../proposal-gate.ts";
61
12
  import { quizAccessConfig, quizGateConfig } from "../quiz-gate.ts";
62
13
 
63
- /** One gated content type: its SSR gate and its public access endpoint. */
64
14
  export interface ContentGateEntry {
65
- /** Stable identity for this gate — what a `page_route` will name to select it. */
66
15
  slug: string;
67
16
  gate: ContentGateConfig;
68
17
  access: AccessConfig;
@@ -79,7 +28,6 @@ export function withholdsContent(entry: ContentGateEntry): boolean {
79
28
  return entry.access.openCapture !== true;
80
29
  }
81
30
 
82
- /** The gates that can withhold content, in order — the assistant's authority. */
83
31
  export function contentWithholdingGates(): ContentGateEntry[] {
84
32
  return CONTENT_GATES.filter(withholdsContent);
85
33
  }