@teamscala/service-boot 0.23.387 → 0.23.389

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.387",
9
+ "version": "0.23.389",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * The AGREEMENT content gate, expressed as config for the content-gate primitive
package/src/auth-gate.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system server-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * Auth middleware factory. Three modes, same BetterAuth email-OTP mechanism:
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * ORPC procedures for the AUTHENTICATED AI-chat conversation lifecycle —
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system service-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * Public ORPC procedure for submitting a public assessment (quiz) response.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system server-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * The `platform-fault-escalation` pg-boss handler — closes the "receiving is
@@ -37,9 +37,9 @@ import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
37
37
  * hanging off one shape: an unhandled variant then fails to compile instead of
38
38
  * silently taking the npm branch. The previous single-member form
39
39
  * (`type: "npm_package"`) made a GitHub-released binary UNREPRESENTABLE, not
40
- * merely unregistered — which is why codex and zerostack are absent from
41
- * `config/scala-singleton-tools` entirely. Both are standalone ELF binaries in
42
- * ~/.local/bin and neither is npm-managed, so neither could be described at all.
40
+ * merely unregistered — which is why standalone-ELF CLIs were absent from
41
+ * `config/scala-singleton-tools` entirely. They live in ~/.local/bin and are
42
+ * not npm-managed, so they could not be described at all.
43
43
  *
44
44
  * Their LOCATION is correct and must not be "fixed": `bun-bin-is-unstable`
45
45
  * designates ${HOME}/.local/bin as the home for operator CLIs precisely so they
@@ -53,7 +53,7 @@ type VersionSource =
53
53
  dist_tag: string;
54
54
  }
