@teamscala/service-boot 0.23.376 → 0.23.378

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.
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.376",
9
+ "version": "0.23.378",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -85,3 +85,45 @@ export function resolveTemplateTitle(
85
85
  });
86
86
  return result || undefined;
87
87
  }
88
+
89
+ /**
90
+ * THE ONE SUBJECT DERIVATION. What a page is ABOUT — the entity whose name the
91
+ * browser tab should carry.
92
+ *
93
+ * This existed as THREE hand-written `||` chains, one per render branch in
94
+ * resolve-page-data, and they had already diverged: two ordered form before
95
+ * proposal, the third ordered proposal first. So which resource named the page
96
+ * depended on which branch happened to render it, silently.
97
+ *
98
+ * The divergence is the cheap half of the cost. The expensive half is that a
99
+ * correction has to be applied three times to work, and applying it twice looks
100
+ * exactly like applying it correctly — measured 2026-08-17, when a fix to the
101
+ * quiz subject was shipped, installed and verified present in the bundle while
102
+ * the page kept its old title, because the branch that rendered it held the
103
+ * second copy.
104
+ *
105
+ * One chain, one precedence, one place to change. Adding a resource type is an
106
+ * edit here rather than a fourth branch in three files.
107
+ */
108
+ export function resolveEntityTitle(resolved: Record<string, unknown>): string | undefined {
109
+ const pick = (key: string, ...fields: string[]): string | undefined => {
110
+ const row = resolved[key] as Record<string, unknown> | undefined;
111
+ if (!row) return undefined;
112
+ for (const field of fields) {
113
+ const value = row[field];
114
+ if (typeof value === "string" && value.trim()) return value.trim();
115
+ }
116
+ return undefined;
117
+ };
118
+ return (
119
+ pick("form", "name") ||
120
+ pick("proposal", "name") ||
121
+ // A report surface carries its own document name in `report_title`
122
+ // ("Personalised Authority Blueprint"); `title` names the DIAGNOSTIC
123
+ // ("The Launcha Authority Index"), which is the wrong subject for the tab
124
+ // of a document sent to a client. The landing page has no report_title
125
+ // and correctly falls through to the quiz title.
126
+ pick("quiz", "report_title", "title") ||
127
+ pick("agent", "name")
128
+ );
129
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Locks the single subject derivation that replaced three divergent copies.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { resolveEntityTitle } from "./meta-title.ts";
11
+
12
+ describe("resolveEntityTitle", () => {
13
+ test("a report is named by its own document name, not the diagnostic", () => {
14
+ // The Launcha Blueprint: `title` names the QUIZ, `report_title` names the
15
+ // DOCUMENT the client receives. The tab must carry the document.
16
+ expect(
17
+ resolveEntityTitle({
18
+ quiz: {
19
+ title: "The Launcha Authority Index",
20
+ report_title: "Personalised Authority Blueprint",
21
+ },
22
+ }),
23
+ ).toBe("Personalised Authority Blueprint");
24
+ });
25
+
26
+ test("a quiz with no report name falls through to its title", () => {
27
+ expect(resolveEntityTitle({ quiz: { title: "The Launcha Authority Index" } })).toBe(
28
+ "The Launcha Authority Index",
29
+ );
30
+ });
31
+
32
+ test("precedence is single and fixed across every caller", () => {
33
+ // The three former copies disagreed: two put form first, one put proposal
34
+ // first, so the subject depended on which render branch ran. One order now.
35
+ const all = {
36
+ form: { name: "Form" },
37
+ proposal: { name: "Proposal" },
38
+ quiz: { report_title: "Report" },
39
+ agent: { name: "Agent" },
40
+ };
41
+ expect(resolveEntityTitle(all)).toBe("Form");
42
+ expect(resolveEntityTitle({ ...all, form: undefined })).toBe("Proposal");
43
+ expect(resolveEntityTitle({ ...all, form: undefined, proposal: undefined })).toBe("Report");
44
+ });
45
+
46
+ test("blank and whitespace-only names do not win", () => {
47
+ expect(resolveEntityTitle({ form: { name: " " }, agent: { name: "Agent" } })).toBe("Agent");
48
+ });
49
+
50
+ test("undefined when nothing names the page", () => {
51
+ expect(resolveEntityTitle({})).toBeUndefined();
52
+ expect(resolveEntityTitle({ quiz: {} })).toBeUndefined();
53
+ });
54
+ });
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { resolveThemeMode } from "../resolve-theme-mode.ts";
12
12
  import { composePageTitle } from "../compose-page-title.ts";
13
+ import { resolveEntityTitle } from "./meta-title.ts";
13
14
  import { getAppLogger } from "@teamscala/logger/app-loggers";
14
15
 
15
16
  /**
@@ -124,11 +125,7 @@ export function resolvedToPageData(
124
125
  // name in <title>/OG, not the generic "Portal". Falls back to the website
125
126
  // name, then "Portal". Without this, the region-slots branch returned
126
127
  // early with a hardcoded "Portal" title (gpt-test: SSR/DOM/OG title FAIL).
127
- const entityTitle =
128
- (resolved.form as { name?: string })?.name ||
129
- (resolved.quiz as { title?: string })?.title ||
130
- (resolved.proposal as { name?: string })?.name ||
131
- (resolved.agent as { name?: string })?.name;
128
+ const entityTitle = resolveEntityTitle(resolved);
132
129
 
133
130
  const regions: RegionEntry[] = [];
134
131
  for (const slot of regionSlots) {
@@ -306,11 +303,7 @@ export function resolvedToPageData(
306
303
  const overrideBody = pickRootProp<string>(regions, "bodyFont");
307
304
 
308
305
  const pageName = (page.name as string) || null;
309
- const entityTitle =
310
- (resolved.proposal as { name?: string })?.name ||
311
- (resolved.form as { name?: string })?.name ||
312
- (resolved.quiz as { title?: string })?.title ||
313
- (resolved.agent as { name?: string })?.name;
306
+ const entityTitle = resolveEntityTitle(resolved);
314
307
  const bodyMetaTitle = regions.find((r) => r.slot === "body")?.metaTitle;
315
308
  const pageRootTitle = pickRootProp<string>(regions, "title");
316
309
  const pageDescription =
@@ -438,11 +431,7 @@ export function resolvedToPageData(
438
431
  const screenMode = pageRoot?.screenMode as string | undefined;
439
432
 
440
433
  const pageName = (page?.name as string) || null;
441
- const entityTitle =
442
- (resolved.form as { name?: string })?.name ||
443
- (resolved.quiz as { title?: string })?.title ||
444
- (resolved.proposal as { name?: string })?.name ||
445
- (resolved.agent as { name?: string })?.name;
434
+ const entityTitle = resolveEntityTitle(resolved);
446
435
  const entityDescription =
447
436
  ((resolved.form as Record<string, unknown>)?.description as string) ||
448
437
  ((resolved.quiz as Record<string, unknown>)?.description as string) ||