@farthershore/product 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { createRequire as __createRequire } from "node:module";const require=__createRequire(import.meta.url);
3
3
 
4
4
  // src/bin.ts
5
- import { writeFile } from "node:fs/promises";
5
+ import { access, writeFile } from "node:fs/promises";
6
+ import { constants } from "node:fs";
6
7
  import { resolve } from "node:path";
7
8
  import { pathToFileURL } from "node:url";
8
9
  import { tsImport } from "tsx/esm/api";
@@ -27,6 +28,97 @@ var ManifestBuilderError = class extends Error {
27
28
  }
28
29
  };
29
30
 
31
+ // src/resource-graph.ts
32
+ var ManifestResourceGraph = class {
33
+ nodes = /* @__PURE__ */ new Map();
34
+ declarationOrder = 0;
35
+ register(kind, key, value, dependsOn = []) {
36
+ const id = nodeId(kind, key);
37
+ if (this.nodes.has(id)) {
38
+ throw new Error(`duplicate resource node ${id}`);
39
+ }
40
+ const node = {
41
+ urn: resourceUrn(kind, key),
42
+ kind,
43
+ key,
44
+ value,
45
+ dependsOn: [...new Set(dependsOn)],
46
+ declarationOrder: this.declarationOrder
47
+ };
48
+ this.declarationOrder += 1;
49
+ this.nodes.set(id, node);
50
+ return node;
51
+ }
52
+ upsert(kind, key, value, dependsOn = []) {
53
+ const id = nodeId(kind, key);
54
+ const existing = this.nodes.get(id);
55
+ if (!existing) {
56
+ return this.register(kind, key, value, dependsOn);
57
+ }
58
+ const node = {
59
+ urn: existing.urn,
60
+ kind,
61
+ key,
62
+ value,
63
+ dependsOn: [...new Set(dependsOn)],
64
+ declarationOrder: existing.declarationOrder
65
+ };
66
+ this.nodes.set(id, node);
67
+ return node;
68
+ }
69
+ clearKind(kind) {
70
+ for (const [id, node] of this.nodes.entries()) {
71
+ if (node.kind === kind) this.nodes.delete(id);
72
+ }
73
+ }
74
+ has(kind, key) {
75
+ return this.nodes.has(nodeId(kind, key));
76
+ }
77
+ get(kind, key) {
78
+ return this.nodes.get(nodeId(kind, key)) ?? null;
79
+ }
80
+ values(kind) {
81
+ return [...this.nodes.values()].filter((node) => node.kind === kind).map((node) => node.value);
82
+ }
83
+ sortedValues(kind, key) {
84
+ return sortBy(this.values(kind), key);
85
+ }
86
+ snapshot() {
87
+ return {
88
+ nodes: sortBy([...this.nodes.values()], (node) => node.urn).map(
89
+ ({ value: _value, ...node }) => node
90
+ )
91
+ };
92
+ }
93
+ missingDependencies() {
94
+ const known = new Set([...this.nodes.values()].map((node) => node.urn));
95
+ return [...this.nodes.values()].flatMap(
96
+ (node) => node.dependsOn.filter((dependency) => !known.has(dependency)).map((dependency) => ({
97
+ from: node.urn,
98
+ missing: dependency
99
+ }))
100
+ );
101
+ }
102
+ };
103
+ function resourceUrn(kind, key) {
104
+ return `urn:farthershore:product:${kind}:${encodeURIComponent(
105
+ key
106
+ )}`;
107
+ }
108
+ function resourceDependency(kind, key) {
109
+ return resourceUrn(kind, key);
110
+ }
111
+ function nodeId(kind, key) {
112
+ return `${kind}:${key}`;
113
+ }
114
+ function sortBy(items, key) {
115
+ return [...items].sort((a, b) => {
116
+ const ka = key(a);
117
+ const kb = key(b);
118
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
119
+ });
120
+ }
121
+
30
122
  // ../contracts/dist/plans/limits-schema.js
31
123
  import { z } from "zod";
32
124
  var limitDimensionSchema = z.string().min(1);
@@ -1251,6 +1343,77 @@ var featureCatalogEntrySchema = z13.object({
1251
1343
  plans: z13.array(z13.string().min(1)).min(1).max(20)
1252
1344
  });
1253
1345
  var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1346
