@teamscala/service-boot 0.23.524 → 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 (81) hide show
  1. package/package.json +1 -9
  2. package/src/agreement-gate/authorized-emails.ts +0 -7
  3. package/src/agreement-gate/placeholder-strip.ts +0 -2
  4. package/src/agreement-gate/settings.ts +0 -15
  5. package/src/agreement-gate.ts +0 -34
  6. package/src/attach-proposal-accept.ts +1 -1
  7. package/src/attach-proposal-magic.ts +1 -1
  8. package/src/auth/sign-out.ts +0 -2
  9. package/src/auth-gate/public-paths.ts +0 -86
  10. package/src/auth-gate.ts +0 -19
  11. package/src/bind-page-handler.ts +0 -76
  12. package/src/build-client-assets/hydrate-bundle.ts +0 -126
  13. package/src/build-client-assets/manifest.ts +0 -18
  14. package/src/build-client-assets/source-candidates.ts +0 -34
  15. package/src/build-client-assets/sourcemaps-upload.ts +0 -42
  16. package/src/build-client-assets/styles-build.ts +0 -50
  17. package/src/capability-slug.ts +0 -10
  18. package/src/compose-page-title.ts +0 -9
  19. package/src/content-gate/access.ts +0 -26
  20. package/src/content-gate/catalog.ts +0 -29
  21. package/src/content-gate/config-types.ts +3 -84
  22. package/src/content-gate/gate.ts +0 -20
  23. package/src/content-page-floor.ts +0 -11
  24. package/src/error-page-logo.ts +2 -12
  25. package/src/expand-roster.ts +0 -9
  26. package/src/gpt-gate.ts +1 -1
  27. package/src/hono-error-handler.ts +0 -4
  28. package/src/install-logging.ts +0 -32
  29. package/src/load-page-manifest.ts +0 -6
  30. package/src/load-service-install-row.ts +41 -4
  31. package/src/module-not-found.ts +0 -11
  32. package/src/mount-orpc-router.ts +1 -88
  33. package/src/mount-server-packages.ts +0 -29
  34. package/src/mutable-app-ref.ts +0 -11
  35. package/src/orpc-contract-rows.json +289 -0
  36. package/src/orpc-openapi-document.ts +0 -6
  37. package/src/orpc-openapi-gate.ts +0 -10
  38. package/src/pg-boss-handler-registry.ts +0 -12
  39. package/src/pg-boss-mount.ts +0 -101
  40. package/src/proposal-gate.ts +1 -41
  41. package/src/quiz-gate.ts +1 -9
  42. package/src/register-page-server-client.ts +0 -53
  43. package/src/register-registry-page-route/ai-chat-prefetch.ts +0 -18
  44. package/src/register-registry-page-route/apply-page-branding.ts +0 -90
  45. package/src/register-registry-page-route/captured-loader-config.ts +0 -1
  46. package/src/register-registry-page-route/graph-cache.ts +0 -17
  47. package/src/register-registry-page-route/meta-title.ts +0 -24
  48. package/src/register-registry-page-route/page-data-shared.ts +0 -6
  49. package/src/register-registry-page-route/prefetch-org-id.ts +0 -17
  50. package/src/register-registry-page-route/proposal-approval-email-token.ts +0 -11
  51. package/src/register-registry-page-route/proposal-brand-stamping.ts +0 -20
  52. package/src/register-registry-page-route/public-shell-page-data.ts +0 -26
  53. package/src/register-registry-page-route/resolve-request-graph.ts +0 -57
  54. package/src/register-registry-page-route/run-content-gates.ts +2 -54
  55. package/src/register-registry-page-route/warn-uncovered-blocks.ts +0 -10
  56. package/src/register-registry-page-route.ts +0 -75
  57. package/src/register-static-assets.ts +0 -45
  58. package/src/resolve-brand-favicon.ts +0 -9
  59. package/src/resolve-service-port.ts +0 -11
  60. package/src/resolve-theme-mode.ts +0 -8
  61. package/src/resolvers/app-header.ts +0 -1
  62. package/src/resolvers/fetch-org-brand.ts +0 -1
  63. package/src/resolvers/portal-route-paths.ts +0 -17
  64. package/src/resource-not-found.ts +0 -24
  65. package/src/select-service-routes.ts +0 -14
  66. package/src/theme-fonts-link.ts +0 -5
  67. package/src/two-phase-full-app.ts +0 -76
  68. package/src/two-phase-minimal-app.ts +1 -105
  69. package/src/types.ts +0 -14
  70. package/src/ui-service.ts +1 -42
  71. package/src/content-gate/gate-entry-marker.test.ts +0 -111
  72. package/src/content-gate/token.segmentation.test.ts +0 -76
  73. package/src/content-gate/token.test.ts +0 -65
  74. package/src/content-gate/token.ts +0 -197
  75. package/src/is-react-element-type.test.ts +0 -45
  76. package/src/is-react-element-type.ts +0 -10
  77. package/src/page-server-orpc/hub-mcp-client.ts +0 -14
  78. package/src/pg-boss-mount.test.ts +0 -101
  79. package/src/resource-chat-inject.page-assistant.test.ts +0 -145
  80. package/src/resource-chat-inject.test.ts +0 -240
  81. package/src/resource-chat-inject.ts +0 -357