55
55
  | {
56
- /** A GitHub release asset — the shape codex and zerostack ship as. */
56
+ /** A GitHub release asset — the shape standalone-ELF CLIs ship as. */
57
57
  type: "github_release";
58
58
  /** "owner/repo". */
59
59
  repo: string;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Locks the mount candidate-chain regression (2026-08-24): scala-agents-ui
7
+ * crash-looped at boot because @teamscala/cli-session exports ./boot/mount.ts
8
+ * and not ./mount.ts. The chain already listed boot/mount.ts as a fallback,
9
+ * but Bun names an unexported subpath by its BARE package, so the first
10
+ * candidate's miss was classified as a real error and boot died before the
11
+ * fallback ran. The whole value of the chain is that candidate 1 may be absent.
12
+ */
13
+
14
+ import { describe, expect, it } from "bun:test";
15
+ import { barePackageOf, isModuleNotFound } from "./module-not-found";
16
+
17
+ describe("barePackageOf", () => {
18
+ it("keeps the scope for scoped packages", () => {
19
+ expect(barePackageOf("@teamscala/cli-session/mount.ts")).toBe(
20
+ "@teamscala/cli-session",
21
+ );
22
+ expect(barePackageOf("@teamscala/cli-session")).toBe(
23
+ "@teamscala/cli-session",
24
+ );
25
+ });
26
+
27
+ it("takes the first segment for unscoped packages", () => {
28
+ expect(barePackageOf("hono/mount.ts")).toBe("hono");
29
+ expect(barePackageOf("hono")).toBe("hono");
30
+ });
31
+ });
32
+
33
+ describe("isModuleNotFound", () => {
34
+ // The exact shape Bun emits for an unexported subpath: it names the
35
+ // PACKAGE, never the subpath that was asked for.
36
+ const bunSubpathMiss = {
37
+ message:
38
+ "Cannot find package '@teamscala/cli-session' imported from /srv/node_modules/@teamscala/service-boot/src/mount-server-packages.ts",
39
+ };
40
+
41
+ it("treats an unexported subpath as not-found so the chain falls through", () => {
42
+ expect(
43
+ isModuleNotFound(bunSubpathMiss, "@teamscala/cli-session/mount.ts"),
44
+ ).toBe(true);
45
+ });
46
+
47
+ it("still matches when the error names the full specifier", () => {
48
+ expect(
49
+ isModuleNotFound(
50
+ { message: "Cannot find module '@teamscala/x/mount.ts'" },
51
+ "@teamscala/x/mount.ts",
52
+ ),
53
+ ).toBe(true);
54
+ });
55
+
56
+ it("does NOT swallow a transitive miss of a DIFFERENT module", () => {
57
+ // A file inside the candidate failing to resolve something else must
58
+ // surface as a real error, or the true cause hides behind "not found".
59
+ expect(
60
+ isModuleNotFound(
61
+ { message: "Cannot find package '@teamscala/some-other-dep'" },
62
+ "@teamscala/cli-session/mount.ts",
63
+ ),
64
+ ).toBe(false);
65
+ });
66
+
67
+ it("does NOT swallow a syntax or runtime error", () => {
68
+ expect(
69
+ isModuleNotFound(
70
+ new SyntaxError("Unexpected token"),
71
+ "@teamscala/cli-session/mount.ts",
72
+ ),
73
+ ).toBe(false);
74
+ });
75
+
76
+ it("reads non-Error throwables defensively (Bun ResolveMessage is not an Error)", () => {
77
+ expect(
78
+ isModuleNotFound(
79
+ "Cannot find package '@teamscala/cli-session'",
80
+ "@teamscala/cli-session/mount.ts",
81
+ ),
82
+ ).toBe(true);
83
+ });
84
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @system service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Classifies a failed dynamic import as "this candidate is absent" vs a real
7
+ * error, for the server_package mount candidate chain. Extracted from
8
+ * mount-server-packages so the classification is testable: as a closure it
9
+ * could not be exercised, and it shipped wrong.
10
+ */
11
+
12
+ /** `@scope/name/sub/path.ts` -> `@scope/name`; `name/sub` -> `name`. */
13
+ export function barePackageOf(specifier: string): string {
14
+ const parts = specifier.split("/");
15
+ return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
16
+ }
17
+
18
+ /**
19
+ * True when the import failed because `requestedPath` itself is absent.
20
+ *
21
+ * Bun reports an unexported SUBPATH as `Cannot find package '<bare>'`, naming
22
+ * the package rather than the subpath, so the bare name of THIS candidate
23
+ * counts as not-found. Without that the first candidate's miss is
24
+ * misclassified as fatal and the fallbacks never run (measured 2026-08-24:
25
+ * scala-agents-ui crash-looped because cli-session exports ./boot/mount.ts and
26
+ * not ./mount.ts). A transitive miss naming a DIFFERENT module still surfaces.
27
+ */
28
+ export function isModuleNotFound(err: unknown, requestedPath: string): boolean {
29
+ const msg =
30
+ (err as { message?: string })?.message ??
31
+ (typeof err === "string" ? err : String(err));
32
+ const bare = barePackageOf(requestedPath);
33
+ return (
34
+ msg.includes(`Cannot find module '${requestedPath}'`) ||
35
+ msg.includes(`Cannot find package '${requestedPath}'`) ||
36
+ msg.includes(`Cannot find module '${bare}'`) ||
37
+ msg.includes(`Cannot find package '${bare}'`)
38
+ );
39
+ }
@@ -20,6 +20,7 @@ import {
20
20
  type MountRegistry,
21
21
  } from "@teamscala/os/runtime-contracts/mount-context";
22
22
  import type { Hono } from "hono";
23
+ import { isModuleNotFound } from "./module-not-found";
23
24
 
24
25
  type ServerPackageMount = (ctx: MountContext) => Promise<void> | void;
25
26
 
@@ -68,19 +69,7 @@ export async function mountServerPackages(
68
69
  return mountRegistry;
69
70
  }
70
71
 
71
- const isModuleNotFound = (err: unknown, requestedPath: string): boolean => {
72
- // Bun's ResolveMessage is not an Error subclass — read message defensively.
73
- // Match ONLY when the missing module is the candidate path itself; transitive
74
- // not-found errors (e.g. a file inside the candidate fails to resolve a
75
- // different module) must surface, not be swallowed as "not found here".
76
- const msg =
77
- (err as { message?: string })?.message ??
78
- (typeof err === "string" ? err : String(err));
79
- return (
80
- msg.includes(`Cannot find module '${requestedPath}'`) ||
81
- msg.includes(`Cannot find package '${requestedPath}'`)
82
- );
83
- };
72
+
84
73
 
85
74
  for (const pkg of serverPackages) {
86
75
  try {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system ui-service-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * Converts the resolved loader-graph into the pageData shape the renderer
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @system ui-service-boot
3
- * @status handwritten
3
+ * @status handwritten @indivisible-unit
4
4
  * @edit edit directly
5
5
  *
6
6
  * Reads `page_route` registry rows for the current service slug and registers