+ var productCleanupPolicyModeSchema = z13.enum([
1347
+ "report",
1348
+ "pull_request"
1349
+ ]);
1350
+ var productChangeApprovalRiskSchema = z13.enum([
1351
+ "safe",
1352
+ "non_blocking",
1353
+ "economic_risk",
1354
+ "blocking"
1355
+ ]);
1356
+ var productOperatorPoliciesSchema = z13.object({
1357
+ /**
1358
+ * Route cleanup operator. Disabled by default; report-mode is the safe
1359
+ * default so a product can surface zero-traffic runtime-route candidates
1360
+ * before it opts into draft PR mutation.
1361
+ */
1362
+ cleanup: z13.object({
1363
+ enabled: z13.boolean().default(false),
1364
+ mode: productCleanupPolicyModeSchema.default("report")
1365
+ }).default({ enabled: false, mode: "report" }),
1366
+ /**
1367
+ * PR approval thresholds for manifest impact reports. This schema makes
1368
+ * the policy a first-class product-as-code field even while enforcement is
1369
+ * still report/label-only.
1370
+ */
1371
+ change_approval: z13.object({
1372
+ auto_merge_max_risk: z13.enum(["none", "safe", "non_blocking"]).default("none"),
1373
+ require_human_for: z13.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
1374
+ }).default({
1375
+ auto_merge_max_risk: "none",
1376
+ require_human_for: ["economic_risk", "blocking"]
1377
+ })
1378
+ }).default({
1379
+ cleanup: { enabled: false, mode: "report" },
1380
+ change_approval: {
1381
+ auto_merge_max_risk: "none",
1382
+ require_human_for: ["economic_risk", "blocking"]
1383
+ }
1384
+ });
1385
+ var customerIdentityRequirementSchema = z13.enum([
1386
+ "org_only",
1387
+ "org_and_user"
1388
+ ]);
1389
+ var customerPortalAuthStrategySchema = z13.enum([
1390
+ "clerk",
1391
+ "test-personas"
1392
+ ]);
1393
+ var productCustomerContextSchema = z13.object({
1394
+ /**
1395
+ * Edge credential identity policy. This is intentionally Product-scoped:
1396
+ * B7 keeps Product as the business boundary and avoids customer-side
1397
+ * Workspace/subject models until per-subject entitlements need them.
1398
+ */
1399
+ identity_requirement: customerIdentityRequirementSchema.optional(),
1400
+ /**
1401
+ * Enables signed customer-context tokens (`fsc_*`) without putting the
1402
+ * runtime signing secret in product/product.config.ts. Core generates/preserves the
1403
+ * secret when this is true and clears it when explicitly false.
1404
+ */
1405
+ context_tokens: z13.object({
1406
+ enabled: z13.boolean().default(true)
1407
+ }).optional(),
1408
+ /**
1409
+ * Portal auth strategy for environment-scoped product applies. Production
1410
+ * portal auth is provisioner-owned; preview/test environments can opt into
1411
+ * test personas through Product-as-Code.
1412
+ */
1413
+ portal_auth: z13.object({
1414
+ strategy: customerPortalAuthStrategySchema
1415
+ }).optional()
1416
+ });
1254
1417
  var productSpecSchema = z13.object({
1255
1418
  product: z13.object({
1256
1419
  name: z13.string().min(1).max(100),
@@ -1282,11 +1445,12 @@ var productSpecSchema = z13.object({
1282
1445
  }).optional(),
1283
1446
  features: featureCatalogSchema.optional(),
1284
1447
  resources: countedResourcesSchema,
1448
+ policies: productOperatorPoliciesSchema,
1449
+ customer_context: productCustomerContextSchema.optional(),
1285
1450
  /**
1286
- * Track B4 — Declarative frontend surface. Product code can declare
1287
- * template-owned nav/pages composed from known portal components. The
1288
- * template renders these for non-reserved paths; contractual dashboard
1289
- * sections remain template-owned.
1451
+ * Legacy/internal declarative frontend surface. New product repos customize
1452
+ * the generated `frontend/` project directly; this remains for existing IR
1453
+ * and internal template metadata.
1290
1454
  */
1291
1455
  frontend: frontendManifestSchema.optional(),
1292
1456
  migrations: migrationDeclsSchema.optional(),
@@ -1410,7 +1574,7 @@ var productSpecSchema = z13.object({
1410
1574
  *
1411
1575
  * Once a product has compiled with a `webhooks` block, API mutations
1412
1576
  * on those endpoints fail with `409 MANAGED_BY_CODE` — see
1413
- * `core/src/routes/management-webhooks.ts`. Tie-breaker: product.config.ts
1577
+ * `core/src/routes/management-webhooks.ts`. Tie-breaker: product/product.config.ts
1414
1578
  * wins over API state if an endpoint id appears in both.
1415
1579
  */
1416
1580
  webhooks: webhooksBlockSchema.optional(),
@@ -1997,6 +2161,8 @@ var productSpecV2Schema = z20.object({
1997
2161
  frontend: frontendManifestSchema.optional(),
1998
2162
  migrations: migrationDeclsSchema.optional(),
1999
2163
  resources: countedResourcesSchema,
2164
+ policies: productOperatorPoliciesSchema,
2165
+ customer_context: productCustomerContextSchema.optional(),
2000
2166
  billing: z20.object({
2001
2167
  gracePeriodDays: z20.number().int().nonnegative().default(3),
2002
2168
  // When true (default), a plan limit INCREASE re-projects onto active
@@ -2194,7 +2360,7 @@ function hashIr(ir) {
2194
2360
  }
2195
2361
 
2196
2362
  // src/version.ts
2197
- var SDK_VERSION = true ? "0.0.0" : "0.0.0-dev";
2363
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
2198
2364
 
2199
2365
  // src/business.ts
2200
2366
  var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
@@ -2243,13 +2409,7 @@ var Business = class {
2243
2409
  [BUSINESS_BRAND] = true;
2244
2410
  name;
2245
2411
  options;
2246
- meters = /* @__PURE__ */ new Map();
2247
- resources = /* @__PURE__ */ new Map();
2248
- plans = /* @__PURE__ */ new Map();
2249
- features = /* @__PURE__ */ new Map();
2250
- policies = /* @__PURE__ */ new Map();
2251
- capabilities = /* @__PURE__ */ new Map();
2252
- migrations = [];
2412
+ graph = new ManifestResourceGraph();
2253
2413
  frontendManifest;
2254
2414
  productPatch = {};
2255
2415
  /** Sugar for binding API routes to features. */
@@ -2269,16 +2429,21 @@ var Business = class {
2269
2429
  }
2270
2430
  this.name = name;
2271
2431
  this.options = options;
2432
+ this.graph.register("product", name, {
2433
+ name,
2434
+ options
2435
+ });
2272
2436
  this.api = {
2273
2437
  route: (match, options2) => {
2274
2438
  const featureKey = keyOf(options2.feature);
2275
- const file = this.features.get(featureKey);
2439
+ const file = this.getFeatureFile(featureKey);
2276
2440
  if (!file) {
2277
2441
  throw new ManifestBuilderError(
2278
2442
  `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
2279
2443
  );
2280
2444
  }
2281
2445
  file.routes.push(this.buildRoute(match, options2));
2446
+ this.syncFeatureGraphNode(file);
2282
2447
  return this;
2283
2448
  }
2284
2449
  };
@@ -2290,6 +2455,7 @@ var Business = class {
2290
2455
  path: item.path,
2291
2456
  ...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
2292
2457
  }));
2458
+ this.syncFrontendGraphNode(manifest);
2293
2459
  return this;
2294
2460
  },
2295
2461
  page: (path, options2) => {
@@ -2314,17 +2480,19 @@ var Business = class {
2314
2480
  }))
2315
2481
  } : {}
2316
2482
  });
2483
+ this.syncFrontendGraphNode(manifest);
2317
2484
  return this;
2318
2485
  },
2319
2486
  manifest: (manifest) => {
2320
2487
  this.frontendManifest = manifest;
2488
+ this.syncFrontendGraphNode(manifest);
2321
2489
  return this;
2322
2490
  }
2323
2491
  };
2324
2492
  this.lifecycle = {
2325
2493
  migration: (id, options2) => {
2326
- this.assertUniqueMigrationId(id);
2327
- this.migrations.push({
2494
+ this.assertNewKey("lifecycle_migration", id, "migration");
2495
+ const migration = {
2328
2496
  id,
2329
2497
  from: this.normalizeMigrationPlanRef(options2.from),
2330
2498
  to: this.normalizeMigrationTargetRef(options2.to),
@@ -2341,12 +2509,26 @@ var Business = class {
2341
2509
  ...pin.notes !== void 0 ? { notes: pin.notes } : {}
2342
2510
  }))
2343
2511
  } : {}
2344
- });
2512
+ };
2513
+ this.graph.register(
2514
+ "lifecycle_migration",
2515
+ id,
2516
+ migration,
2517
+ this.migrationDependsOn(migration)
2518
+ );
2345
2519
  return this;
2346
2520
  },
2347
2521
  migrations: (migrations) => {
2348
2522
  this.assertUniqueMigrationIds(migrations);
2349
- this.migrations = migrations;
2523
+ this.graph.clearKind("lifecycle_migration");
2524
+ for (const migration of migrations) {
2525
+ this.graph.register(
2526
+ "lifecycle_migration",
2527
+ migration.id,
2528
+ migration,
2529
+ this.migrationDependsOn(migration)
2530
+ );
2531
+ }
2350
2532
  return this;
2351
2533
  }
2352
2534
  };
@@ -2356,43 +2538,44 @@ var Business = class {
2356
2538
  return this;
2357
2539
  },
2358
2540
  plan: (spec) => {
2359
- this.assertNewKey(this.plans, spec.key, "plan");
2360
- this.plans.set(spec.key, spec);
2541
+ this.assertNewKey("plan", spec.key, "plan");
2542
+ this.graph.register("plan", spec.key, spec, this.planDependsOn(spec));
2361
2543
  return this;
2362
2544
  },
2363
2545
  routesFile: (file) => {
2364
- this.assertNewKey(this.features, file.feature, "feature");
2365
- this.features.set(file.feature, file);
2546
+ this.assertNewKey("feature", file.feature, "feature");
2547
+ this.registerFeatureFile(file);
2366
2548
  return this;
2367
2549
  },
2368
2550
  policyFile: (file) => {
2369
- this.assertNewKey(this.policies, file.name, "policy");
2370
- this.policies.set(file.name, file);
2551
+ this.assertNewKey("policy", file.name, "policy");
2552
+ this.graph.register("policy", file.name, file);
2371
2553
  return this;
2372
2554
  },
2373
2555
  capabilityFile: (file) => {
2374
- this.assertNewKey(this.capabilities, file.capability, "capability");
2375
- this.capabilities.set(file.capability, file);
2556
+ this.assertNewKey("capability", file.capability, "capability");
2557
+ this.graph.register("capability", file.capability, file);
2376
2558
  return this;
2377
2559
  },
2378
2560
  frontend: (manifest) => {
2379
2561
  this.frontendManifest = manifest;
2562
+ this.syncFrontendGraphNode(manifest);
2380
2563
  return this;
2381
2564
  }
2382
2565
  };
2383
2566
  }
2384
2567
  meter(key, options) {
2385
- this.assertNewKey(this.meters, key, "meter");
2386
- this.meters.set(key, { key, ...options });
2568
+ this.assertNewKey("meter", key, "meter");
2569
+ this.graph.register("meter", key, { key, ...options });
2387
2570
  return { kind: "meter", key };
2388
2571
  }
2389
2572
  resource(name, options = {}) {
2390
- this.assertNewKey(this.resources, name, "resource");
2391
- this.resources.set(name, { name, ...options });
2573
+ this.assertNewKey("counted_resource", name, "resource");
2574
+ this.graph.register("counted_resource", name, { name, ...options });
2392
2575
  return { kind: "resource", key: name };
2393
2576
  }
2394
2577
  capability(key, options = {}) {
2395
- this.assertNewKey(this.capabilities, key, "capability");
2578
+ this.assertNewKey("capability", key, "capability");
2396
2579
  const file = {
2397
2580
  capability: key,
2398
2581
  ...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
@@ -2401,7 +2584,12 @@ var Business = class {
2401
2584
  ...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
2402
2585
  ...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
2403
2586
  };
2404
- this.capabilities.set(key, file);
2587
+ this.graph.register(
2588
+ "capability",
2589
+ key,
2590
+ file,
2591
+ this.capabilityDependsOn(file)
2592
+ );
2405
2593
  return {
2406
2594
  kind: "capability",
2407
2595
  key,
@@ -2413,7 +2601,7 @@ var Business = class {
2413
2601
  };
2414
2602
  }
2415
2603
  feature(key, options = {}) {
2416
- this.assertNewKey(this.features, key, "feature");
2604
+ this.assertNewKey("feature", key, "feature");
2417
2605
  const file = {
2418
2606
  feature: key,
2419
2607
  routes: [],
@@ -2440,30 +2628,34 @@ var Business = class {
2440
2628
  for (const route of options.routes ?? []) {
2441
2629
  file.routes.push(this.buildRoute(route.match, route));
2442
2630
  }
2443
- this.features.set(key, file);
2631
+ this.registerFeatureFile(file);
2444
2632
  const ref = {
2445
2633
  kind: "feature",
2446
2634
  key,
2447
2635
  action: (id, actionOptions) => {
2448
2636
  file.actions ??= [];
2449
- if (file.actions.some((action) => action.id === id)) {
2637
+ if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
2450
2638
  throw new ManifestBuilderError(
2451
2639
  `duplicate action "${id}" \u2014 each action id must be declared once`
2452
2640
  );
2453
2641
  }
2454
- file.actions.push({ id, ...actionOptions });
2642
+ const action = { id, ...actionOptions };
2643
+ file.actions.push(action);
2644
+ this.registerAction(key, action);
2645
+ this.syncFeatureGraphNode(file);
2455
2646
  return { kind: "action", key: id };
2456
2647
  },
2457
2648
  route: (match, routeOptions) => {
2458
2649
  file.routes.push(this.buildRoute(match, routeOptions ?? {}));
2650
+ this.syncFeatureGraphNode(file);
2459
2651
  return ref;
2460
2652
  }
2461
2653
  };
2462
2654
  return ref;
2463
2655
  }
2464
2656
  policy(name, options) {
2465
- this.assertNewKey(this.policies, name, "policy");
2466
- this.policies.set(name, {
2657
+ this.assertNewKey("policy", name, "policy");
2658
+ const file = {
2467
2659
  name,
2468
2660
  type: options.type,
2469
2661
  config: options.config,
@@ -2477,11 +2669,12 @@ var Business = class {
2477
2669
  ...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
2478
2670
  }
2479
2671
  } : {}
2480
- });
2672
+ };
2673
+ this.graph.register("policy", name, file, this.policyDependsOn(file));
2481
2674
  return { kind: "policy", key: name };
2482
2675
  }
2483
2676
  plan(key, options) {
2484
- this.assertNewKey(this.plans, key, "plan");
2677
+ this.assertNewKey("plan", key, "plan");
2485
2678
  const capabilityKeys = (options.capabilities ?? []).map(keyOf);
2486
2679
  const capabilityLimits = {
2487
2680
  ...options.capabilityLimits ?? {}
@@ -2530,20 +2723,37 @@ var Business = class {
2530
2723
  if (mergedCaps.length) {
2531
2724
  spec.capabilities = mergedCaps;
2532
2725
  }
2533
- this.plans.set(key, spec);
2726
+ this.graph.register("plan", key, spec, this.planDependsOn(spec));
2534
2727
  return { kind: "plan", key };
2535
2728
  }
2729
+ use(...modules) {
2730
+ for (const module of modules) {
2731
+ module(this);
2732
+ }
2733
+ return this;
2734
+ }
2735
+ /** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
2736
+ resourceGraph() {
2737
+ return this.graph.snapshot();
2738
+ }
2536
2739
  /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2537
2740
  * with structured issues when the declared state is invalid. */
2538
2741
  toIR() {
2742
+ this.assertGraphDependenciesSatisfied();
2539
2743
  const candidate = {
2540
2744
  irVersion: 1,
2541
2745
  sdkVersion: SDK_VERSION,
2542
2746
  product: this.buildProductSpec(),
2543
- routes: sortBy([...this.features.values()], (file) => file.feature),
2544
- policies: sortBy([...this.policies.values()], (file) => file.name),
2545
- capabilities: sortBy(
2546
- [...this.capabilities.values()],
2747
+ routes: this.graph.sortedValues(
2748
+ "feature",
2749
+ (file) => file.feature
2750
+ ),
2751
+ policies: this.graph.sortedValues(
2752
+ "policy",
2753
+ (file) => file.name
2754
+ ),
2755
+ capabilities: this.graph.sortedValues(
2756
+ "capability",
2547
2757
  (file) => file.capability
2548
2758
  ),
2549
2759
  runtime: { rollout: null, flags: null, migrations: null }
@@ -2571,19 +2781,34 @@ var Business = class {
2571
2781
  ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2572
2782
  },
2573
2783
  metering: {
2574
- meters: sortBy([...this.meters.values()], (meter) => meter.key),
2784
+ meters: this.graph.sortedValues(
2785
+ "meter",
2786
+ (meter) => meter.key
2787
+ ),
2575
2788
  ...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
2576
2789
  },
2577
2790
  ...options.billing !== void 0 ? { billing: options.billing } : {},
2578
- ...this.frontendManifest !== void 0 ? { frontend: this.frontendManifest } : {},
2579
- ...this.migrations.length ? { migrations: this.migrations } : {},
2580
- ...this.resources.size ? {
2581
- resources: sortBy(
2582
- [...this.resources.values()],
2791
+ ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2792
+ ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2793
+ ...this.graph.has("frontend", "manifest") ? {
2794
+ frontend: this.graph.get(
2795
+ "frontend",
2796
+ "manifest"
2797
+ )?.value
2798
+ } : {},
2799
+ ...this.graph.values("lifecycle_migration").length ? {
2800
+ migrations: this.graph.sortedValues(
2801
+ "lifecycle_migration",
2802
+ (migration) => migration.id
2803
+ )
2804
+ } : {},
2805
+ ...this.graph.values("counted_resource").length ? {
2806
+ resources: this.graph.sortedValues(
2807
+ "counted_resource",
2583
2808
  (resource) => resource.name
2584
2809
  )
2585
2810
  } : {},
2586
- plans: sortBy([...this.plans.values()], (plan) => plan.key)
2811
+ plans: this.graph.sortedValues("plan", (plan) => plan.key)
2587
2812
  };
2588
2813
  return deepMerge(
2589
2814
  base,
@@ -2601,6 +2826,7 @@ var Business = class {
2601
2826
  }
2602
2827
  ensureFrontendManifest() {
2603
2828
  this.frontendManifest ??= { version: 1, nav: [], pages: [] };
2829
+ this.syncFrontendGraphNode(this.frontendManifest);
2604
2830
  return this.frontendManifest;
2605
2831
  }
2606
2832
  normalizeMigrationPlanRef(ref) {
@@ -2615,13 +2841,6 @@ var Business = class {
2615
2841
  version: ref.version ?? "head"
2616
2842
  };
2617
2843
  }
2618
- assertUniqueMigrationId(id) {
2619
- if (this.migrations.some((migration) => migration.id === id)) {
2620
- throw new ManifestBuilderError(
2621
- `duplicate migration "${id}" \u2014 each migration id must be declared once`
2622
- );
2623
- }
2624
- }
2625
2844
  assertUniqueMigrationIds(migrations) {
2626
2845
  const seen = /* @__PURE__ */ new Set();
2627
2846
  for (const migration of migrations) {
@@ -2633,27 +2852,149 @@ var Business = class {
2633
2852
  seen.add(migration.id);
2634
2853
  }
2635
2854
  }
2636
- assertNewKey(map, key, label) {
2637
- if (map.has(key)) {
2855
+ assertNewKey(kind, key, label) {
2856
+ if (this.graph.has(kind, key)) {
2638
2857
  throw new ManifestBuilderError(
2639
2858
  `duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
2640
2859
  );
2641
2860
  }
2642
2861
  }
2862
+ assertGraphDependenciesSatisfied() {
2863
+ const missing = this.graph.missingDependencies();
2864
+ if (missing.length === 0) return;
2865
+ const details = missing.slice(0, 8).map(
2866
+ ({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
2867
+ dependency
2868
+ )}`
2869
+ ).join("; ");
2870
+ const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
2871
+ throw new ManifestBuilderError(
2872
+ `manifest has unresolved resource reference(s): ${details}${suffix}`
2873
+ );
2874
+ }
2875
+ getFeatureFile(key) {
2876
+ return this.graph.get("feature", key)?.value ?? null;
2877
+ }
2878
+ registerFeatureFile(file) {
2879
+ this.graph.register(
2880
+ "feature",
2881
+ file.feature,
2882
+ file,
2883
+ this.featureDependsOn(file)
2884
+ );
2885
+ for (const action of file.actions ?? []) {
2886
+ this.registerAction(file.feature, action);
2887
+ }
2888
+ }
2889
+ syncFeatureGraphNode(file) {
2890
+ this.graph.upsert(
2891
+ "feature",
2892
+ file.feature,
2893
+ file,
2894
+ this.featureDependsOn(file)
2895
+ );
2896
+ }
2897
+ registerAction(featureKey, action) {
2898
+ this.assertNewKey("action", action.id, "action");
2899
+ this.graph.register(
2900
+ "action",
2901
+ action.id,
2902
+ action,
2903
+ this.actionDependsOn(featureKey, action)
2904
+ );
2905
+ }
2906
+ syncFrontendGraphNode(manifest) {
2907
+ this.graph.upsert(
2908
+ "frontend",
2909
+ "manifest",
2910
+ manifest,
2911
+ this.frontendDependsOn(manifest)
2912
+ );
2913
+ }
2914
+ capabilityDependsOn(file) {
2915
+ return [
2916
+ ...this.dependenciesFor("feature", file.includes_features),
2917
+ ...this.dependenciesFor("policy", file.includes_policies),
2918
+ ...this.dependenciesFor("capability", file.includes_capabilities)
2919
+ ];
2920
+ }
2921
+ policyDependsOn(file) {
2922
+ return this.dependenciesFor("meter", file.compatible_with?.meters);
2923
+ }
2924
+ featureDependsOn(file) {
2925
+ const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
2926
+ return [
2927
+ ...this.dependenciesFor("policy", file.policies),
2928
+ ...this.dependenciesFor("capability", file.capabilities),
2929
+ ...this.dependenciesFor("plan", file.plans),
2930
+ ...this.dependenciesFor("meter", meterKeys)
2931
+ ];
2932
+ }
2933
+ actionDependsOn(featureKey, action) {
2934
+ return [
2935
+ resourceDependency("feature", featureKey),
2936
+ ...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
2937
+ ];
2938
+ }
2939
+ planDependsOn(plan) {
2940
+ const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
2941
+ const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
2942
+ const pricedMeterDimensions = (plan.meters ?? []).map(
2943
+ (meter) => meter.dimension
2944
+ );
2945
+ const capacityKeys = Object.keys(plan.capability_limits ?? {});
2946
+ return [
2947
+ ...this.dependenciesFor("capability", caps),
2948
+ ...this.existingDependenciesFor("meter", [
2949
+ ...limitDimensions,
2950
+ ...pricedMeterDimensions
2951
+ ]),
2952
+ ...this.existingDependenciesFor("counted_resource", capacityKeys)
2953
+ ];
2954
+ }
2955
+ migrationDependsOn(migration) {
2956
+ return [
2957
+ resourceDependency("plan", migration.from.plan),
2958
+ resourceDependency("plan", migration.to.plan),
2959
+ ...this.dependenciesFor(
2960
+ "plan",
2961
+ migration.pins?.map((pin) => pin.pinTo.plan)
2962
+ )
2963
+ ];
2964
+ }
2965
+ frontendDependsOn(manifest) {
2966
+ return this.dependenciesFor("capability", [
2967
+ ...(manifest.nav ?? []).flatMap(
2968
+ (item) => item.capability ? [item.capability] : []
2969
+ ),
2970
+ ...(manifest.pages ?? []).flatMap((page) => [
2971
+ ...page.capability ? [page.capability] : [],
2972
+ ...(page.components ?? []).flatMap(
2973
+ (component) => component.capability ? [component.capability] : []
2974
+ )
2975
+ ])
2976
+ ]);
2977
+ }
2978
+ dependenciesFor(kind, keys) {
2979
+ return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
2980
+ }
2981
+ existingDependenciesFor(kind, keys) {
2982
+ return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
2983
+ }
2643
2984
  };
2644
2985
  function isBusiness(value) {
2645
2986
  return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
2646
2987
  }
2647
- function sortBy(items, key) {
2648
- return [...items].sort((a, b) => {
2649
- const ka = key(a);
2650
- const kb = key(b);
2651
- return ka < kb ? -1 : ka > kb ? 1 : 0;
2652
- });
2653
- }
2654
2988
  function isPlainObject(value) {
2655
2989
  return typeof value === "object" && value !== null && !Array.isArray(value);
2656
2990
  }
2991
+ function buildCustomerContext(options) {
2992
+ return {
2993
+ ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
2994
+ ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
2995
+ ...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
2996
+ };
2997
+ }
2657
2998
  function deepMerge(base, patch) {
2658
2999
  const out = { ...base };
2659
3000
  for (const [key, value] of Object.entries(patch)) {
@@ -2666,11 +3007,21 @@ function deepMerge(base, patch) {
2666
3007
  }
2667
3008
  return out;
2668
3009
  }
3010
+ function describeResourceUrn(urn) {
3011
+ const parts = urn.split(":");
3012
+ const kind = parts[3] ?? "resource";
3013
+ const key = parts.slice(4).join(":");
3014
+ return `${kind} "${decodeURIComponent(key)}"`;
3015
+ }
2669
3016
 
2670
3017
  // src/bin.ts
3018
+ var DEFAULT_ENTRY_CANDIDATES = [
3019
+ "product/product.config.ts",
3020
+ "product.config.ts"
3021
+ ];
2671
3022
  function parseArgs(argv) {
2672
3023
  const args = {
2673
- entry: "product.config.ts",
3024
+ entry: null,
2674
3025
  out: "manifest-ir.json",
2675
3026
  diagnosticsOut: "manifest-diagnostics.json"
2676
3027
  };
@@ -2688,13 +3039,35 @@ function parseArgs(argv) {
2688
3039
  i++;
2689
3040
  } else if (flag === "--help" || flag === "-h") {
2690
3041
  process.stdout.write(
2691
- "Usage: farthershore-manifest-build [--entry product.config.ts] [--out manifest-ir.json] [--diagnostics-out manifest-diagnostics.json]\n"
3042
+ "Usage: farthershore-manifest-build [--entry product/product.config.ts] [--out manifest-ir.json] [--diagnostics-out manifest-diagnostics.json]\n"
2692
3043
  );
2693
3044
  process.exit(0);
2694
3045
  }
2695
3046
  }
2696
3047
  return args;
2697
3048
  }
3049
+ async function fileExists(path) {
3050
+ try {
3051
+ await access(path, constants.R_OK);
3052
+ return true;
3053
+ } catch {
3054
+ return false;
3055
+ }
3056
+ }
3057
+ async function resolveEntry(args) {
3058
+ if (args.entry) {
3059
+ return { entry: args.entry, entryPath: resolve(process.cwd(), args.entry) };
3060
+ }
3061
+ for (const candidate of DEFAULT_ENTRY_CANDIDATES) {
3062
+ const entryPath = resolve(process.cwd(), candidate);
3063
+ if (await fileExists(entryPath)) {
3064
+ return { entry: candidate, entryPath };
3065
+ }
3066
+ }
3067
+ throw new Error(
3068
+ `no Product SDK entry found; expected ${DEFAULT_ENTRY_CANDIDATES.join(" or ")}`
3069
+ );
3070
+ }
2698
3071
  function validationIssues(error) {
2699
3072
  if (error instanceof ManifestValidationError) return error.issues;
2700
3073
  if (typeof error === "object" && error !== null && error.name === "ManifestValidationError" && Array.isArray(error.issues)) {
@@ -2707,16 +3080,25 @@ function errorMessage(error) {
2707
3080
  }
2708
3081
  async function main() {
2709
3082
  const args = parseArgs(process.argv.slice(2));
2710
- const entryPath = resolve(process.cwd(), args.entry);
3083
+ let resolvedEntry;
3084
+ try {
3085
+ resolvedEntry = await resolveEntry(args);
3086
+ } catch (error) {
3087
+ process.stderr.write(
3088
+ `farthershore-manifest-build: ${errorMessage(error)}
3089
+ `
3090
+ );
3091
+ process.exit(1);
3092
+ }
2711
3093
  let mod;
2712
3094
  try {
2713
3095
  mod = await tsImport(
2714
- pathToFileURL(entryPath).href,
3096
+ pathToFileURL(resolvedEntry.entryPath).href,
2715
3097
  import.meta.url
2716
3098
  );
2717
3099
  } catch (error) {
2718
3100
  process.stderr.write(
2719
- `farthershore-manifest-build: failed to load ${args.entry}: ${error instanceof Error ? error.message : String(error)}
3101
+ `farthershore-manifest-build: failed to load ${resolvedEntry.entry}: ${error instanceof Error ? error.message : String(error)}
2720
3102
  `
2721
3103
  );
2722
3104
  process.exit(1);
@@ -2726,7 +3108,7 @@ async function main() {
2726
3108
  const candidate = isBusiness(direct) ? direct : interop;
2727
3109
  if (!isBusiness(candidate)) {
2728
3110
  process.stderr.write(
2729
- `farthershore-manifest-build: ${args.entry} must \`export default\` the Business returned by fs.business(\u2026)
3111
+ `farthershore-manifest-build: ${resolvedEntry.entry} must \`export default\` the Business returned by fs.business(\u2026)
2730
3112
  `
2731
3113
  );
2732
3114
  process.exit(1);