@malloy-publisher/server 0.0.225 → 0.0.227

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 (98) hide show
  1. package/README.md +2 -11
  2. package/dist/app/api-doc.yaml +29 -24
  3. package/dist/app/assets/{EnvironmentPage-DYTeXDll.js → EnvironmentPage-DvOJ7L_b.js} +1 -1
  4. package/dist/app/assets/{HomePage-pDK2BPJY.js → HomePage-CXguJsXS.js} +1 -1
  5. package/dist/app/assets/{LightMode-C2bwGPY1.js → LightMode-ZsshUznu.js} +1 -1
  6. package/dist/app/assets/{MainPage-WtBulMH_.js → MainPage-BIe0VwBa.js} +2 -2
  7. package/dist/app/assets/{MaterializationsPage-hMgOtflG.js → MaterializationsPage-BuZ6UJVx.js} +1 -1
  8. package/dist/app/assets/{ModelPage-B2N5kYII.js → ModelPage-DsPf-s8B.js} +1 -1
  9. package/dist/app/assets/{PackagePage-CEN90nQG.js → PackagePage-CEVNAKZa.js} +1 -1
  10. package/dist/app/assets/{RouteError-BG2c5Zf0.js → RouteError-Chn7lL96.js} +1 -1
  11. package/dist/app/assets/{ThemeEditorPage-DNfeUwEZ.js → ThemeEditorPage-DWC_FdNU.js} +1 -1
  12. package/dist/app/assets/{WorkbookPage-NKI1BhFS.js → WorkbookPage-CGrsFz8p.js} +1 -1
  13. package/dist/app/assets/{core-C6anj5c0.es-DDLHqpzt.js → core-vVgoO8IR.es-BD_THWs_.js} +1 -1
  14. package/dist/app/assets/{index-DMQtnaf4.js → index-BioohWQj.js} +1 -1
  15. package/dist/app/assets/{index-CzNqKMVl.js → index-D6YtyiJ0.js} +1 -1
  16. package/dist/app/assets/{index-C6gZ6sSY.js → index-DNUZpnaa.js} +4 -4
  17. package/dist/app/assets/{index-JXgvyZna.js → index-gEWxu09x.js} +1 -1
  18. package/dist/app/index.html +1 -1
  19. package/dist/instrumentation.mjs +71 -15
  20. package/dist/package_load_worker.mjs +169 -40
  21. package/dist/server.mjs +782 -7050
  22. package/package.json +1 -4
  23. package/src/controller/package.controller.spec.ts +17 -17
  24. package/src/controller/package.controller.ts +7 -6
  25. package/src/health.ts +3 -8
  26. package/src/logger.spec.ts +193 -0
  27. package/src/logger.ts +115 -18
  28. package/src/mcp/error_messages.ts +8 -37
  29. package/src/mcp/handler_utils.ts +10 -129
  30. package/src/mcp/mcp_constants.ts +0 -16
  31. package/src/mcp/{agent_server.protocol.spec.ts → server.protocol.spec.ts} +14 -12
  32. package/src/mcp/server.ts +29 -37
  33. package/src/mcp/skills/build_skills_bundle.ts +1 -1
  34. package/src/mcp/skills/skills_bundle.json +1 -1
  35. package/src/mcp/tools/docs_search_tool.ts +1 -1
  36. package/src/mcp/tools/execute_query_tool.ts +2 -2
  37. package/src/mcp/tools/get_context_eval.ts +3 -3
  38. package/src/mcp/tools/get_context_tool.spec.ts +196 -1
  39. package/src/mcp/tools/get_context_tool.ts +165 -67
  40. package/src/package_load/package_load_pool.ts +8 -1
  41. package/src/package_load/package_load_worker.ts +75 -24
  42. package/src/package_load/protocol.ts +21 -1
  43. package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
  44. package/src/package_load/rpc_wait_accountant.ts +76 -0
  45. package/src/package_load_metrics.spec.ts +114 -0
  46. package/src/package_load_metrics.ts +127 -0
  47. package/src/pg_helpers.spec.ts +2 -206
  48. package/src/pg_helpers.ts +4 -120
  49. package/src/server.ts +0 -16
  50. package/src/service/build_plan.spec.ts +42 -60
  51. package/src/service/build_plan.ts +37 -61
  52. package/src/service/environment.ts +5 -1
  53. package/src/service/materialization_test_fixtures.ts +6 -8
  54. package/src/service/package.ts +165 -86
  55. package/src/service/package_manifest.spec.ts +22 -1
  56. package/src/service/package_manifest.ts +29 -0
  57. package/src/service/persistence_policy.spec.ts +234 -0
  58. package/src/test_helpers/metrics_harness.ts +40 -0
  59. package/tests/harness/mcp_test_setup.ts +1 -1
  60. package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
  61. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
  62. package/dxt/malloy_bridge.py +0 -354
  63. package/dxt/manifest.json +0 -22
  64. package/src/dto/connection.dto.spec.ts +0 -186
  65. package/src/dto/connection.dto.ts +0 -308
  66. package/src/dto/index.ts +0 -2
  67. package/src/dto/package.dto.spec.ts +0 -42
  68. package/src/dto/package.dto.ts +0 -27
  69. package/src/dto/validate.spec.ts +0 -76
  70. package/src/dto/validate.ts +0 -31
  71. package/src/ducklake_version.spec.ts +0 -43
  72. package/src/ducklake_version.ts +0 -26
  73. package/src/mcp/agent_server.spec.ts +0 -18
  74. package/src/mcp/agent_server.ts +0 -144
  75. package/src/mcp/prompts/handlers.ts +0 -84
  76. package/src/mcp/prompts/index.ts +0 -11
  77. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  78. package/src/mcp/prompts/prompt_service.ts +0 -67
  79. package/src/mcp/prompts/utils.ts +0 -62
  80. package/src/mcp/resource_metadata.ts +0 -47
  81. package/src/mcp/resources/environment_resource.ts +0 -187
  82. package/src/mcp/resources/model_resource.ts +0 -155
  83. package/src/mcp/resources/notebook_resource.ts +0 -137
  84. package/src/mcp/resources/package_resource.ts +0 -373
  85. package/src/mcp/resources/query_resource.ts +0 -122
  86. package/src/mcp/resources/source_resource.ts +0 -141
  87. package/src/mcp/resources/view_resource.ts +0 -136
  88. package/src/mcp/tools/discovery_tools.ts +0 -280
  89. package/src/service/materialization_cron_gate.spec.ts +0 -181
  90. package/src/storage/BaseRepository.ts +0 -31
  91. package/src/storage/StorageManager.mock.ts +0 -50
  92. package/tests/harness/e2e.ts +0 -96
  93. package/tests/harness/mocks.ts +0 -39
  94. package/tests/harness/uris.ts +0 -31
  95. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
  96. package/tests/integration/mcp/setup.spec.ts +0 -5
  97. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  98. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
