@powerhousedao/codegen 6.2.2-dev.8 → 6.2.2-dev.81

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.
@@ -5,21 +5,23 @@ import path, { join } from "path";
5
5
  import { DocumentModelGlobalStateSchema, ManifestSchema } from "@powerhousedao/shared/document-model";
6
6
  import fs, { copyFile, mkdir, readdir, writeFile } from "node:fs/promises";
7
7
  import { camelCase, capitalCase, constantCase, kebabCase, pascalCase } from "change-case";
8
+ import { DEFAULT_SWITCHBOARD_PORT } from "@powerhousedao/shared/clis/constants";
9
+ import { deriveProjectPorts } from "@powerhousedao/shared/clis/project-ports";
8
10
  import { DEFAULT_CONNECT_CONFIG } from "@powerhousedao/shared/connect";
9
11
  import { DEFAULT_REGISTRY_URL } from "@powerhousedao/shared/registry";
10
12
  import { capitalize, concat, conditional, constant, endsWith, filter, find, first, flatMap, forEach, isDefined, isIncludedIn, isNonNullish, isNot, isStrictEqual, isString, isTruthy, last, map, mapValues, merge, pipe, prop, sort, split, startsWith, subtract, unique, uniqueBy, when } from "remeda";
11
- import { IndentationText, Project, SyntaxKind, VariableDeclarationKind, ts } from "ts-morph";
13
+ import { FileSystemRefreshResult, IndentationText, Project, SyntaxKind, VariableDeclarationKind, ts } from "ts-morph";
12
14
  import arg from "arg";
13
15
  import { format } from "oxfmt";
14
16
  import { z } from "zod";
15
17
  import { loadJsonFile, loadJsonFileSync } from "load-json-file";
18
+ import { existsSync, realpathSync } from "node:fs";
16
19
  import { writeJsonFile } from "write-json-file";
17
20
  import { stripVTControlCharacters } from "node:util";
18
21
  import path$1, { join as join$1, relative } from "node:path";
19
22
  import { generate } from "@graphql-codegen/cli";
20
- import { generatorTypeDefs, validationSchema } from "@powerhousedao/document-engineering/graphql";
21
- import { Kind, parse } from "graphql";
22
- import { realpathSync } from "node:fs";
23
+ import { generatorTypeDefs, getPHCustomScalarByTypeName, validationSchema } from "@powerhousedao/document-engineering/graphql";
24
+ import { Kind, parse, print } from "graphql";
23
25
  import { fileURLToPath, pathToFileURL } from "node:url";
24
26
  function isEOL(c) {
25
27
  return c === 10 || c === 13;
@@ -2153,12 +2155,22 @@ This project creates document models, editors, processors and subgraphs for the
2153
2155
  - **Reactor Package**: A deployable bundle that extends the Reactor. It contains one or more document models, editors, processors, and subgraphs. A Vetra project generates a Reactor Package.
2154
2156
  - **Connect**: The Powerhouse web application for document management. End users open Connect to browse drives, create documents, and interact with editors.
2155
2157
  - **Switchboard**: The Powerhouse API service. It exposes GraphQL and MCP endpoints so external tools can read/write documents programmatically.
2156
- - **Vetra**: The local development environment for building Reactor Packages. It includes Vetra Studio (a local Connect instance) and Vetra Switchboard (a local Switchboard with reactor-mcp). Start it with \`ph vetra\`.
2158
+ - **Vetra**: The local development environment for building Reactor Packages. It includes Vetra Studio (a local Connect instance) and Vetra Switchboard (a local Switchboard with reactor-mcp). **You start it yourself** with \`ph vetra --strictPort --watch\` as a background process — see the MCP rules below. Its ports are assigned per project in \`powerhouse.config.json\`, so they never collide with another project's.
2157
2159
 
2158
2160
  ## CRITICAL: MCP Tool Usage Rules
2159
2161
 
2160
2162
  **MANDATORY**: The \`reactor-mcp\` MUST BE USED when handling documents or document-models for the Powerhouse/Vetra ecosystem.
2161
- If the \`reactor-mcp\` server is unavailable, ask the user to run \`ph vetra\` on a separate terminal to start the server and try to reconnect to the MCP server, DO NOT run it yourself.
2163
+ If the \`reactor-mcp\` server is unavailable, **start Vetra yourself** do not ask the user to do it:
2164
+
2165
+ 1. **Check for a running instance.** If \`.ph/vetra-runtime.json\` exists and describes a live instance, Vetra is already up: report its Connect URL and stop. Never start a second one — a project must have exactly one reactor, because two would contend for the same \`.ph/\` storage and leave you with two drives that diverge.
2166
+ 2. **Start it in the background:** \`ph vetra --strictPort --watch\`. Pass no port flags. The project's ports live in \`powerhouse.config.json\` and are already what \`.mcp.json\` points at; choosing your own port is how you end up talking to a different project's reactor. \`--strictPort\` makes a genuine conflict fail loudly instead of silently binding a neighbouring port.
2167
+ 3. **Wait for readiness.** Poll \`GET http://localhost:<port>/ready\` until it returns 200, for at most 60 seconds, where \`<port>\` is the one in \`.mcp.json\`. Poll that port specifically: it is the only one your MCP client will ever use, so a timeout there is the correct signal even if Vetra came up somewhere else.
2168
+ 4. **Tell the user the Connect URL** so they can watch their editors render.
2169
+ 5. **Reconnect to the MCP server** and continue the task.
2170
+
2171
+ Escalate to the user only when startup actually fails — \`--strictPort\` rejected the port, \`/ready\` never returned 200, or the process crashed — and include the real error output.
2172
+
2173
+ **Never edit \`.mcp.json\` to chase a port.** A mismatch between it and the running switchboard is a bug worth reporting, not routing around; \`ph vetra\` owns that reconciliation and will rewrite the file itself when a local override moves the port.
2162
2174
 
2163
2175
  ### Key Requirements:
2164
2176
 
@@ -2881,16 +2893,18 @@ const claudeSettingsLocalTemplate = json`
2881
2893
  `.raw;
2882
2894
  //#endregion
2883
2895
  //#region src/templates/boilerplate/cursor/mcp.json.ts
2884
- const cursorMcpTemplate = json`
2896
+ /** Cursor's copy of the MCP config. Same literal-port reasoning as `.mcp.json`. */
2897
+ const buildCursorMcpTemplate = (port) => json`
2885
2898
  {
2886
2899
  "mcpServers": {
2887
2900
  "reactor-mcp": {
2888
2901
  "type": "http",
2889
- "url": "http://localhost:4001/mcp"
2902
+ "url": "http://localhost:${String(port)}/mcp"
2890
2903
  }
2891
2904
  }
2892
2905
  }
2893
2906
  `.raw;
2907
+ const cursorMcpTemplate = buildCursorMcpTemplate(DEFAULT_SWITCHBOARD_PORT);
2894
2908
  //#endregion
2895
2909
  //#region src/templates/boilerplate/docker/connect-entrypoint.sh.ts
2896
2910
  const connectEntrypointTemplate = `#!/bin/sh
@@ -4446,16 +4460,25 @@ const { updateLocalPackage } = startConnect(localPackage);
4446
4460
  `.raw;
4447
4461
  //#endregion
4448
4462
  //#region src/templates/boilerplate/mcp.json.ts
4449
- const mcpTemplate = json`
4463
+ /**
4464
+ * Render `.mcp.json` for a project whose switchboard listens on `port`.
4465
+ *
4466
+ * The port is a literal rather than an env reference on purpose: an MCP
4467
+ * client resolves this file at session start and cannot read `.env` or
4468
+ * `powerhouse.config.json`. `ph vetra` reconciles the literal if an override
4469
+ * moves the port.
4470
+ */
4471
+ const buildMcpTemplate = (port) => json`
4450
4472
  {
4451
4473
  "mcpServers": {
4452
4474
  "reactor-mcp": {
4453
4475
  "type": "http",
4454
- "url": "http://localhost:4001/mcp"
4476
+ "url": "http://localhost:${String(port)}/mcp"
4455
4477
  }
4456
4478
  }
4457
4479
  }
4458
4480
  `.raw;
4481
+ const mcpTemplate = buildMcpTemplate(DEFAULT_SWITCHBOARD_PORT);
4459
4482
  //#endregion
4460
4483
  //#region src/templates/boilerplate/npmrc.ts
4461
4484
  const npmrcTemplate = `@jsr:registry=https://npm.jsr.io`;
@@ -4482,9 +4505,9 @@ const packageJsonTemplate = (projectName, peerDependencies, devDependencies) =>
4482
4505
  ${exportsTemplate}
4483
4506
  },