@@ -1,145 +0,0 @@
1
- // @system codegen
2
- // @status generated
3
- // @edit change the suite in the owned-suites band, then re-run codegen. Hand-edits are overwritten.
4
- //
5
- // This suite's assertions are OWNED by the codegen band: the band module
6
- // carries them verbatim, this file is the emission, and hand edits here are
7
- // overwritten on the next run. The rationale each assertion carries moved
8
- // with it into the band.
9
-
10
- import { describe, expect, mock, test } from "bun:test";
11
-
12
- /** The ONE active launcher block_schema slug — the Puck registry key. */
13
- const REGISTERED_LAUNCHER_SLUG = "ResourceLauncher";
14
-
15
- mock.module("@teamscala/db/registry/load-config", () => ({
16
- loadRegistryConfig: async () => ({ agentId: "agent-1" }),
17
- }));
18
- mock.module("./page-server-orpc/server-client.ts", () => ({
19
- getServerClient: () => ({
20
- ai_agents: {
21
- findFirst: async () => ({
22
- name: "Results Assistant",
23
- welcome_message: "Hi! Ask me anything about your results.",
24
- conversation_starters: ["What are my strengths?"],
25
- primary_color: "#6366f1",
26
- }),
27
- },
28
- }),
29
- }));
30
- mock.module("@teamscala/logger/app-loggers", () => ({
31
- getAppLogger: () => ({ error: (...args: unknown[]) => void args }),
32
- }));
33
-
34
- const { injectResourceChatLauncher } = await import("./resource-chat-inject.ts");
35
-
36
- const ORG = "org-1";
37
- const AGENT_OPTS = {
38
- agentConfigSlug: "assessment-assistant-agent" as never,
39
- launchLabel: "Ask about your results",
40
- placeholder: "Ask about your results…",
41
- regionSlots: ["header", "body", "footer"] as readonly string[],
42
- };
43
-
44
- /** A resolved response row as the loader's response source selects it. */
45
- const RESPONSE_ROW = {
46
- id: "resp-1",
47
- assessment_id: "asm-1",
48
- organisation_id: ORG,
49
- total_score: 64,
50
- first_name: "Anna",
51
- };
52
-
53
- /** The paired key as resolveReportHalves returns it — present on EVERY link
54
- * shape, carrying the anchor half's organisation. */
55
- const PAIRED = {
56
- response: RESPONSE_ROW,
57
- research: { id: "research-live", assessment_id: "asm-1", organisation_id: ORG },
58
- assessment_id: "asm-1",
59
- organisation_id: ORG,
60
- };
61
-
62
- function pageGraph(
63
- responseSource: unknown,
64
- pairSource: Record<string, unknown>,
65
- ): Record<string, unknown> {
66
- return {
67
- response: responseSource,
68
- pair: pairSource,
69
- quiz: { id: "asm-1", organisation_id: ORG, title: "Authority Index" },
70
- brand: { primaryColor: "#123456" },
71
- body: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
72
- header: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
73
- footer: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
74
- };
75
- }
76
-
77
- /** The launcher renders `position: fixed`, so it may land in whichever
78
- * region slot is declared first — count across EVERY slot, not one. */
79
- function launcherBlocks(
80
- resolved: Record<string, unknown>,
81
- ): Array<{ type: string; props?: Record<string, unknown> }> {
82
- const out: Array<{ type: string; props?: Record<string, unknown> }> = [];
83
- for (const slot of AGENT_OPTS.regionSlots) {
84
- const content = (
85
- resolved[slot] as {
86
- puck_data?: { content?: Array<{ type: string; props?: Record<string, unknown> }> };
87
- }
88
- ).puck_data?.content;
89
- for (const block of content ?? []) {
90
- if (block.type === REGISTERED_LAUNCHER_SLUG) out.push(block);
91
- }
92
- }
93
- return out;
94
- }
95
-
96
- async function launcherCount(
97
- responseSource: unknown,
98
- pairSource: Record<string, unknown>,
99
- resourceKey: string,
100
- ): Promise<number> {
101
- const resolved = pageGraph(responseSource, pairSource);
102
- await injectResourceChatLauncher(resolved, "", { ...AGENT_OPTS, resourceKey });
103
- return launcherBlocks(resolved).length;
104
- }
105
-
106
- describe("page assistant renders on every link shape", () => {
107
- test("response-id link: raw hit exists, launcher renders", async () => {
108
- expect(await launcherCount(RESPONSE_ROW, PAIRED, "pair")).toBe(1);
109
- });
110
-
111
- test("live research-anchor link: raw hit is null, launcher still renders", async () => {
112
- expect(await launcherCount(null, PAIRED, "pair")).toBe(1);
113
- });
114
-
115
- test("superseded research-anchor link: raw hit is null, launcher still renders", async () => {
116
- // identical graph shape to the live anchor — resolution to the live
117
- // pass happens upstream in the pair resolver, so the injection surface
118
- // is the same and is exercised identically
119
- expect(await launcherCount(null, PAIRED, "pair")).toBe(1);
120
- });
121
-
122
- test("the launcher block carries the agent, org scope and context", async () => {
123
- const resolved = pageGraph(null, PAIRED);
124
- await injectResourceChatLauncher(resolved, "viewer@example.com", {
125
- ...AGENT_OPTS,
126
- resourceKey: "pair",
127
- });
128
- const launchers = launcherBlocks(resolved);
129
- expect(launchers.length).toBe(1);
130
- const launcher = launchers[0];
131
- expect(launcher.props.agentId).toBe("agent-1");
132
- expect(launcher.props.organisationId).toBe(ORG);
133
- expect(typeof launcher.props.runtimeContext).toBe("string");
134
- expect((launcher.props.runtimeContext as string).length).toBeGreaterThan(0);
135
- });
136
-
137
- test("the DEFECT SHAPE: keying the raw response hit on an anchor link silently renders none", async () => {
138
- // resource_key "response" + a null raw hit is the 2026-09-08 regression.
139
- // The injector contract is fail-soft for the render, so the absence is
140
- // quiet — this pins the shape with the reason spelled out, so the row
141
- // can never go back to a key that is null on anchor links without the
142
- // next reader finding this.
143
- expect(await launcherCount(null, PAIRED, "response")).toBe(0);
144
- });
145
- });
@@ -1,240 +0,0 @@
1
- // @system codegen
2
- // @status generated
3
- // @edit change the suite in the owned-suites band, then re-run codegen. Hand-edits are overwritten.
4
- //
5
- // This suite's assertions are OWNED by the codegen band: the band module
6
- // carries them verbatim, this file is the emission, and hand edits here are
7
- // overwritten on the next run. The rationale each assertion carries moved
8
- // with it into the band.
9
-
10
- import { describe, expect, mock, test } from "bun:test";
11
-
12
- /** The ONE active launcher block_schema slug — the Puck registry key. */
13
- const REGISTERED_LAUNCHER_SLUG = "ResourceLauncher";
14
-
15
- mock.module("@teamscala/db/registry/load-config", () => ({
16
- loadRegistryConfig: async () => ({ agentId: "agent-1" }),
17
- }));
18
- mock.module("./page-server-orpc/server-client.ts", () => ({
19
- getServerClient: () => ({
20
- ai_agents: {
21
- findFirst: async () => ({
22
- name: "Agreement Assistant",
23
- welcome_message: "Hi! Ask me anything about this agreement.",
24
- conversation_starters: ["What are the key terms?", "How do I sign this?"],
25
- primary_color: "#6366f1",
26
- }),
27
- },
28
- }),
29
- }));
30
- mock.module("@teamscala/logger/app-loggers", () => ({
31
- getAppLogger: () => ({ error: () => {} }),
32
- }));
33
-
34
- const { injectResourceChatLauncher } = await import("./resource-chat-inject.ts");
35
-
36
- function makeResolved(resourceKey: string): Record<string, unknown> {
37
- return {
38
- [resourceKey]: { organisation_id: "org-1", name: "Demo" },
39
- brand: { primaryColor: "#123456" },
40
- page: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
41
- };
42
- }
43
-
44
- function pushedBlocks(resolved: Record<string, unknown>) {
45
- const pd = (
46
- resolved.page as {
47
- puck_data: { content: Array<{ type: string; props?: Record<string, unknown> }> };
48
- }
49
- ).puck_data;
50
- return pd.content;
51
- }
52
-
53
- describe("chat injectors — pushed block name resolves to a registered block", () => {
54
- test("the generic resource injector pushes the registered launcher slug", async () => {
55
- const resolved = makeResolved("agreement");
56
- await injectResourceChatLauncher(resolved, "viewer@example.com", {
57
- resourceKey: "agreement",
58
- agentConfigSlug: "agreement-assistant-agent",
59
- launchLabel: "Ask about this agreement",
60
- });
61
- const blocks = pushedBlocks(resolved);
62
- expect(blocks).toHaveLength(1);
63
- expect(blocks[0].type).toBe(REGISTERED_LAUNCHER_SLUG);
64
- });
65
-
66
- test("neither injector pushes a pre-consolidation launcher name", async () => {
67
- const a = makeResolved("agreement");
68
- await injectResourceChatLauncher(a, "viewer@example.com", {
69
- resourceKey: "agreement",
70
- agentConfigSlug: "agreement-assistant-agent",
71
- launchLabel: "Ask about this agreement",
72
- });
73
- const p = makeResolved("proposal");
74
- await injectResourceChatLauncher(p, "viewer@example.com", {
75
- resourceKey: "proposal",
76
- agentConfigSlug: "proposal-assistant-agent",
77
- launchLabel: "Ask about this proposal",
78
- });
79
-
80
- const dead = ["ResourceChatLauncher", "ChatLauncherBlock"];
81
- for (const blocks of [pushedBlocks(a), pushedBlocks(p)]) {
82
- for (const block of blocks) {
83
- expect(dead).not.toContain(block.type);
84
- }
85
- }
86
- });
87
-
88
- test("the assistant's words come from the agent row, not the caller", async () => {
89
- // THE REGRESSION THIS PINS: title, welcome message and starters used to
90
- // be string literals in the injectors while ai_agents held a blank
91
- // column for each. Whoever changed the row saw no change on the page.
92
- const resolved = makeResolved("agreement");
93
- await injectResourceChatLauncher(resolved, "viewer@example.com", {
94
- resourceKey: "agreement",
95
- agentConfigSlug: "agreement-assistant-agent",
96
- launchLabel: "Ask about this agreement",
97
- });
98
- const props = pushedBlocks(resolved)[0].props ?? {};
99
- expect(props.title).toBe("Agreement Assistant");
100
- expect(props.welcomeMessage).toBe("Hi! Ask me anything about this agreement.");
101
- expect(props.conversationStarters).toEqual([
102
- "What are the key terms?",
103
- "How do I sign this?",
104
- ]);
105
- // launchLabel has no column yet, so it is still the caller's.
106
- expect(props.launchLabel).toBe("Ask about this agreement");
107
- });
108
- });
109
-
110
- /** A region-slot page — the shape a quiz report resolves (no `page` source). */
111
- function makeAssessmentResolved(): Record<string, unknown> {
112
- return {
113
- response: {
114
- organisation_id: "org-1",
115
- email: "reader@example.com",
116
- total_score: 95,
117
- category_scores: { "Be Seen": 100, "Be Chosen": 83 },
118
- },
119
- quiz: { id: "quiz-1", report_title: "The Launcha Authority Index" },
120
- reportData: { narrative: "You are strongest at being seen." },
121
- brand: { primaryColor: "#123456" },
122
- header: { puck_data: { content: [{ type: "SiteHeader" }] } },
123
- body: { puck_data: { root: { props: {} }, zones: {}, content: [] } },
124
- footer: { puck_data: { content: [{ type: "SiteFooter" }] } },
125
- };
126
- }
127
-
128
- /** The launcher's props, wherever in the graph it was attached. */
129
- function launcherProps(resolved: Record<string, unknown>): Record<string, unknown> {
130
- for (const value of Object.values(resolved)) {
131
- const content = (value as { puck_data?: { content?: unknown } } | null)?.puck_data
132
- ?.content;
133
- if (!Array.isArray(content)) continue;
134
- const launcher = (content as Array<{ type: string; props?: Record<string, unknown> }>).find(
135
- (block) => block.type === REGISTERED_LAUNCHER_SLUG,
136
- );
137
- if (launcher) return launcher.props ?? {};
138
- }
139
- throw new Error("no launcher block was attached to any region");
140
- }
141
-
142
- function bodyBlocks(resolved: Record<string, unknown>) {
143
- return (
144
- resolved.body as {
145
- puck_data: { content: Array<{ type: string; props?: Record<string, unknown> }> };
146
- }
147
- ).puck_data.content;
148
- }
149
-
150
- /**
151
- * REGION SLOTS COME FROM THE LOADER ROW, exactly as the caller reads them —
152
- * the injector is never allowed to guess which resolved keys hold content.
153
- */
154
- const assessmentOpts = {
155
- resourceKey: "response",
156
- agentConfigSlug: "assessment-assistant-agent" as never,
157
- launchLabel: "Ask about your results",
158
- regionSlots: ["header", "body", "footer"],
159
- };
160
-
161
- describe("region-slot pages (a generated report, not an authored document)", () => {
162
- test("attaches to a declared region when there is no `page` source", async () => {
163
- // THE DEFECT THIS PINS: the injector read resolved.page.puck_data.content
164
- // and returned early when absent. A generated report resolves region
165
- // slots instead, so the early return fired and the assistant was
166
- // silently missing — fail-soft, so nothing was logged.
167
- const resolved = makeAssessmentResolved();
168
- await injectResourceChatLauncher(resolved, "reader@example.com", assessmentOpts);
169
- // The first declared slot holding a content tree. The launcher is
170
- // position:fixed, so any rendered region shows it identically.
171
- const header = (
172
- resolved.header as { puck_data: { content: Array<{ type: string }> } }
173
- ).puck_data.content;
174
- expect(header.map((block) => block.type)).toContain(REGISTERED_LAUNCHER_SLUG);
175
- });
176
-
177
- test("follows the loader's slot order rather than a hardcoded key", async () => {
178
- // Reordering the row must move the launcher. If this passes while the
179
- // code names "body", the derivation is a literal wearing a costume.
180
- const resolved = makeAssessmentResolved();
181
- await injectResourceChatLauncher(resolved, "reader@example.com", {
182
- ...assessmentOpts,
183
- regionSlots: ["body", "header", "footer"],
184
- });
185
- expect(bodyBlocks(resolved).map((block) => block.type)).toContain(
186
- REGISTERED_LAUNCHER_SLUG,
187
- );
188
- });
189
-
190
- test("sends the assessment RESULT, not just the rendered page", async () => {
191
- // A report's body page is a shared template — one builda_pages row serves
192
- // every respondent — so the content tree holds zero personal facts. An
193
- // assistant given only the page would know nothing about this reader.
194
- const resolved = makeAssessmentResolved();
195
- await injectResourceChatLauncher(resolved, "reader@example.com", assessmentOpts);
196
- const context = String(launcherProps(resolved).runtimeContext ?? "");
197
- expect(context).toContain("total_score");
198
- expect(context).toContain("95");
199
- expect(context).toContain("Be Chosen");
200
- expect(context).toContain("You are strongest at being seen.");
201
- expect(context).toContain("The Launcha Authority Index");
202
- });
203
-
204
- test("sends the rendered document too, not just the result", async () => {
205
- // The mirror of the test above: the reader is asking about what is on
206
- // screen, so the assistant needs the document as well as the data.
207
- const resolved = makeAssessmentResolved();
208
- (
209
- resolved.body as { puck_data: { content: unknown[] } }
210
- ).puck_data.content.push({ type: "ScoreDial", props: { label: "Authority" } });
211
- await injectResourceChatLauncher(resolved, "reader@example.com", assessmentOpts);
212
- const context = String(launcherProps(resolved).runtimeContext ?? "");
213
- expect(context).toContain("ScoreDial");
214
- expect(context).toContain("Authority");
215
- });
216
-
217
- test("names no resolved key — it sends whatever the loader produced", async () => {
218
- // THE REGRESSION THIS PINS: a draft excluded brand/website/header/footer
219
- // by name, which is a loader row copied into TypeScript. Those keys are
220
- // database facts, so the serialiser must not have opinions about them —
221
- // it walks the graph and a loader edit propagates with no code change.
222
- const resolved = makeAssessmentResolved();
223
- await injectResourceChatLauncher(resolved, "reader@example.com", assessmentOpts);
224
- const context = String(launcherProps(resolved).runtimeContext ?? "");
225
- for (const key of Object.keys(resolved)) {
226
- expect(context).toContain(key);
227
- }
228
- });
229
-
230
- test("carries a source a future loader adds, with no edit here", async () => {
231
- // The serialiser walks the graph rather than naming keys, so a loader
232
- // resolving a new source reaches the assistant for free.
233
- const resolved = makeAssessmentResolved();
234
- resolved.benchmarks = { industryMedian: 62 };
235
- await injectResourceChatLauncher(resolved, "reader@example.com", assessmentOpts);
236
- const context = String(launcherProps(resolved).runtimeContext ?? "");
237
- expect(context).toContain("industryMedian");
238
- expect(context).toContain("62");
239
- });
240
- });