@@ -1,105 +1,5 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import { ConnectionAuthError } from "./errors";
3
- import {
4
- classifyPgError,
5
- handlePgAttachError,
6
- pgConnectTimeoutSeconds,
7
- redactPgSecrets,
8
- withPgConnectTimeout,
9
- } from "./pg_helpers";
10
-
11
- describe("pgConnectTimeoutSeconds", () => {
12
- const ORIGINAL_TIMEOUT = process.env.PG_CONNECT_TIMEOUT_SECONDS;
13
-
14
- afterEach(() => {
15
- if (ORIGINAL_TIMEOUT === undefined) {
16
- delete process.env.PG_CONNECT_TIMEOUT_SECONDS;
17
- } else {
18
- process.env.PG_CONNECT_TIMEOUT_SECONDS = ORIGINAL_TIMEOUT;
19
- }
20
- });
21
-
22
- it("defaults to 5 when env unset", () => {
23
- delete process.env.PG_CONNECT_TIMEOUT_SECONDS;
24
- expect(pgConnectTimeoutSeconds()).toBe(5);
25
- });
26
-
27
- it("honors PG_CONNECT_TIMEOUT_SECONDS override", () => {
28
- process.env.PG_CONNECT_TIMEOUT_SECONDS = "12";
29
- expect(pgConnectTimeoutSeconds()).toBe(12);
30
- });
31
-
32
- it("falls back to 5 when env value is invalid", () => {
33
- process.env.PG_CONNECT_TIMEOUT_SECONDS = "not-a-number";
34
- expect(pgConnectTimeoutSeconds()).toBe(5);
35
- });
36
-
37
- it("falls back to 5 when env value is zero or negative", () => {
38
- process.env.PG_CONNECT_TIMEOUT_SECONDS = "0";
39
- expect(pgConnectTimeoutSeconds()).toBe(5);
40
- process.env.PG_CONNECT_TIMEOUT_SECONDS = "-3";
41
- expect(pgConnectTimeoutSeconds()).toBe(5);
42
- });
43
- });
44
-
45
- describe("withPgConnectTimeout", () => {
46
- it("appends to keyword form when missing", () => {
47
- expect(withPgConnectTimeout("host=h dbname=d user=u password=p", 5)).toBe(
48
- "host=h dbname=d user=u password=p connect_timeout=5",
49
- );
50
- });
51
-
52
- it("appends to postgres: keyword form (DuckLake catalogUrl shape)", () => {
53
- expect(
54
- withPgConnectTimeout("postgres:host=h user=u password=p dbname=d", 5),
55
- ).toBe("postgres:host=h user=u password=p dbname=d connect_timeout=5");
56
- });
57
-
58
- it("does not override a user-supplied connect_timeout in keyword form", () => {
59
- expect(withPgConnectTimeout("host=h connect_timeout=30", 99)).toBe(
60
- "host=h connect_timeout=30",
61
- );
62
- });
63
-
64
- it("appends to URI form with no query", () => {
65
- expect(withPgConnectTimeout("postgresql://u:p@h:5432/d", 5)).toBe(
66
- "postgresql://u:p@h:5432/d?connect_timeout=5",
67
- );
68
- });
69
-
70
- it("appends to URI form with existing query", () => {
71
- expect(
72
- withPgConnectTimeout("postgresql://u:p@h/d?sslmode=require", 5),
73
- ).toBe("postgresql://u:p@h/d?sslmode=require&connect_timeout=5");
74
- });
75
-
76
- it("appends to URI with bare trailing ?", () => {
77
- expect(withPgConnectTimeout("postgresql://h/d?", 5)).toBe(
78
- "postgresql://h/d?connect_timeout=5",
79
- );
80
- });
81
-
82
- it("does not double-append when URI already has connect_timeout (?-style)", () => {
83
- expect(
84
- withPgConnectTimeout("postgresql://h/d?connect_timeout=10", 5),
85
- ).toBe("postgresql://h/d?connect_timeout=10");
86
- });
87
-
88
- it("does not double-append when URI already has connect_timeout (&-style)", () => {
89
- expect(
90
- withPgConnectTimeout(
91
- "postgresql://h/d?sslmode=require&connect_timeout=10",
92
- 5,
93
- ),
94
- ).toBe("postgresql://h/d?sslmode=require&connect_timeout=10");
95
- });
96
-
97
- it("recognizes postgres:// (alternative scheme) as URI form", () => {
98
- expect(withPgConnectTimeout("postgres://u@h/d", 5)).toBe(
99
- "postgres://u@h/d?connect_timeout=5",
100
- );
101
- });
102
- });
1
+ import { describe, expect, it } from "bun:test";
2
+ import { redactPgSecrets } from "./pg_helpers";
103
3
 