4484
4507
  "scripts": {
4485
- "test": "vitest run",
4508
+ "test": "vitest run --passWithNoTests",
4486
4509
  "test:watch": "vitest",
4487
- "test:coverage": "vitest run --coverage",
4510
+ "test:coverage": "vitest run --coverage --passWithNoTests",
4488
4511
  "lint": "oxlint --type-aware --type-check",
4489
4512
  "lint:fix": "oxlint --type-aware --type-check --fix",
4490
4513
  "format": "oxfmt",
@@ -4578,22 +4601,25 @@ const packageJsonExportsTemplate = {
4578
4601
  * see fully-defined values instead of `undefined`.
4579
4602
  */
4580
4603
  async function buildPowerhouseConfigTemplate(args) {
4604
+ const ports = deriveProjectPorts(args.name);
4581
4605
  const config = {
4582
4606
  $schema: "https://raw.githubusercontent.com/powerhouse-inc/powerhouse/main/packages/shared/clis/source-config.schema.json",
4583
4607
  documentModelsDir: "./document-models",
4584
4608
  editorsDir: "./editors",
4585
4609
  processorsDir: "./processors",
4586
4610
  subgraphsDir: "./subgraphs",
4587
- studio: { port: 3e3 },
4588
- reactor: { port: 4001 },
4611
+ studio: { port: ports.studioPort },
4612
+ reactor: { port: ports.switchboardPort },
4589
4613
  packages: [],
4590
4614
  packageRegistryUrl: DEFAULT_REGISTRY_URL,
4591
4615
  connect: DEFAULT_CONNECT_CONFIG
4592
4616
  };
4593
- if (args.remoteDrive) config.vetra = {
4594
- driveId: args.remoteDrive.split("/").pop() ?? "",
4595
- driveUrl: args.remoteDrive
4596
- };
4617
+ const vetra = { connectPort: ports.vetraConnectPort };
4618
+ if (args.remoteDrive) {
4619
+ vetra.driveId = args.remoteDrive.split("/").pop() ?? "";
4620
+ vetra.driveUrl = args.remoteDrive;
4621
+ }
4622
+ config.vetra = vetra;
4597
4623
  return `${JSON.stringify(config, null, 2)}\n`;
4598
4624
  }
4599
4625
  //#endregion
@@ -5185,36 +5211,88 @@ ${buildCreatorsExports(v.specification.modules, v.camelCaseDocumentType)}
5185
5211
  `.raw;
5186
5212
  //#endregion
5187
5213
  //#region src/templates/document-model/gen/document-schema.ts
5188
- const documentModelDocumentSchemaFileTemplate = (v) => ts$1`
5214
+ function priorVersions(versions, currentVersion) {
5215
+ return versions.filter((k) => k < currentVersion);
5216
+ }
5217
+ function makeOlderStateSchemaImports(versions, currentVersion, phStateName) {
5218
+ return priorVersions(versions, currentVersion).map((k) => `import { ${phStateName}Schema as ${phStateName}SchemaV${k} } from "../../v${k}/gen/document-schema.js";`).join("\n");
5219
+ }
5220
+ function makeStateSchemasByVersion(versions, currentVersion, phStateName) {
5221
+ return `{ ${versions.filter((k) => k <= currentVersion).map((k) => {
5222
+ return `${k}: ${k === currentVersion ? `${phStateName}Schema` : `${phStateName}SchemaV${k}`}`;
5223
+ }).join(", ")} }`;
5224
+ }
5225
+ function makeDocumentSchemasByVersion(versions, currentVersion, v) {
5226
+ return `{ ${versions.filter((k) => k <= currentVersion).map((k) => {
5227
+ if (k === currentVersion) return `${k}: ${v.phDocumentSchemaName}`;
5228
+ return `${k}: z.object({ header: ${v.phDocumentTypeName}HeaderSchema, state: ${v.phStateName}SchemaV${k}, initialState: ${v.phStateName}SchemaV${k} })`;
5229
+ }).join(", ")} }`;
5230
+ }
5189
5231
  /**
5190
- * WARNING: DO NOT EDIT
5191
- * This file is auto-generated and updated by codegen
5192
- */
5193
- import {
5194
- BaseDocumentHeaderSchema,
5195
- BaseDocumentStateSchema,
5196
- } from "document-model";
5197
- import { z } from "zod";
5198
- import { ${v.documentTypeVariableName} } from "./document-type.js";
5199
- import { ${v.stateSchemaName} } from "./schema/zod.js";
5200
- import type { ${v.phDocumentTypeName}, ${v.phStateName} } from "./types.js";
5232
+ * Version-aware validators are only emitted for versions with released
5233
+ * predecessors: a document is validated against the schema of the version it
5234
+ * is stamped with, so a later version may add non-nullable fields without
5235
+ * rejecting documents that have not been upgraded yet.
5236
+ */
5237
+ function makeVersionAwareValidators(v) {
5238
+ return `
5239
+ const ${v.phStateName}SchemasByVersion: Record<number, z.ZodType> = ${makeStateSchemasByVersion(v.versions, v.version, v.phStateName)};
5201
5240
 
5202
- /** Schema for validating the header object of a ${v.pascalCaseDocumentType} document */
5203
- export const ${v.phDocumentTypeName}HeaderSchema = BaseDocumentHeaderSchema.extend({
5204
- documentType: z.literal(${v.documentTypeVariableName}),
5205
- });
5241
+ const ${v.phDocumentSchemaName}sByVersion: Record<number, z.ZodType> = ${makeDocumentSchemasByVersion(v.versions, v.version, v)};
5206
5242
 
5207
- /** Schema for validating the state object of a ${v.pascalCaseDocumentType} document */
5208
- export const ${v.phStateName}Schema = BaseDocumentStateSchema.extend({
5209
- global: ${v.stateSchemaName}(),
5210
- });
5243
+ /** The document model version stamped in a state's document scope. States stamped with 0 or nothing predate versioning and are treated as version 1. */
5244
+ function stampedDocumentModelVersion(state: unknown): number {
5245
+ if (typeof state !== "object" || state === null) return 1;
5246
+ const documentScope = (state as { document?: unknown }).document;
5247
+ if (typeof documentScope !== "object" || documentScope === null) return 1;
5248
+ const version = (documentScope as { version?: unknown }).version;
5249
+ return normalizeDocumentModelVersion(typeof version === "number" ? version : undefined);
5250
+ }
5211
5251
 
5212
- export const ${v.phDocumentSchemaName} = z.object({
5213
- header: ${v.phDocumentTypeName}HeaderSchema,
5214
- state: ${v.phStateName}Schema,
5215
- initialState: ${v.phStateName}Schema,
5216
- });
5252
+ function resolve${v.phStateName}Schema(state: unknown): z.ZodType {
5253
+ const schema = ${v.phStateName}SchemasByVersion[stampedDocumentModelVersion(state)];
5254
+ return schema ?? ${v.phStateName}Schema;
5255
+ }
5256
+
5257
+ function resolve${v.phDocumentSchemaName}(document: unknown): z.ZodType {
5258
+ const state = typeof document === "object" && document !== null
5259
+ ? (document as { state?: unknown }).state
5260
+ : undefined;
5261
+ const schema = ${v.phDocumentSchemaName}sByVersion[stampedDocumentModelVersion(state)];
5262
+ return schema ?? ${v.phDocumentSchemaName};
5263
+ }
5264
+
5265
+ /** Simple helper function to check if a state object is a ${v.pascalCaseDocumentType} document state object. Validates against the schema of the version the state is stamped with. */
5266
+ export function ${v.isPhStateOfTypeFunctionName}(
5267
+ state: unknown,
5268
+ ): state is ${v.phStateName} {
5269
+ return resolve${v.phStateName}Schema(state).safeParse(state).success;
5270
+ }
5271
+
5272
+ /** Simple helper function to assert that a document state object is a ${v.pascalCaseDocumentType} document state object. Validates against the schema of the version the state is stamped with. */
5273
+ export function ${v.assertIsPhStateOfTypeFunctionName}(
5274
+ state: unknown,
5275
+ ): asserts state is ${v.phStateName} {
5276
+ resolve${v.phStateName}Schema(state).parse(state);
5277
+ }
5278
+
5279
+ /** Simple helper function to check if a document is a ${v.pascalCaseDocumentType} document. Validates against the schema of the version the document is stamped with, so documents on older versions remain valid until they are upgraded. */
5280
+ export function ${v.isPhDocumentOfTypeFunctionName}(
5281
+ document: unknown,
5282
+ ): document is ${v.phDocumentTypeName} {
5283
+ return resolve${v.phDocumentSchemaName}(document).safeParse(document).success;
5284
+ }
5217
5285
 
5286
+ /** Simple helper function to assert that a document is a ${v.pascalCaseDocumentType} document. Validates against the schema of the version the document is stamped with, so documents on older versions remain valid until they are upgraded. */
5287
+ export function ${v.assertIsPhDocumentOfTypeFunctionName}(
5288
+ document: unknown,
5289
+ ): asserts document is ${v.phDocumentTypeName} {
5290
+ resolve${v.phDocumentSchemaName}(document).parse(document);
5291
+ }
5292
+ `;
5293
+ }
5294
+ function makeSingleVersionValidators(v) {
5295
+ return `
5218
5296
  /** Simple helper function to check if a state object is a ${v.pascalCaseDocumentType} document state object */
5219
5297
  export function ${v.isPhStateOfTypeFunctionName}(
5220
5298
  state: unknown,
@@ -5242,7 +5320,42 @@ export function ${v.assertIsPhDocumentOfTypeFunctionName}(
5242
5320
  ): asserts document is ${v.phDocumentTypeName} {
5243
5321
  ${v.phDocumentSchemaName}.parse(document);
5244
5322
  }
5245
- `.raw;
5323
+ `;
5324
+ }
5325
+ const documentModelDocumentSchemaFileTemplate = (v) => {
5326
+ const olderImports = makeOlderStateSchemaImports(v.versions, v.version, v.phStateName);
5327
+ const hasPriorVersions = olderImports.length > 0;
5328
+ return ts$1`
5329
+ /**
5330
+ * WARNING: DO NOT EDIT
5331
+ * This file is auto-generated and updated by codegen
5332
+ */
5333
+ import {
5334
+ BaseDocumentHeaderSchema,
5335
+ BaseDocumentStateSchema,${hasPriorVersions ? "\n normalizeDocumentModelVersion," : ""}
5336
+ } from "document-model";
5337
+ import { z } from "zod";
5338
+ import { ${v.documentTypeVariableName} } from "./document-type.js";
5339
+ import { ${v.stateSchemaName} } from "./schema/zod.js";
5340
+ import type { ${v.phDocumentTypeName}, ${v.phStateName} } from "./types.js";
5341
+ ${hasPriorVersions ? olderImports + "\n" : ""}
5342
+ /** Schema for validating the header object of a ${v.pascalCaseDocumentType} document */
5343
+ export const ${v.phDocumentTypeName}HeaderSchema = BaseDocumentHeaderSchema.extend({
5344
+ documentType: z.literal(${v.documentTypeVariableName}),
5345
+ });
5346
+
5347
+ /** Schema for validating the state object of a ${v.pascalCaseDocumentType} document */
5348
+ export const ${v.phStateName}Schema = BaseDocumentStateSchema.extend({
5349
+ global: ${v.stateSchemaName}(),
5350
+ });
5351
+
5352
+ export const ${v.phDocumentSchemaName} = z.object({
5353
+ header: ${v.phDocumentTypeName}HeaderSchema,
5354
+ state: ${v.phStateName}Schema,
5355
+ initialState: ${v.phStateName}Schema,
5356
+ });
5357
+ ${hasPriorVersions ? makeVersionAwareValidators(v) : makeSingleVersionValidators(v)}`.raw;
5358
+ };
5246
5359
  //#endregion
5247
5360
  //#region src/templates/document-model/gen/document-type.ts
5248
5361
  const documentModelDocumentTypeTemplate = (v) => ts$1`
@@ -6198,7 +6311,7 @@ const documentModelOperationsModuleTestFileTemplate = (v) => ts$1`
6198
6311
  */
6199
6312
 
6200
6313
  import { describe, it, expect } from 'vitest';
6201
- import { generateMock } from 'document-model';
6314
+ import { generateMock } from 'document-model/mock';
6202
6315
  import {
6203
6316
  reducer,
6204
6317
  utils,
@@ -6231,11 +6344,66 @@ const upgradeManifestTemplate = (v) => ts$1`
6231
6344
  `.raw;
6232
6345
  //#endregion
6233
6346
  //#region src/templates/document-model/upgrades/upgrade-transition.ts
6234
- const upgradeTransitionTemplate = (v) => ts$1`
6347
+ /**
6348
+ * The generated v{N}.ts upgrade transition. Written once and never
6349
+ * overwritten, so hand edits survive regeneration.
6350
+ *
6351
+ * For mechanical schema changes the migration is derived: fields added in
6352
+ * the new version are initialized (from the new version's initial value or
6353
+ * the schema's zero value) and existing data is preserved. When the change
6354
+ * cannot be derived — a field changed type, a nested type changed shape —
6355
+ * the reducer throws until the migration is hand-written: silently running
6356
+ * a no-op migration would stamp documents with a version whose schema their
6357
+ * state does not satisfy, crashing every consumer that validates them.
6358
+ */
6359
+ const upgradeTransitionTemplate = (v) => {
6360
+ const body = v.plan.kind === "manual" ? manualReducer(v, v.plan.reason) : fillReducer(v, v.plan.fills);
6361
+ return ts$1`
6362
+ /**
6363
+ * WARNING: DO NOT EDIT
6364
+ * This file is auto-generated and updated by codegen
6365
+ */
6235
6366
  import type { Action, PHDocument, UpgradeTransition } from "document-model";
6236
6367
  import type { ${v.phStateName} as StateV${v.version - 1} } from "${v.documentModelImportPath}/v${v.version - 1}";
6237
6368
  import type { ${v.phStateName} as StateV${v.version} } from "${v.documentModelImportPath}/v${v.version}";
6238
6369
 
6370
+ ${body}
6371
+
6372
+ export const v${v.version}: UpgradeTransition = {
6373
+ toVersion: ${v.version},
6374
+ upgradeReducer,
6375
+ description: "",
6376
+ };
6377
+ `.raw;
6378
+ };
6379
+ function manualReducer(v, reason) {
6380
+ const message = `The ${v.documentModelState.id} v${v.version} migration is not implemented: ${reason}. Implement it in document-models/${v.documentModelDirName}/upgrades/v${v.version}.ts.`;
6381
+ return `
6382
+ /*
6383
+ * This migration could not be derived automatically: ${reason}.
6384
+ * Implement it (migrate BOTH state and initialState), then remove the throw.
6385
+ */
6386
+ function upgradeReducer(
6387
+ document: PHDocument<StateV${v.version - 1}>,
6388
+ action: Action,
6389
+ ): PHDocument<StateV${v.version}> {
6390
+ throw new Error(${JSON.stringify(message)});
6391
+ }
6392
+ `;
6393
+ }
6394
+ function fillReducer(v, fills) {
6395
+ const fillConsts = [];
6396
+ const scopeLines = (source) => ["global", "local"].filter((scope) => fills[scope]).map((scope) => ` ${scope}: { ...added${capitalize$1(scope)}Fields, ...document.${source}.${scope} },`).join("\n");
6397
+ for (const scope of ["global", "local"]) {
6398
+ const fill = fills[scope];
6399
+ if (!fill) continue;
6400
+ fillConsts.push(`const added${capitalize$1(scope)}Fields = ${JSON.stringify(fill, null, 2)} satisfies Partial<StateV${v.version}["${scope}"]>;`);
6401
+ }
6402
+ if (fillConsts.length === 0) return `
6403
+ /*
6404
+ * No fields were added between v${v.version - 1} and v${v.version}, so existing state
6405
+ * carries over unchanged.
6406
+ */
6239
6407
  function upgradeReducer(
6240
6408
  document: PHDocument<StateV${v.version - 1}>,
6241
6409
  action: Action,
@@ -6244,13 +6412,37 @@ function upgradeReducer(
6244
6412
  ...document,
6245
6413
  };
6246
6414
  }
6415
+ `;
6416
+ return `
6417
+ ${fillConsts.join("\n\n")}
6247
6418
 
6248
- export const v${v.version}: UpgradeTransition = {
6249
- toVersion: ${v.version},
6250
- upgradeReducer,
6251
- description: "",
6252
- };
6253
- `.raw;
6419
+ /*
6420
+ * Fields added in v${v.version} are initialized from the new version's initial
6421
+ * value (or the schema's zero value); existing data wins for every field
6422
+ * that already existed. Both state and initialState are migrated so a
6423
+ * rebuild from the operation log converges with the stored state.
6424
+ */
6425
+ function upgradeReducer(
6426
+ document: PHDocument<StateV${v.version - 1}>,
6427
+ action: Action,
6428
+ ): PHDocument<StateV${v.version}> {
6429
+ return {
6430
+ ...document,
6431
+ state: {
6432
+ ...document.state,
6433
+ ${scopeLines("state")}
6434
+ },
6435
+ initialState: {
6436
+ ...document.initialState,
6437
+ ${scopeLines("initialState")}
6438
+ },
6439
+ };
6440
+ }
6441
+ `;
6442
+ }
6443
+ function capitalize$1(value) {
6444
+ return value.charAt(0).toUpperCase() + value.slice(1);
6445
+ }
6254
6446
  //#endregion
6255
6447
  //#region src/templates/document-model/utils.ts
6256
6448
  const documentModelUtilsTemplate = ({ phStateName, pascalCaseDocumentType }) => ts$1`
@@ -6268,9 +6460,23 @@ export const utils: DocumentModelUtils<${phStateName}> = { ...genUtils, ...custo
6268
6460
  `.raw;
6269
6461
  //#endregion
6270
6462
  //#region src/templates/processors/utils.ts
6271
- function getDocumentType(documentTypes) {
6272
- if (!documentTypes.length) return `"*"`;
6273
- return documentTypes.map((type) => `"${type}"`).join(", ");
6463
+ const FILTER_FIELD_ORDER = [
6464
+ "branch",
6465
+ "documentId",
6466
+ "documentType",
6467
+ "scope"
6468
+ ];
6469
+ /** Splits, trims and compacts one filter field, so `-t a -t b` and
6470
+ * `-t "a,b"` mean the same thing instead of the latter yielding one value. */
6471
+ function parseFilterValues(values) {
6472
+ return (values ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
6473
+ }
6474
+ /** Renders a generated factory's `ProcessorFilter`, omitting empty fields:
6475
+ * only `documentId` honours `"*"`, so `["*"]` elsewhere matches nothing. */
6476
+ function renderProcessorFilter(fields) {
6477
+ const properties = FILTER_FIELD_ORDER.map((name) => [name, parseFilterValues(fields[name])]).filter(([, values]) => values.length > 0).map(([name, values]) => ` ${name}: [${values.map((value) => `"${value}"`).join(", ")}],`);
6478
+ if (properties.length === 0) return "{}";
6479
+ return `{\n${properties.join("\n")}\n}`;
6274
6480
  }
6275
6481
  //#endregion
6276
6482
  //#region src/templates/processors/analytics/factory.ts
@@ -6288,12 +6494,12 @@ export const ${v.camelCaseName}FactoryBuilder: ProcessorFactoryBuilder = (module
6288
6494
  return [
6289
6495
  {
6290
6496
  processor: new ${v.pascalCaseName}(module.analyticsStore),
6291
- filter: {
6292
- branch: ["main"],
6293
- documentId: ["*"],
6294
- scope: ["*"],
6295
- documentType: [${getDocumentType(v.documentTypes)}],
6296
- },
6497
+ // An omitted field matches every value. Only \`documentId\` honours "*".
6498
+ filter: ${renderProcessorFilter({
6499
+ branch: ["main"],
6500
+ documentId: ["*"],
6501
+ documentType: v.documentTypes
6502
+ })},
6297
6503
  },
6298
6504
  ];
6299
6505
  }
@@ -6347,7 +6553,7 @@ const factoryBuildersTemplate = ts$1`
6347
6553
  * WARNING: DO NOT EDIT
6348
6554
  * This file is auto-generated and updated by codegen
6349
6555
  */
6350
- import type { ProcessorFactoryBuilder } from "@powerhousedao/reactor";
6556
+ import type { ProcessorFactoryBuilder } from "@powerhousedao/reactor-browser";
6351
6557
 
6352
6558
  export const processorFactoryBuilders: ProcessorFactoryBuilder[] = [];
6353
6559
  `.raw;
@@ -6421,16 +6627,22 @@ export const ${v.camelCaseName}FactoryBuilder: ProcessorFactoryBuilder = (module
6421
6627
  namespace,
6422
6628
  );
6423
6629
 
6424
- // Create a filter for the processor
6425
- const filter: ProcessorFilter = {
6426
- branch: ["main"],
6427
- documentId: ["*"],
6428
- documentType: [${getDocumentType(v.documentTypes)}],
6429
- scope: ["global"],
6430
- };
6630
+ // Create a filter for the processor. An omitted field matches every value.
6631
+ // Only \`documentId\` honours "*", so "*" elsewhere would match nothing.
6632
+ const filter: ProcessorFilter = ${renderProcessorFilter({
6633
+ branch: ["main"],
6634
+ documentId: ["*"],
6635
+ documentType: v.documentTypes,
6636
+ scope: ["global"]
6637
+ })};
6431
6638
 
6432
6639
  // Create the processor
6433
6640
  const processor = new ${v.pascalCaseName}(namespace, filter, store);
6641
+
6642
+ // Run the processor's migrations. Nothing in the runtime calls this, so
6643
+ // without it the first write hits a database with no tables.
6644
+ await processor.initAndUpgrade();
6645
+
6434
6646
  return [
6435
6647
  {
6436
6648
  processor,
@@ -6659,8 +6871,47 @@ async function formatSafe(sourceText, parser = "typescript") {
6659
6871
  return sourceText;
6660
6872
  }
6661
6873
  }
6874
+ /**
6875
+ * Formats the current working directory with the pinned oxfmt version.
6876
+ * The pin matters when the project was scaffolded with skipInstall: with no
6877
+ * local oxfmt, a bare `npx oxfmt` downloads the latest release, whose
6878
+ * behavior can drift from the version the boilerplate depends on.
6879
+ */
6662
6880
  async function runOxfmt() {
6663
- await spawnAsync("npx", ["oxfmt", "."]);
6881
+ await spawnAsync("npx", [`oxfmt@${externalDevDependencies.oxfmt}`, "."]);
6882
+ }
6883
+ //#endregion
6884
+ //#region src/utils/generate-mock-import.ts
6885
+ const GENERATE_MOCK_NAME = "generateMock";
6886
+ const GENERATE_MOCK_MODULE_SPECIFIER = "document-model/mock";
6887
+ /**
6888
+ * Points every `generateMock` import at its subpath. An import that also
6889
+ * brings in other names keeps them where they are and gets a separate
6890
+ * `generateMock` import added.
6891
+ */
6892
+ function fixGenerateMockImports(sourceFile) {
6893
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
6894
+ const namedImports = importDeclaration.getNamedImports();
6895
+ const generateMockSpecifier = namedImports.find((namedImport) => namedImport.getName() === GENERATE_MOCK_NAME);
6896
+ if (!generateMockSpecifier) continue;
6897
+ if (importDeclaration.getModuleSpecifierValue() === "document-model/mock") continue;
6898
+ if (namedImports.length === 1 && !importDeclaration.getDefaultImport() && !importDeclaration.getNamespaceImport()) {
6899
+ importDeclaration.setModuleSpecifier(GENERATE_MOCK_MODULE_SPECIFIER);
6900
+ continue;
6901
+ }
6902
+ const alias = generateMockSpecifier.getAliasNode()?.getText();
6903
+ generateMockSpecifier.remove();
6904
+ sourceFile.addImportDeclaration({
6905
+ namedImports: [{
6906
+ name: GENERATE_MOCK_NAME,
6907
+ alias
6908
+ }],
6909
+ moduleSpecifier: GENERATE_MOCK_MODULE_SPECIFIER
6910
+ });
6911
+ }
6912
+ }
6913
+ function hasGenerateMockImport(sourceFile) {
6914
+ return sourceFile.getImportDeclarations().some((importDeclaration) => importDeclaration.getNamedImports().some((namedImport) => namedImport.getName() === GENERATE_MOCK_NAME));
6664
6915
  }
6665
6916
  //#endregion
6666
6917
  //#region src/utils/get-editor-metadata.ts
@@ -6713,20 +6964,52 @@ function getSubgraphMetadata(project, dirName) {
6713
6964
  //#region src/utils/source-files.ts
6714
6965
  /** Gets a SourceFile by name in a ts-morph Project, or creates a new one
6715
6966
  * if none with that path exists.
6967
+ *
6968
+ * "Exists" has to mean *on disk*, not merely "already loaded in this Project".
6969
+ * Projects here are built with `skipAddingFilesFromTsConfig` (see
6970
+ * `buildTsMorphProject`), so they start empty and `getSourceFile` alone reports
6971
+ * every existing file as missing. Combined with `overwrite: true` that silently
6972
+ * discarded whatever the user had written — `alreadyExists` was always `false`,
6973
+ * so builders that scaffold-once-then-amend (tests, reducers, subgraphs,
6974
+ * editors, processors) re-scaffolded from scratch on every run and hand-written
6975
+ * code was lost on `project.save()`.
6976
+ *
6977
+ * Reading the file in when it is on disk but not yet loaded mirrors what
6978
+ * `DirectoryManager.createSourceFile` already does. Files that codegen fully
6979
+ * owns are unaffected: they call `replaceWithText` immediately afterwards.
6980
+ *
6981
+ * The rule has to hold on the cache-hit path too, which is the other half of
6982
+ * the same data loss. `ph vetra` builds one Project per process and reuses it
6983
+ * for every codegen run, and nothing ever refreshes it, so a SourceFile loaded
6984
+ * by an earlier run keeps the text it had then: codegen scaffolds a reducer
6985
+ * with TODO stubs, the user writes the real body on disk, and the next run in
6986
+ * that session still sees the stubs and writes them back on `project.save()`.
6987
+ * A fresh process (`ph generate`) never hit this — the cache miss forced a read
6988
+ * from disk. Refreshing a cached SourceFile before handing it out makes the
6989
+ * long-lived Project behave like a fresh one. Only cached files are refreshed;
6990
+ * `addSourceFileAtPathIfExists` has just read disk, so refreshing that again
6991
+ * would be I/O for a guaranteed `NoChange`.
6716
6992
  */
6717
6993
  function getOrCreateSourceFile(project, filePath) {
6718
6994
  const dirName = path.dirname(filePath);
6719
6995
  if (!project.getDirectory(dirName)) project.createDirectory(dirName);
6720
- const sourceFile = project.getSourceFile(filePath);
6996
+ const cachedSourceFile = project.getSourceFile(filePath);
6997
+ const sourceFile = cachedSourceFile ? refreshCachedSourceFile(cachedSourceFile) : project.addSourceFileAtPathIfExists(filePath);
6721
6998
  if (!sourceFile) return {
6722
6999
  alreadyExists: false,
6723
- sourceFile: project.createSourceFile(filePath, "", { overwrite: true })
7000
+ sourceFile: project.createSourceFile(filePath, "", { overwrite: false })
6724
7001
  };
6725
7002
  return {
6726
7003
  alreadyExists: true,
6727
7004
  sourceFile
6728
7005
  };
6729
7006
  }
7007
+ /** Re-reads a cached SourceFile from disk, `undefined` once it is gone from
7008
+ * disk (ts-morph forgets it then, so the caller re-creates it). */
7009
+ function refreshCachedSourceFile(sourceFile) {
7010
+ if (!sourceFile.isSaved()) return sourceFile;
7011
+ return sourceFile.refreshFromFileSystemSync() === FileSystemRefreshResult.Deleted ? void 0 : sourceFile;
7012
+ }
6730
7013
  /** Gets a Directory by name in a ts-morph Project, or creates a new one
6731
7014
  * if none with that path exists.
6732
7015
  */
@@ -7249,7 +7532,7 @@ async function writeAiConfigFiles(projectDir = process.cwd()) {
7249
7532
  await writeFileEnsuringDir(join(projectDir, ".cursor/mcp.json"), cursorMcpTemplate.trimStart());
7250
7533
  await writeFileEnsuringDir(join(projectDir, ".claude/settings.local.json"), claudeSettingsLocalTemplate.trimStart());
7251
7534
  }
7252
- async function writeProjectRootFiles(args, projectDir = process.cwd()) {
7535
+ async function writeProjectRootFiles(args, _projectDir = process.cwd()) {
7253
7536
  const { name, tag, version, remoteDrive, packageManager } = args;
7254
7537
  await writeFileEnsuringDir("LICENSE", licenseTemplate);
7255
7538
  await writeFileEnsuringDir("README.md", readmeTemplate);
@@ -7261,15 +7544,12 @@ async function writeProjectRootFiles(args, projectDir = process.cwd()) {
7261
7544
  version
7262
7545
  });
7263
7546
  await writeFileEnsuringDir("powerhouse.config.json", await buildPowerhouseConfigTemplate({
7547
+ name,
7264
7548
  tag,
7265
7549
  version,
7266
7550
  remoteDrive
7267
7551
  }));
7268
7552
  await writeFileEnsuringDir("package.json", packageJson);
7269
- await applyProjectCustomizations({
7270
- name,
7271
- projectDir
7272
- });
7273
7553
  }
7274
7554
  /**
7275
7555
  * Per-project customizations applied to a project directory — the parts
@@ -7286,14 +7566,32 @@ async function applyProjectCustomizations(args) {
7286
7566
  pkg.name = name;
7287
7567
  await writeJsonFile(pkgPath, pkg, { indent: 2 });
7288
7568
  await createOrUpdateManifest({ name }, projectDir);
7569
+ const ports = deriveProjectPorts(name);
7570
+ const configPath = join(projectDir, "powerhouse.config.json");
7571
+ const config = await loadJsonFile(configPath);
7572
+ config.studio = {
7573
+ ...config.studio,
7574
+ port: ports.studioPort
7575
+ };
7576
+ config.reactor = {
7577
+ ...config.reactor,
7578
+ port: ports.switchboardPort
7579
+ };
7580
+ const vetra = {
7581
+ ...config.vetra,
7582
+ connectPort: ports.vetraConnectPort
7583
+ };
7289
7584
  if (remoteDrive) {
7290
- const configPath = join(projectDir, "powerhouse.config.json");
7291
- const config = await loadJsonFile(configPath);
7292
- config.vetra = {
7293
- driveId: remoteDrive.split("/").pop() ?? "",
7294
- driveUrl: remoteDrive
7295
- };
7296
- await writeJsonFile(configPath, config, { indent: 2 });
7585
+ vetra.driveId = remoteDrive.split("/").pop() ?? "";
7586
+ vetra.driveUrl = remoteDrive;
7587
+ }
7588
+ config.vetra = vetra;
7589
+ await writeJsonFile(configPath, config, { indent: 2 });
7590
+ const mcpFiles = [[".mcp.json", buildMcpTemplate(ports.switchboardPort)], [join(".cursor", "mcp.json"), buildCursorMcpTemplate(ports.switchboardPort)]];
7591
+ for (const [rel, contents] of mcpFiles) {
7592
+ const target = join(projectDir, rel);
7593
+ if (!existsSync(target)) continue;
7594
+ await writeFileEnsuringDir(target, contents.trimStart());
7297
7595
  }
7298
7596
  }
7299
7597
  async function writeCIFiles(projectDir = process.cwd()) {
@@ -7453,7 +7751,9 @@ const DATE_LIKE_SCALARS = new Set(["Date", "DateTime"]);
7453
7751
  const SCALAR_MOCK_OVERRIDES = {
7454
7752
  Date: `"2024-01-01T00:00:00.000Z"`,
7455
7753
  DateTime: `"2024-01-01T00:00:00.000Z"`,
7456
- URL: `"https://example.com"`
7754
+ URL: `"https://example.com"`,
7755
+ AttachmentRef: `"attachment://v1:${"a".repeat(64)}"`,
7756
+ Address: `"eip155:0x${"0".repeat(40)}"`
7457
7757
  };
7458
7758
  function unwrapNamedTypeName(type) {
7459
7759
  if (type.kind === Kind.NAMED_TYPE) return type.name.value;
@@ -7803,8 +8103,8 @@ async function makeReducerOperationHandlerForModule({ project, module, version,
7803
8103
  const operationsInterfaceTypeName = `${pascalCaseDocumentType}${pascalCaseModuleName}Operations`;
7804
8104
  const operationsInterfaceVariableName = `${camelCaseDocumentType}${pascalCaseModuleName}Operations`;
7805
8105
  const existingOperationsInterfaceTypeImport = sourceFile.getImportDeclaration((importDeclaration) => !!importDeclaration.getNamedImports().find((importSpecifier) => importSpecifier.getName() === operationsInterfaceTypeName));
7806
- if (existingOperationsInterfaceTypeImport) existingOperationsInterfaceTypeImport.remove();
7807
- sourceFile.addImportDeclaration({
8106
+ if (existingOperationsInterfaceTypeImport) existingOperationsInterfaceTypeImport.setModuleSpecifier(versionImportPath);
8107
+ else sourceFile.addImportDeclaration({
7808
8108
  namedImports: [operationsInterfaceTypeName],
7809
8109
  moduleSpecifier: versionImportPath,
7810
8110
  isTypeOnly: true
@@ -7915,7 +8215,7 @@ async function makeOperationModuleTestFile(args) {
7915
8215
  version
7916
8216
  });
7917
8217
  } else sourceFile.replaceWithText(ts$1`
7918
- import { generateMock } from "document-model";
8218
+ import { generateMock } from "document-model/mock";
7919
8219
  import { describe, expect, it } from "vitest";
7920
8220
 
7921
8221
  describe("${moduleOperationsTypeName}", () => {
@@ -7967,13 +8267,13 @@ async function makeOperationModuleTestFile(args) {
7967
8267
  })));
7968
8268
  }));
7969
8269
  describeCallBody.addStatements(testCasesToAdd);
7970
- const GENERATE_MOCK_NAME = "generateMock";
7971
- const GENERATE_MOCK_MODULE_SPECIFIER = "document-model";
7972
- const generateMockImport = sourceFile.getImportDeclaration((i) => i.getNamedImports().some((v) => v.getText().includes(GENERATE_MOCK_NAME)));
7973
- if (sourceFile.getText().includes(GENERATE_MOCK_NAME) && !generateMockImport) sourceFile.addImportDeclaration({
7974
- namedImports: [GENERATE_MOCK_NAME],
7975
- moduleSpecifier: GENERATE_MOCK_MODULE_SPECIFIER
7976
- });
8270
+ if (sourceFile.getText().includes("generateMock")) {
8271
+ fixGenerateMockImports(sourceFile);
8272
+ if (!hasGenerateMockImport(sourceFile)) sourceFile.addImportDeclaration({
8273
+ namedImports: [GENERATE_MOCK_NAME],
8274
+ moduleSpecifier: GENERATE_MOCK_MODULE_SPECIFIER
8275
+ });
8276
+ }
7977
8277
  sourceFile.fixUnusedIdentifiers();
7978
8278
  await formatSourceFileWithPrettier(sourceFile);
7979
8279
  }
