@teamscala/service-boot 0.23.372 → 0.23.374

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.372",
9
+ "version": "0.23.374",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -65,10 +65,10 @@ describe("decideSweep — thresholds", () => {
65
65
  });
66
66
 
67
67
  test("the 490-over-12h shape escalates (duration + recurrence)", () => {
68
- const sustained = agg("host-command-bus.error", 490, 720, 0);
68
+ const sustained = agg("host-command-bus.command.failed", 490, 720, 0);
69
69
  const decision = decideSweep([sustained], [], POLICY, NOW);
70
70
  expect(decision.escalate).toHaveLength(1);
71
- expect(decision.escalate[0]!.aggregate.kind).toBe("host-command-bus.error");
71
+ expect(decision.escalate[0]!.aggregate.kind).toBe("host-command-bus.command.failed");
72
72
  expect(decision.escalate[0]!.episode).toBeNull();
73
73
  });
74
74
 
@@ -85,9 +85,9 @@ describe("decideSweep — thresholds", () => {
85
85
  });
86
86
 
87
87
  test("override raises a floor too — bursty kinds can be tightened", () => {
88
- const sustained = agg("llm.provider.error", 40, 60, 0); // passes default
88
+ const sustained = agg("llm.provider.call.failed", 40, 60, 0); // passes default
89
89
  const decision = decideSweep([sustained], [], POLICY, NOW, {
90
- "llm.provider.error": { minCount: 100 },
90
+ "llm.provider.call.failed": { minCount: 100 },
91
91
  });
92
92
  expect(decision.escalate).toHaveLength(0);
93
93
  });