104
4
  describe("redactPgSecrets", () => {
105
5
  it("redacts bare password values", () => {
@@ -120,107 +20,3 @@ describe("redactPgSecrets", () => {
120
20
  );
121
21
  });
122
22
  });
123
-
124
- describe("classifyPgError", () => {
125
- it.each([
126
- 'password authentication failed for user "alice"',
127
- "no pg_hba.conf entry for host",
128
- 'role "alice" does not exist',
129
- 'database "billing" does not exist',
130
- "permission denied for relation foo",
131
- ])("classifies '%s' as auth error", (msg) => {
132
- const result = classifyPgError(new Error(msg), "PG attach");
133
- expect(result).toBeInstanceOf(ConnectionAuthError);
134
- expect(result?.message).toContain("PG attach:");
135
- });
136
-
137
- it("returns undefined for unrelated errors", () => {
138
- expect(
139
- classifyPgError(
140
- new Error('relation "users" does not exist'),
141
- "PG attach",
142
- ),
143
- ).toBeUndefined();
144
- expect(
145
- classifyPgError(new Error("connection reset by peer"), "PG attach"),
146
- ).toBeUndefined();
147
- });
148
-
149
- it("returns undefined for non-Error values", () => {
150
- expect(
151
- classifyPgError("password authentication failed", "ctx"),
152
- ).toBeUndefined();
153
- expect(classifyPgError(undefined, "ctx")).toBeUndefined();
154
- });
155
-
156
- it("redacts embedded passwords in the wrapped message", () => {
157
- const result = classifyPgError(
158
- new Error(
159
- "password authentication failed: tried host=h password=hunter2",
160
- ),
161
- "DuckLake attach",
162
- );
163
- expect(result?.message).toContain("password=***");
164
- expect(result?.message).not.toContain("hunter2");
165
- });
166
- });
167
-
168
- describe("handlePgAttachError", () => {
169
- it("swallows 'already exists' errors", () => {
170
- const outcome = handlePgAttachError(
171
- new Error('database "db_x" already exists'),
172
- "ctx",
173
- );
174
- expect(outcome.action).toBe("swallow");
175
- });
176
-
177
- it("swallows 'already attached' errors", () => {
178
- const outcome = handlePgAttachError(
179
- new Error("DuckLake catalog db_x is already attached"),
180
- "ctx",
181
- );
182
- expect(outcome.action).toBe("swallow");
183
- });
184
-
185
- it("classifies libpq auth failures as ConnectionAuthError", () => {
186
- const outcome = handlePgAttachError(
187
- new Error('password authentication failed for user "alice"'),
188
- "PG attach db_x",
189
- );
190
- expect(outcome.action).toBe("throw");
191
- if (outcome.action === "throw") {
192
- expect(outcome.error).toBeInstanceOf(ConnectionAuthError);
193
- expect(outcome.error.message).toContain("PG attach db_x:");
194
- }
195
- });
196
-
197
- it("passes through unrelated Error instances unchanged", () => {
198
- const original = new Error("network unreachable");
199
- const outcome = handlePgAttachError(original, "ctx");
200
- expect(outcome.action).toBe("throw");
201
- if (outcome.action === "throw") {
202
- expect(outcome.error).toBe(original);
203
- expect(outcome.error).not.toBeInstanceOf(ConnectionAuthError);
204
- }
205
- });
206
-
207
- it("wraps non-Error throwables so callers always get an Error", () => {
208
- const outcome = handlePgAttachError("a string was thrown", "ctx");
209
- expect(outcome.action).toBe("throw");
210
- if (outcome.action === "throw") {
211
- expect(outcome.error).toBeInstanceOf(Error);
212
- expect(outcome.error.message).toBe("a string was thrown");
213
- }
214
- });
215
-
216
- it("prefers 'already attached' over auth classification when both keywords appear", () => {
217
- // Defensive: if a future DuckDB version emits a combined message,
218
- // 'already attached' wins so we don't bubble up a false auth failure
219
- // on what is actually a benign idempotent re-attach.
220
- const outcome = handlePgAttachError(
221
- new Error("already attached; permission denied tail"),
222
- "ctx",
223
- );
224
- expect(outcome.action).toBe("swallow");
225
- });
226
- });
package/src/pg_helpers.ts CHANGED
@@ -1,64 +1,7 @@
1
- // Postgres / libpq helpers shared between `service/` (user-facing
2
- // connections) and `storage/` (materialization-storage catalog). Lives at
3
- // `src/` root so neither layer takes a dependency on the other — see
4
- // CLAUDE.md's "Two parallel DuckLake/PG attach paths" note for why this
5
- // matters.
6
- import { ConnectionAuthError } from "./errors";
7
-
8
- // Default Postgres connect_timeout (seconds), used by the materialization
9
- // storage catalog ATTACH so a slow or wedged libpq handshake fails the
10
- // caller in seconds instead of stalling the worker until the K8s liveness
11
- // probe trips.
12
- //
13
- // libpq enforces a documented minimum of 2 seconds — values below 2
14
- // effectively round up to ~2s wall clock.
15
- export function pgConnectTimeoutSeconds(): number {
16
- const raw = process.env.PG_CONNECT_TIMEOUT_SECONDS;
17
- if (!raw) return 5;
18
- const parsed = Number.parseInt(raw, 10);
19
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 5;
20
- }
21
-
22
- // libpq accepts both keyword=value form ("host=h dbname=d") and URI form
23
- // ("postgresql://u:p@h/d?param=v"). The materialization-storage catalogUrl
24
- // can also arrive as `postgres:<keyword=value>` (no `//`). We detect URI
25
- // form (with `//`) so we know whether to append a new parameter using
26
- // `?`/`&` or a leading space.
27
- const URI_FORM_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
28
-
29
- // Match an existing connect_timeout key in either form. URI form uses
30
- // `?key=` or `&key=`; keyword form uses whitespace separation or start-of-
31
- // string. Without the `[?&]` alternatives a URI-form user-supplied timeout
32
- // would be missed and we'd double-append, producing an invalid URL.
33
- const HAS_CONNECT_TIMEOUT_RE = /[?&\s]connect_timeout=|^connect_timeout=/;
34
-
35
- // Append `connect_timeout=N` to a libpq-compatible connection string if
36
- // the caller hasn't already set one. Handles keyword form ("host=h ..."),
37
- // URI form ("postgresql://..."), and the `postgres:host=h ...` keyword
38
- // form with a scheme prefix used by DuckLake catalogUrls.
39
- export function withPgConnectTimeout(
40
- connectionString: string,
41
- timeout: number,
42
- ): string {
43
- if (HAS_CONNECT_TIMEOUT_RE.test(connectionString)) {
44
- return connectionString;
45
- }
46
- if (URI_FORM_RE.test(connectionString)) {
47
- // URI form: append as query parameter. `?` if no query string yet,
48
- // `&` otherwise. A bare trailing `?` (empty query) gets no extra
49
- // separator. We don't try to handle URL fragments — libpq URIs don't
50
- // use them.
51
- if (!connectionString.includes("?")) {
52
- return `${connectionString}?connect_timeout=${timeout}`;
53
- }
54
- if (connectionString.endsWith("?")) {
55
- return `${connectionString}connect_timeout=${timeout}`;
56
- }
57
- return `${connectionString}&connect_timeout=${timeout}`;
58
- }
59
- // Keyword=value form (with or without `postgres:` scheme prefix).
60
- return `${connectionString} connect_timeout=${timeout}`;
61
- }
1
+ // Postgres / libpq helper shared between `service/` (user-facing connections)
2
+ // and `storage/` (materialization-storage catalog). Lives at `src/` root so
3
+ // neither layer takes a dependency on the other — see CLAUDE.md's "Two
4
+ // parallel DuckLake/PG attach paths" note for why this matters.
62
5
 
63
6
  // Redact libpq `password=...` values from a string before it goes into a
64
7
  // log line or HTTP response body. Handles bare and quoted values.
@@ -68,62 +11,3 @@ export function withPgConnectTimeout(
68
11
  export function redactPgSecrets(s: string): string {
69
12
  return s.replace(/password=('[^']*'|"[^"]*"|\S+)/gi, "password=***");
70
13
  }
71
-
72
- // Substring-match libpq error patterns that indicate a non-retryable
73
- // auth/permission failure. Returns a ConnectionAuthError when matched so
74
- // callers can fast-fail with HTTP 422 (semantically "the supplied creds
75
- // are bad; don't retry") instead of letting the raw error fall through to
76
- // a generic 500 that retry loops treat as transient.
77
- export function classifyPgError(
78
- error: unknown,
79
- context: string,
80
- ): ConnectionAuthError | undefined {
81
- if (!(error instanceof Error)) return undefined;
82
- const msg = error.message;
83
- const patterns = [
84
- /password authentication failed/i,
85
- /pg_hba\.conf/i,
86
- /role ".*" does not exist/i,
87
- /database ".*" does not exist/i,
88
- /permission denied/i,
89
- ];
90
- if (!patterns.some((p) => p.test(msg))) return undefined;
91
- return new ConnectionAuthError(`${context}: ${redactPgSecrets(msg)}`);
92
- }
93
-
94
- // Outcome of inspecting an error thrown by an `ATTACH` call:
95
- // - `{ action: "swallow" }`: DuckDB reported the db is already attached
96
- // (idempotent re-attach); caller should log and continue.
97
- // - `{ action: "throw", error: ConnectionAuthError }`: classified as a
98
- // non-retryable auth failure; caller should warn-log and throw it.
99
- // - `{ action: "throw", error: <original> }`: unrecognized; caller
100
- // should rethrow as-is to preserve the original cause for diagnosis.
101
- //
102
- // Extracted so the decision tree gets a direct unit test without needing
103
- // to stub DuckDB or run a real ATTACH.
104
- export type PgAttachErrorOutcome =
105
- | { action: "swallow" }
106
- | { action: "throw"; error: Error };
107
-
108
- export function handlePgAttachError(
109
- error: unknown,
110
- context: string,
111
- ): PgAttachErrorOutcome {
112
- if (
113
- error instanceof Error &&
114
- (error.message.includes("already exists") ||
115
- error.message.includes("already attached"))
116
- ) {
117
- return { action: "swallow" };
118
- }
119
- const authErr = classifyPgError(error, context);
120
- if (authErr) {
121
- return { action: "throw", error: authErr };
122
- }
123
- if (error instanceof Error) {
124
- return { action: "throw", error };
125
- }
126
- // Non-Error thrown values get wrapped so the catch contract stays
127
- // (always throws an Error).
128
- return { action: "throw", error: new Error(String(error)) };
129
- }
package/src/server.ts CHANGED
@@ -41,7 +41,6 @@ import { queryConcurrency } from "./query_concurrency";
41
41
  import { MaterializationController } from "./controller/materialization.controller";
42
42
  import { ThemeController } from "./controller/theme.controller";
43
43
  import { initializeMcpServer } from "./mcp/server";
44
- import { startAgentMcpServer } from "./mcp/agent_server";
45
44
  import { registerLegacyRoutes } from "./server-old";
46
45
  import { EnvironmentStore } from "./service/environment_store";
47
46
  import { MaterializationService } from "./service/materialization_service";
@@ -50,8 +49,6 @@ import { PackageMemoryGovernor } from "./service/package_memory_governor";
50
49
  import { ThemeStore } from "./service/theme_store";
51
50
  import { assertSafePackageName, safeJoinUnderRoot } from "./path_safety";
52
51
 
53
- export { normalizeQueryArray } from "./query_param_utils";
54
-
55
52
  // Parse command line arguments
56
53
  function parseArgs() {
57
54
  const args = process.argv.slice(2);
@@ -162,7 +159,6 @@ parseArgs();
162
159
  const PUBLISHER_PORT = Number(process.env.PUBLISHER_PORT || 4000);
163
160
  const PUBLISHER_HOST = process.env.PUBLISHER_HOST || "0.0.0.0";
164
161
  const MCP_PORT = Number(process.env.MCP_PORT || 4040);
165
- const AGENT_MCP_PORT = Number(process.env.AGENT_MCP_PORT || 4041);
166
162
  const MCP_ENDPOINT = "/mcp";
167
163
  const SHUTDOWN_DRAIN_DURATION_SECONDS = Number(
168
164
  process.env.SHUTDOWN_DRAIN_DURATION_SECONDS || 0,
@@ -1868,21 +1864,9 @@ mcpServer.timeout = 600000;
1868
1864
  mcpServer.keepAliveTimeout = 600000;
1869
1865
  mcpServer.headersTimeout = 600000;
1870
1866
 
1871
- // Separate, isolated MCP server for the agent retrieval tools (get_context,
1872
- // search_docs) on its own listener. Kept apart from the core MCP server above.
1873
- const agentMcpServer = startAgentMcpServer(
1874
- environmentStore,
1875
- PUBLISHER_HOST,
1876
- AGENT_MCP_PORT,
1877
- );
1878
- agentMcpServer.timeout = 600000;
1879
- agentMcpServer.keepAliveTimeout = 600000;
1880
- agentMcpServer.headersTimeout = 600000;
1881
-
1882
1867
  registerSignalHandlers(
1883
1868
  mainServer,
1884
1869
  mcpServer,
1885
1870
  SHUTDOWN_DRAIN_DURATION_SECONDS,
1886
1871
  SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS,
1887
- agentMcpServer,
1888
1872
  );
@@ -10,7 +10,7 @@ import {
10
10
  deriveBuildPlan,
11
11
  flattenDependsOn,
12
12
  iterGraphSources,
13
- resolveFreshnessSchedule,
13
+ resolveFreshness,
14
14
  resolvePackageConnections,
15
15
  } from "./build_plan";
16
16
  import { fakeSource } from "./materialization_test_fixtures";
@@ -213,10 +213,10 @@ describe("deriveBuildPlan", () => {
213
213
  });
214
214
  });
215
215
 
216
- it("reports declared sharing/refresh verbatim and null when unset", () => {
217
- // The control plane distinguishes unset from an explicit `shared` (it
218
- // applies the platform default itself), so the publisher must report
219
- // the declared value verbatim never substitute the default.
216
+ it("reports declared refresh verbatim (null when unset) and does not emit sharing/schedule", () => {
217
+ // `refresh` is a metadata pass-through; `sharing`/`schedule` were retired
218
+ // from the contract and must not be emitted as typed fields (they stay in
219
+ // the raw annotationFields for the publish-time validator to detect).
220
220
  const declared = fakeSource({
221
221
  name: "declared",
222
222
  sourceEntityId: "bid-d",
@@ -243,17 +243,22 @@ describe("deriveBuildPlan", () => {
243
243
  { duckdb: "dig" },
244
244
  );
245
245
 
246
- expect(plan.sources["declared@m"].sharing).toBe("private");
247
246
  expect(plan.sources["declared@m"].refresh).toBe("incremental");
248
- // The raw annotation map still carries every field alongside the typed
249
- // projections.
247
+ // Retired typed fields are absent from the wire projection.
248
+ expect(
249
+ (plan.sources["declared@m"] as Record<string, unknown>).sharing,
250
+ ).toBeUndefined();
251
+ expect(
252
+ (plan.sources["declared@m"] as Record<string, unknown>).schedule,
253
+ ).toBeUndefined();
254
+ // The raw annotation map still carries every field (so the validator can
255
+ // reject a source-level sharing/schedule at publish).
250
256
  expect(plan.sources["declared@m"].annotationFields).toEqual({
251
257
  name: "d_table",
252
258
  sharing: "private",
253
259
  refresh: "incremental",
254
260
  });
255
- // Unset is null — not "shared" — on the wire.
256
- expect(plan.sources["unset@m"].sharing).toBeNull();
261
+ // Unset refresh is null on the wire.
257
262
  expect(plan.sources["unset@m"].refresh).toBeNull();
258
263
  });
259
264
 
@@ -297,71 +302,48 @@ describe("deriveBuildPlan", () => {
297
302
  });
298
303
  });
299
304
 
300
- describe("resolveFreshnessSchedule", () => {
301
- it("reports source-level freshness + schedule verbatim", () => {
305
+ describe("resolveFreshness", () => {
306
+ it("reports source-level freshness verbatim", () => {
302
307
  const source = fakeSource({
303
308
  name: "s",
304
309
  sourceEntityId: "bid",
305
310
  freshnessSchedule: {
306
311
  freshness: { window: "1h", fallback: "stale_ok" },
307
- schedule: "0 */6 * * *",
308
312
  },
309
313
  });
310
- expect(resolveFreshnessSchedule(source, null)).toEqual({
311
- freshness: { window: "1h", fallback: "stale_ok" },
312
- schedule: "0 */6 * * *",
314
+ expect(resolveFreshness(source, null)).toEqual({
315
+ window: "1h",
316
+ fallback: "stale_ok",
313
317
  });
314
318
  });
315
319
 
316
- it("returns null for both when unset at every level", () => {
320
+ it("returns null when unset at every level", () => {
317
321
  const source = fakeSource({ name: "s", sourceEntityId: "bid" });
318
- expect(resolveFreshnessSchedule(source, null)).toEqual({
319
- freshness: null,
320
- schedule: null,
321
- });
322
+ expect(resolveFreshness(source, null)).toBeNull();
322
323
  });
323
324
 
324
325
  it("falls back to model-file then package per field (most-specific-wins)", () => {
325
- // freshness.window from source, freshness.fallback from model-file. The
326
- // package-level cron is NOT inherited as the source's effective schedule;
327
- // schedule here comes only from the source's own model-file default.
326
+ // freshness.window from source, freshness.fallback from model-file.
328
327
  const source = fakeSource({
329
328
  name: "s",
330
329
  sourceEntityId: "bid",
331
330
  freshnessSchedule: { freshness: { window: "1h" } },
332
- modelFreshnessSchedule: {
333
- freshness: { fallback: "fail" },
334
- schedule: "0 3 * * *",
335
- },
331
+ modelFreshnessSchedule: { freshness: { fallback: "fail" } },
336
332
  });
337
333
  const pkg = {
338
- schedule: "0 0 * * *",
334
+ schedule: null,
339
335
  freshness: { window: "24h", fallback: "live" as const },
340
336
  };
341
- expect(resolveFreshnessSchedule(source, pkg)).toEqual({
342
- freshness: { window: "1h", fallback: "fail" },
343
- schedule: "0 3 * * *",
344
- });
345
- });
346
-
347
- it("does not inherit the package-level cron as the source's schedule", () => {
348
- const source = fakeSource({ name: "s", sourceEntityId: "bid" });
349
- const pkg = { schedule: "0 0 * * *", freshness: { window: "24h" } };
350
- // Freshness inherits from the package; schedule stays null (the package
351
- // cron is gated separately, not folded into the per-source cron).
352
- expect(resolveFreshnessSchedule(source, pkg)).toEqual({
353
- freshness: { window: "24h" },
354
- schedule: null,
337
+ expect(resolveFreshness(source, pkg)).toEqual({
338
+ window: "1h",
339
+ fallback: "fail",
355
340
  });
356
341
  });
357
342
 
358
343
  it("inherits the package freshness when the source and model are unset", () => {
359
344
  const source = fakeSource({ name: "s", sourceEntityId: "bid" });
360
345
  const pkg = { schedule: null, freshness: { window: "24h" } };
361
- expect(resolveFreshnessSchedule(source, pkg)).toEqual({
362
- freshness: { window: "24h" },
363
- schedule: null,
364
- });
346
+ expect(resolveFreshness(source, pkg)).toEqual({ window: "24h" });
365
347
  });
366
348
 
367
349
  it("drops an invalid fallback rather than defaulting it", () => {
@@ -370,23 +352,17 @@ describe("resolveFreshnessSchedule", () => {
370
352
  sourceEntityId: "bid",
371
353
  freshnessSchedule: { freshness: { window: "1h", fallback: "bogus" } },
372
354
  });
373
- expect(resolveFreshnessSchedule(source, null)).toEqual({
374
- freshness: { window: "1h" },
375
- schedule: null,
376
- });
355
+ expect(resolveFreshness(source, null)).toEqual({ window: "1h" });
377
356
  });
378
357
  });
379
358
 
380
- describe("deriveBuildPlan freshness/schedule", () => {
381
- it("projects the resolved per-source freshness + schedule onto the plan", () => {
359
+ describe("deriveBuildPlan freshness", () => {
360
+ it("projects the resolved per-source freshness onto the plan (no schedule/sharing)", () => {
382
361
  const source = fakeSource({
383
362
  name: "s",
384
363
  sourceEntityId: "bid",
385
- annotationFields: { name: "s_table", sharing: "private" },
386
- freshnessSchedule: {
387
- freshness: { window: "1h" },
388
- schedule: "0 */6 * * *",
389
- },
364
+ annotationFields: { name: "s_table" },
365
+ freshnessSchedule: { freshness: { window: "1h" } },
390
366
  });
391
367
  const plan = deriveBuildPlan(
392
368
  [
@@ -406,12 +382,18 @@ describe("deriveBuildPlan freshness/schedule", () => {
406
382
  );
407
383
 
408
384
  // Source window wins over the package default; package fallback fills the
409
- // unset source fallback; source schedule reported verbatim.
385
+ // unset source fallback.
410
386
  expect(plan.sources["s@m"].freshness).toEqual({
411
387
  window: "1h",
412
388
  fallback: "live",
413
389
  });
414
- expect(plan.sources["s@m"].schedule).toBe("0 */6 * * *");
390
+ // Retired fields are not emitted.
391
+ expect(
392
+ (plan.sources["s@m"] as Record<string, unknown>).schedule,
393
+ ).toBeUndefined();
394
+ expect(
395
+ (plan.sources["s@m"] as Record<string, unknown>).sharing,
396
+ ).toBeUndefined();
415
397
  });
416
398
  });
417
399