@@ -7992,13 +8292,253 @@ async function makeDocumentModelTestFile(args) {
7992
8292
  await formatSourceFileWithPrettier(sourceFile);
7993
8293
  }
7994
8294
  //#endregion
8295
+ //#region src/file-builders/document-model/upgrade-migration.ts
8296
+ /**
8297
+ * Derives the migration between two consecutive spec versions. Mechanical
8298
+ * changes — field additions on the scope's state type (initialized from the
8299
+ * new version's initial value, falling back to the schema's zero value) and
8300
+ * field removals — produce a "fill" plan. Anything else (changed field
8301
+ * types, edits to nested types that pre-existing documents reference,
8302
+ * additions codegen cannot synthesize a value for) produces a "manual" plan.
8303
+ */
8304
+ function buildMigrationPlan(args) {
8305
+ const { previousSpec, specification, stateName, localStateName } = args;
8306
+ if (!previousSpec) return {
8307
+ kind: "manual",
8308
+ reason: "the previous specification version is not available"
8309
+ };
8310
+ const fills = {};
8311
+ const scopes = [{
8312
+ scope: "global",
8313
+ typeName: stateName
8314
+ }, {
8315
+ scope: "local",
8316
+ typeName: localStateName
8317
+ }];
8318
+ for (const { scope, typeName } of scopes) {
8319
+ const oldState = previousSpec.state[scope];
8320
+ const newState = specification.state[scope];
8321
+ const analysis = analyzeScope({
8322
+ oldSchema: oldState?.schema ?? "",
8323
+ newSchema: newState?.schema ?? "",
8324
+ newInitialValue: newState?.initialValue ?? "",
8325
+ typeName,
8326
+ scope
8327
+ });
8328
+ if (analysis.kind === "manual") return analysis;
8329
+ if (analysis.fill && Object.keys(analysis.fill).length > 0) fills[scope] = analysis.fill;
8330
+ }
8331
+ return {
8332
+ kind: "fill",
8333
+ fills
8334
+ };
8335
+ }
8336
+ function analyzeScope(args) {
8337
+ const { oldSchema, newSchema, newInitialValue, typeName, scope } = args;
8338
+ const oldDoc = safeParseSdl(oldSchema);
8339
+ const newDoc = safeParseSdl(newSchema);
8340
+ if (oldSchema.trim() && !oldDoc) return {
8341
+ kind: "manual",
8342
+ reason: `the previous ${scope} state schema could not be parsed`
8343
+ };
8344
+ if (newSchema.trim() && !newDoc) return {
8345
+ kind: "manual",
8346
+ reason: `the new ${scope} state schema could not be parsed`
8347
+ };
8348
+ const oldType = oldDoc ? findObjectType(oldDoc, typeName) : void 0;
8349
+ const newType = newDoc ? findObjectType(newDoc, typeName) : void 0;
8350
+ if (!newType || !newDoc) return {
8351
+ kind: "fill",
8352
+ fill: void 0
8353
+ };
8354
+ if (oldType && oldDoc) {
8355
+ const oldReachable = collectReachableTypes(oldDoc, typeName);
8356
+ for (const [name, oldDefinition] of oldReachable) {
8357
+ if (name === typeName) continue;
8358
+ const newDefinition = findTypeDefinition(newDoc, name);
8359
+ if (!newDefinition) continue;
8360
+ if (print(oldDefinition) !== print(newDefinition)) return {
8361
+ kind: "manual",
8362
+ reason: `the ${scope} state type "${name}" changed between versions`
8363
+ };
8364
+ }
8365
+ }
8366
+ const oldFields = new Map((oldType?.fields ?? []).map((field) => [field.name.value, field]));
8367
+ const newFields = newType.fields ?? [];
8368
+ const initialValueObject = safeParseJsonRecord(newInitialValue);
8369
+ const fill = {};
8370
+ for (const field of newFields) {
8371
+ const fieldName = field.name.value;
8372
+ const oldField = oldFields.get(fieldName);
8373
+ if (oldField) {
8374
+ if (print(oldField.type) !== print(field.type)) return {
8375
+ kind: "manual",
8376
+ reason: `the ${scope} state field "${fieldName}" changed type between versions`
8377
+ };
8378
+ continue;
8379
+ }
8380
+ if (initialValueObject && fieldName in initialValueObject) {
8381
+ fill[fieldName] = initialValueObject[fieldName];
8382
+ continue;
8383
+ }
8384
+ const zero = zeroValueForType(field.type, newDoc, /* @__PURE__ */ new Set());
8385
+ if (!zero.ok) return {
8386
+ kind: "manual",
8387
+ reason: `no initial value could be derived for the added ${scope} state field "${fieldName}"`
8388
+ };
8389
+ fill[fieldName] = zero.value;
8390
+ }
8391
+ return {
8392
+ kind: "fill",
8393
+ fill
8394
+ };
8395
+ }
8396
+ function zeroValueForType(typeNode, schemaDoc, visitedTypes) {
8397
+ if (typeNode.kind !== Kind.NON_NULL_TYPE) return {
8398
+ ok: true,
8399
+ value: null
8400
+ };
8401
+ const inner = typeNode.type;
8402
+ if (inner.kind === Kind.LIST_TYPE) return {
8403
+ ok: true,
8404
+ value: []
8405
+ };
8406
+ const name = inner.name.value;
8407
+ switch (name) {
8408
+ case "String":
8409
+ case "ID": return {
8410
+ ok: true,
8411
+ value: ""
8412
+ };
8413
+ case "Int":
8414
+ case "Float": return {
8415
+ ok: true,
8416
+ value: 0
8417
+ };
8418
+ case "Boolean": return {
8419
+ ok: true,
8420
+ value: false
8421
+ };
8422
+ }
8423
+ const definition = findTypeDefinition(schemaDoc, name);
8424
+ if (!definition) {
8425
+ const defaultValue = getPHCustomScalarByTypeName(name)?.getDefaultValue?.();
8426
+ if (defaultValue !== void 0) return {
8427
+ ok: true,
8428
+ value: defaultValue
8429
+ };
8430
+ return { ok: false };
8431
+ }
8432
+ if (definition.kind === Kind.ENUM_TYPE_DEFINITION) return zeroValueForEnum(definition);
8433
+ if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) {
8434
+ if (visitedTypes.has(name)) return { ok: false };
8435
+ visitedTypes.add(name);
8436
+ const value = {};
8437
+ for (const field of definition.fields ?? []) {
8438
+ const fieldZero = zeroValueForType(field.type, schemaDoc, visitedTypes);
8439
+ if (!fieldZero.ok) return { ok: false };
8440
+ value[field.name.value] = fieldZero.value;
8441
+ }
8442
+ visitedTypes.delete(name);
8443
+ return {
8444
+ ok: true,
8445
+ value
8446
+ };
8447
+ }
8448
+ if (definition.kind === Kind.SCALAR_TYPE_DEFINITION) {
8449
+ const defaultValue = getPHCustomScalarByTypeName(name)?.getDefaultValue?.();
8450
+ if (defaultValue !== void 0) return {
8451
+ ok: true,
8452
+ value: defaultValue
8453
+ };
8454
+ return { ok: false };
8455
+ }
8456
+ return { ok: false };
8457
+ }
8458
+ function zeroValueForEnum(definition) {
8459
+ const first = definition.values?.[0]?.name.value;
8460
+ if (first === void 0) return { ok: false };
8461
+ return {
8462
+ ok: true,
8463
+ value: first
8464
+ };
8465
+ }
8466
+ function collectReachableTypes(schemaDoc, rootTypeName) {
8467
+ const reachable = /* @__PURE__ */ new Map();
8468
+ const queue = [rootTypeName];
8469
+ while (queue.length > 0) {
8470
+ const name = queue.shift();
8471
+ if (reachable.has(name)) continue;
8472
+ const definition = findTypeDefinition(schemaDoc, name);
8473
+ if (!definition) continue;
8474
+ reachable.set(name, definition);
8475
+ if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) for (const field of definition.fields ?? []) queue.push(namedTypeOf(field.type));
8476
+ else if (definition.kind === Kind.UNION_TYPE_DEFINITION) for (const member of definition.types ?? []) queue.push(member.name.value);
8477
+ }
8478
+ return reachable;
8479
+ }
8480
+ function namedTypeOf(typeNode) {
8481
+ let node = typeNode;
8482
+ while (node.kind !== Kind.NAMED_TYPE) node = node.type;
8483
+ return node.name.value;
8484
+ }
8485
+ function findObjectType(schemaDoc, name) {
8486
+ const definition = findTypeDefinition(schemaDoc, name);
8487
+ return definition?.kind === Kind.OBJECT_TYPE_DEFINITION ? definition : void 0;
8488
+ }
8489
+ function findTypeDefinition(schemaDoc, name) {
8490
+ for (const definition of schemaDoc.definitions) switch (definition.kind) {
8491
+ case Kind.OBJECT_TYPE_DEFINITION:
8492
+ case Kind.ENUM_TYPE_DEFINITION:
8493
+ case Kind.SCALAR_TYPE_DEFINITION:
8494
+ case Kind.UNION_TYPE_DEFINITION:
8495
+ case Kind.INTERFACE_TYPE_DEFINITION:
8496
+ case Kind.INPUT_OBJECT_TYPE_DEFINITION: if (definition.name.value === name) return definition;
8497
+ }
8498
+ }
8499
+ function safeParseSdl(sdl) {
8500
+ if (!sdl.trim()) return null;
8501
+ try {
8502
+ return parse(sdl);
8503
+ } catch {
8504
+ return null;
8505
+ }
8506
+ }
8507
+ function safeParseJsonRecord(json) {
8508
+ if (!json.trim()) return null;
8509
+ try {
8510
+ const parsed = JSON.parse(json);
8511
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
8512
+ return null;
8513
+ } catch {
8514
+ return null;
8515
+ }
8516
+ }
8517
+ /**
8518
+ * Serializes a fill value to TypeScript source. Fill values come from JSON
8519
+ * (initial values) or the zero-value synthesizer, so JSON serialization is
8520
+ * sufficient.
8521
+ */
8522
+ function fillToTsLiteral(fill) {
8523
+ return JSON.stringify(fill, null, 2);
8524
+ }
8525
+ //#endregion
7995
8526
  //#region src/file-builders/document-model/upgrades-dir.ts