@@ -95,16 +95,16 @@ describe("decideSweep — thresholds", () => {
95
95
 
96
96
  describe("decideSweep — dedup (one escalation per fault, no re-fire)", () => {
97
97
  test("already escalated and unresolved does NOT re-escalate", () => {
98
- const sustained = agg("host-command-bus.error", 500, 720, 0);
99
- const open = episode({ kind: "host-command-bus.error", escalatedAt: new Date(NOW.getTime() - 3_600_000) });
98
+ const sustained = agg("host-command-bus.command.failed", 500, 720, 0);
99
+ const open = episode({ kind: "host-command-bus.command.failed", escalatedAt: new Date(NOW.getTime() - 3_600_000) });
100
100
  const decision = decideSweep([sustained], [open], POLICY, NOW);
101
101
  expect(decision.escalate).toHaveLength(0);
102
102
  });
103
103
 
104
104
  test("open delivered episode past the update window gets an UPDATE, not a message", () => {
105
- const sustained = agg("llm.provider.error", 300, 720, 0);
105
+ const sustained = agg("llm.provider.call.failed", 300, 720, 0);
106
106
  const open = episode({
107
- kind: "llm.provider.error",
107
+ kind: "llm.provider.call.failed",
108
108
  escalatedAt: new Date(NOW.getTime() - 90 * 60_000), // last update long ago
109
109
  lastUpdatedAt: new Date(NOW.getTime() - 90 * 60_000),
110
110
  });
@@ -115,9 +115,9 @@ describe("decideSweep — dedup (one escalation per fault, no re-fire)", () => {
115
115
  });
116
116
 
117
117
  test("open episode inside the update window gets NOTHING (dedup window)", () => {
118
- const sustained = agg("llm.provider.error", 300, 720, 0);
118
+ const sustained = agg("llm.provider.call.failed", 300, 720, 0);
119
119
  const open = episode({
120
- kind: "llm.provider.error",
120
+ kind: "llm.provider.call.failed",
121
121
  escalatedAt: new Date(NOW.getTime() - 10 * 60_000),
122
122
  lastUpdatedAt: new Date(NOW.getTime() - 10 * 60_000),
123
123
  });
@@ -150,9 +150,9 @@ describe("decideSweep — resolution + re-arm", () => {
150
150
  });
151
151
 
152
152
  test("after resolution the same fault escalates again (new episode)", () => {
153
- const recurring = agg("host-command-bus.error", 50, 40, 0);
153
+ const recurring = agg("host-command-bus.command.failed", 50, 40, 0);
154
154
  const resolved = episode({
155
- kind: "host-command-bus.error",
155
+ kind: "host-command-bus.command.failed",
156
156
  resolvedAt: new Date(NOW.getTime() - 35 * 60_000),
157
157
  });
158
158
  const decision = decideSweep([recurring], [resolved], POLICY, NOW);
@@ -190,12 +190,12 @@ describe("extractGowaMessageId — delivery verification", () => {
190
190
  describe("composeEscalationMessage", () => {
191
191
  test("carries the three escalation-routing fields and stays phone-sized", () => {
192
192
  const message = composeEscalationMessage({
193
- kind: "host-command-bus.error",
194
- aggregate: agg("host-command-bus.error", 490, 720, 0),
193
+ kind: "host-command-bus.command.failed",
194
+ aggregate: agg("host-command-bus.command.failed", 490, 720, 0),
195
195
  impact: "scala-tools verbs are failing host-wide",
196
196
  recommendation: "check oxmgr logs for the bus consumer",
197
197
  });
198
- expect(message).toContain("Platform fault: host-command-bus.error");
198
+ expect(message).toContain("Platform fault: host-command-bus.command.failed");
199
199
  expect(message).toContain("Observed: 490 errors over 12 h");
200
200
  expect(message).toContain("Impact: scala-tools verbs are failing host-wide");
201
201
  expect(message).toContain("Recommendation: check oxmgr logs for the bus consumer");
@@ -5,9 +5,10 @@
5
5
  *
6
6
  * The `platform-fault-escalation` pg-boss handler — closes the "receiving is
7
7
  * decoupled from consequence" gap measured 2026-08-15 (760 error events over
8
- * 12 hours, zero escalations: host-command-bus.error 490, llm.provider.error
9
- * 231, guard.fail.open 16, scale-to-zero.wake_failed 12, listen.connect_failed
10
- * 11). Emission works and observability-is-passive is satisfied; nothing
8
+ * 12 hours, zero escalations: host-command-bus.command.failed (then spelled
9
+ * host-command-bus.error) 490, llm.provider.call.failed (then spelled
10
+ * llm.provider.error) 231, guard.fail.open 16, scale-to-zero.wake_failed 12,
11
+ * listen.connect_failed 11). Emission works and observability-is-passive is satisfied; nothing
11
12
  * acted. This sweep makes RECEIVING have CONSEQUENCE: sustained platform
12
13
  * faults escalate to the OPERATOR over the existing PA path (WhatsApp, the
13
14
  * scala PA operator-relay bot — escalation-routing's channel; no second
@@ -599,6 +599,73 @@ export async function registerRegistryPageRoute(
599
599
  // Branding fetch failed — keep the default brand (no regression).
600
600
  }
601
601
  }
602
+ // BRAND FROM THE ORG THAT OWNS THE RESOURCE, not the website's owner.
603
+ // A public resource page (an assessment report, a shared document) is
604
+ // hosted on a shared website but BELONGS to a tenant. resolvePageBrand
605
+ // reads pageData.orgId, which is the WEBSITE's org, so every tenant's
606
+ // report rendered the host org's logo and palette.
607
+ //
608
+ // Measured 2026-08-17: the Launcha Authority Blueprint is owned by GMG
609
+ // APAC (#FF4500, launcha logo) and rendered with GBG Indo's navy and
610
+ // scala mark, because both pages live on the shared "Public Pages"
611
+ // site. The loader had already resolved the right org — nothing
612
+ // consumed it.
613
+ //
614
+ // `brand_org_path` names where the owning org id lives in the resolved
615
+ // loader data (e.g. "quiz.organisation_id"), so which resource owns the
616
+ // brand is a ROW, never a per-service branch
617
+ // (`business-logic-is-rows-plus-executors`). Absent or unresolvable,
618
+ // the website's org stands — this can only ever narrow the brand to a
619
+ // more specific owner, never blank it.
620
+ const brandOrgPath = (routeConfig as { brand_org_path?: string }).brand_org_path;
621
+ if (brandOrgPath) {
622
+ let cursor: unknown = resolved;
623
+ for (const key of brandOrgPath.split(".")) {
624
+ if (cursor == null || typeof cursor !== "object") {
625
+ cursor = undefined;
626
+ break;
627
+ }
628
+ cursor = (cursor as Record<string, unknown>)[key];
629
+ }
630
+ if (typeof cursor === "string" && cursor.length > 0) {
631
+ pageData.orgId = cursor;
632
+ // orgId alone is not enough. resolvePageBrand fills only the
633
+ // accent/secondary/muted gaps from the org; primaryColor, logo and
634
+ // companyName come from pageData.brand, which was built from the
635
+ // website's org. Replace those too, the same way the session
636
+ // override above does, or the page keeps the host's logo while
637
+ // claiming the tenant's org.
638
+ try {
639
+ const owner = await fetchOrganisationPageBranding(
640
+ getServerClient() as never,
641
+ cursor,
642
+ );
643
+ if (owner) {
644
+ const existing = (pageData.brand as Record<string, unknown>) ?? {};
645
+ const existingLogo = (existing.logo as Record<string, unknown>) ?? {};
646
+ pageData.brand = {
647
+ ...existing,
648
+ companyName: owner.company_name,
649
+ companyFriendlyName: owner.company_name,
650
+ primaryColor: owner.brand_primary_color ?? existing.primaryColor,
651
+ headingFont: owner.brand_heading_font ?? existing.headingFont,
652
+ bodyFont: owner.brand_body_font ?? existing.bodyFont,
653
+ logo: {
654
+ light: owner.company_logo_for_light_theme,
655
+ dark: owner.company_logo_for_dark_theme,
656
+ brand: existingLogo.brand ?? null,
657
+ },
658
+ };
659
+ }
660
+ } catch {
661
+ // Branding fetch failed — keep the website brand (no regression).
662
+ }
663
+ } else {
664
+ log.warn(
665
+ `[scala-install:${serviceSlug}] brand_org_path "${brandOrgPath}" resolved no org id for ${params.path}; keeping the website org brand`,
666
+ );
667
+ }
668
+ }
602
669
  warnUncoveredBlocks(pageData, components, serviceSlug, params.path, log);
603
670
  // Fire the AI-chat conversation prefetch NOW (in parallel with the
604
671
  // proposal/partner/client post-processors below) so its ~eager cost is