7996
8527
  async function makeUpgradeFile(args) {
7997
8528
  const { project, version, upgradesDirPath } = args;
7998
8529
  if (version < 2) return;
7999
8530
  const { alreadyExists, sourceFile } = getOrCreateSourceFile(project, path.join(upgradesDirPath, `v${version}.ts`));
8000
8531
  if (alreadyExists) return;
8001
- const template = upgradeTransitionTemplate(args);
8532
+ const plan = buildMigrationPlan({
8533
+ previousSpec: args.documentModelState.specifications.find((specification) => specification.version === version - 1),
8534
+ specification: args.specification,
8535
+ stateName: args.stateName,
8536
+ localStateName: args.localStateName
8537
+ });
8538
+ const template = upgradeTransitionTemplate({
8539
+ ...args,
8540
+ plan
8541
+ });
8002
8542
  sourceFile.replaceWithText(template);
8003
8543
  await formatSourceFileWithPrettier(sourceFile);
8004
8544
  }
@@ -8280,6 +8820,10 @@ async function makeDocumentModelsIndexFile(args) {
8280
8820
  moduleSpecifier
8281
8821
  });
8282
8822
  }));
8823
+ sourceFile.addExportDeclaration({
8824
+ namedExports: ["upgradeManifests"],
8825
+ moduleSpecifier: "./upgrade-manifests.js"
8826
+ });
8283
8827
  await formatSourceFileWithPrettier(sourceFile);
8284
8828
  }
8285
8829
  /** Writes a json file derived from a `documentModelState` */
@@ -8592,6 +9136,29 @@ function getFactoryBuildersArray(sourceFile, name) {
8592
9136
  return sourceFile.getDescendantsOfKind(ts.SyntaxKind.VariableStatement).flatMap((d) => d.getDescendantsOfKind(ts.SyntaxKind.VariableDeclaration)).find((d) => d.getName() === name)?.getDescendantsOfKind(ts.SyntaxKind.ArrayLiteralExpression).at(0);
8593
9137
  }
8594
9138
  //#endregion
9139
+ //#region src/file-builders/project-index.ts
9140
+ const AI_TOOLS_MODULE_SPECIFIER = "./ai/tools.js";
9141
+ /**
9142
+ * Idempotently syncs the optional `aiTools` export in the project's root
9143
+ * `index.ts`: the line is present if and only if `ai/tools.ts` exists.
9144
+ * The export is how the host app (Connect) discovers the tool descriptors
9145
+ * this package offers to the in-browser AI assistant.
9146
+ */
9147
+ function syncProjectAiToolsExport(project) {
9148
+ const { directory: documentModelsDir } = getOrCreateDirectory(project, "document-models");
9149
+ const projectDir = documentModelsDir.getParentOrThrow().getPath();
9150
+ const indexPath = join$1(projectDir, "index.ts");
9151
+ if (!existsSync(indexPath)) return;
9152
+ const { sourceFile } = getOrCreateSourceFile(project, indexPath);
9153
+ const hasAiToolsFile = existsSync(join$1(projectDir, "ai", "tools.ts"));
9154
+ const existing = sourceFile.getExportDeclarations().find((declaration) => declaration.getModuleSpecifier()?.getText().trim() === `"${AI_TOOLS_MODULE_SPECIFIER}"` && declaration.getNamedExports().some((exported) => exported.getText() === "aiTools"));
9155
+ if (hasAiToolsFile && !existing) sourceFile.addExportDeclarations([{
9156
+ namedExports: ["aiTools"],
9157
+ moduleSpecifier: AI_TOOLS_MODULE_SPECIFIER
9158
+ }]);
9159
+ else if (!hasAiToolsFile && existing) existing.remove();
9160
+ }
9161
+ //#endregion
8595
9162
  //#region src/file-builders/subgraphs.ts
8596
9163
  async function tsMorphGenerateSubgraph(args) {
8597
9164
  const { subgraphName, project } = args;
@@ -8658,6 +9225,6 @@ async function makeSubgraphsIndexFile(args) {
8658
9225
  await formatSourceFileWithPrettier(sourceFile);
8659
9226
  }
8660
9227
  //#endregion
8661
- export { getVariableDeclarationByTypeName as $, documentModelsTemplate as $n, documentModelOperationsModuleErrorFileTemplate as $t, writeGeneratedProcessorsFiles as A, buildPowerhouseConfigTemplate as An, factoryBuildersTemplate as At, updateVersionedImports as B, reactorTsTemplate as Bn, makeTestCaseForOperation as Bt, buildBoilerplatePackageJson as C, vitestConfigTemplate as Cn, relationalDbSchemaTemplate as Ct, writeCIFiles as D, styleTemplate as Dn, relationalDbFactoryTemplate as Dt, writeAllGeneratedProjectFiles as E, subgraphsIndexTemplate as En, relationalDbIndexTemplate as Et, tsMorphGenerateApp as F, packageJsonTemplate as Fn, upgradeTransitionTemplate as Ft, getAllImportModuleSpecifiers as G, syncAndPublishWorkflowTemplate as Gn, documentModelIndexTemplate as Gt, DEFAULT_PROJECT_OPTIONS as H, legacyIndexHtmlTemplate as Hn, documentModelSrcUtilsTemplate as Ht, makeEditorModuleFile as I, npmrcTemplate as In, upgradeManifestTemplate as It, getObjectLiteral as J, oxlintConfigTemplate as Jn, documentModelGenTypesTemplate as Jt, getAllImportNames as K, geminiSettingsTemplate as Kn, documentModelHooksFileTemplate as Kt, makeEditorsFile as L, mcpTemplate as Ln, documentModelOperationsModuleTestFileTemplate as Lt, writeGeneratedSubgraphsFiles as M, packageJsonScriptsTemplate as Mn, analyticsIndexTemplate as Mt, writeModuleFiles as N, pnpmWorkspaceTemplate as Nn, analyticsFactoryTemplate as Nt, writeGeneratedDocumentModelsFiles as O, readmeTemplate as On, processorsIndexTemplate as Ot, writeProjectRootFiles as P, exportsTemplate as Pn, documentModelUtilsTemplate as Pt, getStringPropertyValue as Q, documentModelsIndexTemplate as Qn, documentModelOperationsModuleOperationsFileTemplate as Qt, makeEditorsIndexFile as R, mainTsxTemplate as Rn, makeOperationImportNames as Rt, writeCliDocsMarkdownFile as S, docsFromCliHelpTemplate as Sn, subgraphIndexFileTemplate as St, writeAiConfigFiles as T, tsconfigPathsTemplate as Tn, relationalDbMigrationsTemplate as Tt, buildTsMorphProject as U, indexHtmlTemplate as Un, documentModelSrcIndexFileTemplate as Ut, getInitialStates as V, indexTsTemplate as Vn, documentModelTestFileTemplate as Vt, getDefaultProjectOptions as W, gitIgnoreTemplate as Wn, documentModelModuleFileTemplate as Wt, getProperyAssignmentByName as X, editorsTemplate as Xn, documentModelGenReducerFileTemplate as Xt, getObjectProperty as Y, editorsIndexTemplate as Yn, documentModelSchemaIndexTemplate as Yt, getStringArrayPropertyElements as Z, upgradeManifestsTemplate as Zn, documentModelPhFactoriesFileTemplate as Zt, scalarsValidation as _, documentModelGenControllerFileTemplate as _n, parseArgs as _t, getOrCreateManifestFile as a, getDocumentModelVariableNames as an, claudeSettingsLocalTemplate as ar, getOrCreateSourceFile as at, getCommandsHelpInfo as b, documentEditorModuleFileTemplate as bn, customSubgraphSchemaTemplate as bt, operationHasEmptyInput as c, getLatestDocumentModelSpecVersionNumber as cn, appConfigFileTemplate as cr, getProcessorMetadata as ct, generateDocumentModelZodSchemas as d, getActionType as dn, appFoldersFileTemplate as dr, formatSafe as dt, documentModelOperationsModuleCreatorsFileTemplate as en, switchboardEntrypointTemplate as er, loadDocumentModelInDir as et, generateTypesAndZodSchemasFromGraphql as f, getActionTypeName as fn, appFilesFileTemplate as fr, formatSourceFileWithPrettier as ft, scalars as g, documentModelGenCreatorsFileTemplate as gn, createDocumentFileTemplate as gr, configSpec as gt, getMockOverrideFieldNames as h, documentModelDocumentSchemaFileTemplate as hn, appDriveContentsFileTemplate as hr, documentModelDocumentTypeMetadata as ht, createOrUpdateManifest as i, getDocumentModelSpecByVersionNumber as in, cursorMcpTemplate as ir, getOrCreateDirectory as it, writeGeneratedProjectRootFiles as j, packageJsonExportsTemplate as jn, analyticsProcessorTemplate as jt, writeGeneratedEditorsFiles as k, ManifestTemplate as kn, processorsFactoryTemplate as kt, operationHasInput as l, getActionInputName as ln, driveExplorerNavigationBreadcrumbsFileTemplate as lr, getAppMetadata as lt, getInputFieldNames as m, documentModelDocumentTypeTemplate as mn, driveExplorerFileTemplate as mr, getDocumentTypeMetadata as mt, tsMorphGenerateSubgraph as n, getModuleExportType as nn, dockerfileTemplate as nr, buildStringLiteral as nt, pruneManifestSection as o, getEditorVariableNames as on, agentsTemplate as or, getPreviousVersionSourceFile as ot, getDateLikeFieldNames as p, documentModelGenIndexFileTemplate as pn, emptyStateFileTemplate as pr, runOxfmt as pt, getBooleanPropertyValue as q, oxfmtConfigTemplate as qn, documentModelGenUtilsTemplate as qt, tsMorphGenerateProcessor as r, getDocumentModelDirName as rn, connectEntrypointTemplate as rr, ensureDirectoriesExist as rt, makeModulesIndexFile as s, getLatestDocumentModelSpec as sn, appEditorFileTemplate as sr, getSubgraphMetadata as st, makeSubgraphsIndexFile as t, documentModelOperationModuleActionsFileTemplate as tn, nginxConfTemplate as tr, buildObjectLiteral as tt, tsMorphGenerateDocumentModel as u, getActionInputTypeNames as un, folderTreeFileTemplate as ur, getEditorMetadata as ut, tsMorphGenerateDocumentEditor as v, documentModelGenActionsFileTemplate as vn, parseConfig as vt, applyProjectCustomizations as w, tsConfigTemplate as wn, relationalDbProcessorTemplate as wt, makeCliDocsFromHelp as x, documentEditorEditorFileTemplate as xn, subgraphLibFileTemplate as xt, getCommandHelpInfo as y, documentModelRootActionsFileTemplate as yn, customSubgraphResolversTemplate as yt, validateDocumentModelState as z, licenseTemplate as zn, makeOperationsImports as zt };
9228
+ export { getProperyAssignmentByName as $, indexHtmlTemplate as $n, documentModelModuleFileTemplate as $t, writeCIFiles as A, documentEditorEditorFileTemplate as An, relationalDbSchemaTemplate as At, makeEditorsFile as B, packageJsonExportsTemplate as Bn, analyticsFactoryTemplate as Bt, getCommandsHelpInfo as C, documentModelDocumentTypeTemplate as Cn, appFilesFileTemplate as Cr, configSpec as Ct, applyProjectCustomizations as D, documentModelGenActionsFileTemplate as Dn, createDocumentFileTemplate as Dr, customSubgraphSchemaTemplate as Dt, buildBoilerplatePackageJson as E, documentModelGenControllerFileTemplate as En, appDriveContentsFileTemplate as Er, customSubgraphResolversTemplate as Et, writeGeneratedSubgraphsFiles as F, subgraphsIndexTemplate as Fn, processorsIndexTemplate as Ft, DEFAULT_PROJECT_OPTIONS as G, npmrcTemplate as Gn, upgradeManifestTemplate as Gt, validateDocumentModelState as H, pnpmWorkspaceTemplate as Hn, renderProcessorFilter as Ht, writeModuleFiles as I, styleTemplate as In, processorsFactoryTemplate as It, getAllImportModuleSpecifiers as J, mainTsxTemplate as Jn, makeOperationsImports as Jt, buildTsMorphProject as K, buildMcpTemplate as Kn, documentModelOperationsModuleTestFileTemplate as Kt, writeProjectRootFiles as L, readmeTemplate as Ln, factoryBuildersTemplate as Lt, writeGeneratedEditorsFiles as M, vitestConfigTemplate as Mn, relationalDbMigrationsTemplate as Mt, writeGeneratedProcessorsFiles as N, tsConfigTemplate as Nn, relationalDbIndexTemplate as Nt, writeAiConfigFiles as O, documentModelRootActionsFileTemplate as On, subgraphLibFileTemplate as Ot, writeGeneratedProjectRootFiles as P, tsconfigPathsTemplate as Pn, relationalDbFactoryTemplate as Pt, getObjectProperty as Q, legacyIndexHtmlTemplate as Qn, documentModelSrcIndexFileTemplate as Qt, tsMorphGenerateApp as R, ManifestTemplate as Rn, analyticsProcessorTemplate as Rt, getCommandHelpInfo as S, documentModelGenIndexFileTemplate as Sn, appFoldersFileTemplate as Sr, documentModelDocumentTypeMetadata as St, writeCliDocsMarkdownFile as T, documentModelGenCreatorsFileTemplate as Tn, driveExplorerFileTemplate as Tr, parseConfig as Tt, updateVersionedImports as U, exportsTemplate as Un, documentModelUtilsTemplate as Ut, makeEditorsIndexFile as V, packageJsonScriptsTemplate as Vn, parseFilterValues as Vt, getInitialStates as W, packageJsonTemplate as Wn, upgradeTransitionTemplate as Wt, getBooleanPropertyValue as X, reactorTsTemplate as Xn, documentModelTestFileTemplate as Xt, getAllImportNames as Y, licenseTemplate as Yn, makeTestCaseForOperation as Yt, getObjectLiteral as Z, indexTsTemplate as Zn, documentModelSrcUtilsTemplate as Zt, getInputFieldNames as _, getLatestDocumentModelSpecVersionNumber as _n, agentsTemplate as _r, hasGenerateMockImport as _t, createOrUpdateManifest as a, documentModelGenReducerFileTemplate as an, editorsIndexTemplate as ar, buildStringLiteral as at, scalarsValidation as b, getActionType as bn, driveExplorerNavigationBreadcrumbsFileTemplate as br, runOxfmt as bt, makeModulesIndexFile as c, documentModelOperationsModuleErrorFileTemplate as cn, documentModelsIndexTemplate as cr, getOrCreateSourceFile as ct, tsMorphGenerateDocumentModel as d, getModuleExportType as dn, nginxConfTemplate as dr, getProcessorMetadata as dt, documentModelIndexTemplate as en, gitIgnoreTemplate as er, getStringArrayPropertyElements as et, buildMigrationPlan as f, getDocumentModelDirName as fn, dockerfileTemplate as fr, getAppMetadata as ft, getDateLikeFieldNames as g, getLatestDocumentModelSpec as gn, claudeSettingsLocalTemplate as gr, fixGenerateMockImports as gt, generateTypesAndZodSchemasFromGraphql as h, getEditorVariableNames as hn, cursorMcpTemplate as hr, GENERATE_MOCK_NAME as ht, tsMorphGenerateProcessor as i, documentModelSchemaIndexTemplate as in, oxlintConfigTemplate as ir, buildObjectLiteral as it, writeGeneratedDocumentModelsFiles as j, docsFromCliHelpTemplate as jn, relationalDbProcessorTemplate as jt, writeAllGeneratedProjectFiles as k, documentEditorModuleFileTemplate as kn, subgraphIndexFileTemplate as kt, operationHasEmptyInput as l, documentModelOperationsModuleCreatorsFileTemplate as ln, documentModelsTemplate as lr, getPreviousVersionSourceFile as lt, generateDocumentModelZodSchemas as m, getDocumentModelVariableNames as mn, buildCursorMcpTemplate as mr, GENERATE_MOCK_MODULE_SPECIFIER as mt, tsMorphGenerateSubgraph as n, documentModelGenUtilsTemplate as nn, geminiSettingsTemplate as nr, getVariableDeclarationByTypeName as nt, getOrCreateManifestFile as o, documentModelPhFactoriesFileTemplate as on, editorsTemplate as or, ensureDirectoriesExist as ot, fillToTsLiteral as p, getDocumentModelSpecByVersionNumber as pn, connectEntrypointTemplate as pr, getEditorMetadata as pt, getDefaultProjectOptions as q, mcpTemplate as qn, makeOperationImportNames as qt, syncProjectAiToolsExport as r, documentModelGenTypesTemplate as rn, oxfmtConfigTemplate as rr, loadDocumentModelInDir as rt, pruneManifestSection as s, documentModelOperationsModuleOperationsFileTemplate as sn, upgradeManifestsTemplate as sr, getOrCreateDirectory as st, makeSubgraphsIndexFile as t, documentModelHooksFileTemplate as tn, syncAndPublishWorkflowTemplate as tr, getStringPropertyValue as tt, operationHasInput as u, documentModelOperationModuleActionsFileTemplate as un, switchboardEntrypointTemplate as ur, getSubgraphMetadata as ut, getMockOverrideFieldNames as v, getActionInputName as vn, appEditorFileTemplate as vr, formatSafe as vt, makeCliDocsFromHelp as w, documentModelDocumentSchemaFileTemplate as wn, emptyStateFileTemplate as wr, parseArgs as wt, tsMorphGenerateDocumentEditor as x, getActionTypeName as xn, folderTreeFileTemplate as xr, getDocumentTypeMetadata as xt, scalars as y, getActionInputTypeNames as yn, appConfigFileTemplate as yr, formatSourceFileWithPrettier as yt, makeEditorModuleFile as z, buildPowerhouseConfigTemplate as zn, analyticsIndexTemplate as zt };
8662
9229
 
8663
- //# sourceMappingURL=file-builders-Btk4MK1I.mjs.map
9230
+ //# sourceMappingURL=file-builders-BZEQ9XnO.mjs.map