@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/index.js CHANGED
@@ -20,6 +20,97 @@ var ManifestBuilderError = class extends Error {
20
20
  }
21
21
  };
22
22
 
23
+ // src/resource-graph.ts
24
+ var ManifestResourceGraph = class {
25
+ nodes = /* @__PURE__ */ new Map();
26
+ declarationOrder = 0;
27
+ register(kind, key, value, dependsOn = []) {
28
+ const id = nodeId(kind, key);
29
+ if (this.nodes.has(id)) {
30
+ throw new Error(`duplicate resource node ${id}`);
31
+ }
32
+ const node = {
33
+ urn: resourceUrn(kind, key),
34
+ kind,
35
+ key,
36
+ value,
37
+ dependsOn: [...new Set(dependsOn)],
38
+ declarationOrder: this.declarationOrder
39
+ };
40
+ this.declarationOrder += 1;
41
+ this.nodes.set(id, node);
42
+ return node;
43
+ }
44
+ upsert(kind, key, value, dependsOn = []) {
45
+ const id = nodeId(kind, key);
46
+ const existing = this.nodes.get(id);
47
+ if (!existing) {
48
+ return this.register(kind, key, value, dependsOn);
49
+ }
50
+ const node = {
51
+ urn: existing.urn,
52
+ kind,
53
+ key,
54
+ value,
55
+ dependsOn: [...new Set(dependsOn)],
56
+ declarationOrder: existing.declarationOrder
57
+ };
58
+ this.nodes.set(id, node);
59
+ return node;
60
+ }
61
+ clearKind(kind) {
62
+ for (const [id, node] of this.nodes.entries()) {
63
+ if (node.kind === kind) this.nodes.delete(id);
64
+ }
65
+ }
66
+ has(kind, key) {
67
+ return this.nodes.has(nodeId(kind, key));
68
+ }
69
+ get(kind, key) {
70
+ return this.nodes.get(nodeId(kind, key)) ?? null;
71
+ }
72
+ values(kind) {
73
+ return [...this.nodes.values()].filter((node) => node.kind === kind).map((node) => node.value);
74
+ }
75
+ sortedValues(kind, key) {
76
+ return sortBy(this.values(kind), key);
77
+ }
78
+ snapshot() {
79
+ return {
80
+ nodes: sortBy([...this.nodes.values()], (node) => node.urn).map(
81
+ ({ value: _value, ...node }) => node
82
+ )
83
+ };
84
+ }
85
+ missingDependencies() {
86
+ const known = new Set([...this.nodes.values()].map((node) => node.urn));
87
+ return [...this.nodes.values()].flatMap(
88
+ (node) => node.dependsOn.filter((dependency) => !known.has(dependency)).map((dependency) => ({
89
+ from: node.urn,
90
+ missing: dependency
91
+ }))
92
+ );
93
+ }
94
+ };
95
+ function resourceUrn(kind, key) {
96
+ return `urn:farthershore:product:${kind}:${encodeURIComponent(
97
+ key
98
+ )}`;
99
+ }
100
+ function resourceDependency(kind, key) {
101
+ return resourceUrn(kind, key);
102
+ }
103
+ function nodeId(kind, key) {
104
+ return `${kind}:${key}`;
105
+ }
106
+ function sortBy(items, key) {
107
+ return [...items].sort((a, b) => {
108
+ const ka = key(a);
109
+ const kb = key(b);
110
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
111
+ });
112
+ }
113
+
23
114
  // ../contracts/dist/plans/limits-schema.js
24
115
  import { z } from "zod";
25
116
  var limitDimensionSchema = z.string().min(1);
@@ -1244,6 +1335,77 @@ var featureCatalogEntrySchema = z13.object({
1244
1335
  plans: z13.array(z13.string().min(1)).min(1).max(20)
1245
1336
  });
1246
1337
  var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1338
+ var productCleanupPolicyModeSchema = z13.enum([
1339
+ "report",
1340
+ "pull_request"
1341
+ ]);
1342
+ var productChangeApprovalRiskSchema = z13.enum([
1343
+ "safe",
1344
+ "non_blocking",
1345
+ "economic_risk",
1346
+ "blocking"
1347
+ ]);
1348
+ var productOperatorPoliciesSchema = z13.object({
1349
+ /**
1350
+ * Route cleanup operator. Disabled by default; report-mode is the safe
1351
+ * default so a product can surface zero-traffic runtime-route candidates
1352
+ * before it opts into draft PR mutation.
1353
+ */
1354
+ cleanup: z13.object({
1355
+ enabled: z13.boolean().default(false),
1356
+ mode: productCleanupPolicyModeSchema.default("report")
1357
+ }).default({ enabled: false, mode: "report" }),
1358
+ /**
1359
+ * PR approval thresholds for manifest impact reports. This schema makes
1360
+ * the policy a first-class product-as-code field even while enforcement is
1361
+ * still report/label-only.
1362
+ */
1363
+ change_approval: z13.object({
1364
+ auto_merge_max_risk: z13.enum(["none", "safe", "non_blocking"]).default("none"),
1365
+ require_human_for: z13.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
1366
+ }).default({
1367
+ auto_merge_max_risk: "none",
1368
+ require_human_for: ["economic_risk", "blocking"]
1369
+ })
1370
+ }).default({
1371
+ cleanup: { enabled: false, mode: "report" },
1372
+ change_approval: {
1373
+ auto_merge_max_risk: "none",
1374
+ require_human_for: ["economic_risk", "blocking"]
1375
+ }
1376
+ });
1377
+ var customerIdentityRequirementSchema = z13.enum([
1378
+ "org_only",
1379
+ "org_and_user"
1380
+ ]);
1381
+ var customerPortalAuthStrategySchema = z13.enum([
1382
+ "clerk",
1383
+ "test-personas"
1384
+ ]);
1385
+ var productCustomerContextSchema = z13.object({
1386
+ /**
1387
+ * Edge credential identity policy. This is intentionally Product-scoped:
1388
+ * B7 keeps Product as the business boundary and avoids customer-side
1389
+ * Workspace/subject models until per-subject entitlements need them.
1390
+ */
1391
+ identity_requirement: customerIdentityRequirementSchema.optional(),
1392
+ /**
1393
+ * Enables signed customer-context tokens (`fsc_*`) without putting the
1394
+ * runtime signing secret in product/product.config.ts. Core generates/preserves the
1395
+ * secret when this is true and clears it when explicitly false.
1396
+ */
1397
+ context_tokens: z13.object({
1398
+ enabled: z13.boolean().default(true)
1399
+ }).optional(),
1400
+ /**
1401
+ * Portal auth strategy for environment-scoped product applies. Production
1402
+ * portal auth is provisioner-owned; preview/test environments can opt into
1403
+ * test personas through Product-as-Code.
1404
+ */
1405
+ portal_auth: z13.object({
1406
+ strategy: customerPortalAuthStrategySchema
1407
+ }).optional()
1408
+ });
1247
1409
  var productSpecSchema = z13.object({
1248
1410
  product: z13.object({
1249
1411
  name: z13.string().min(1).max(100),
@@ -1275,11 +1437,12 @@ var productSpecSchema = z13.object({
1275
1437
  }).optional(),
1276
1438
  features: featureCatalogSchema.optional(),
1277
1439
  resources: countedResourcesSchema,
1440
+ policies: productOperatorPoliciesSchema,
1441
+ customer_context: productCustomerContextSchema.optional(),
1278
1442
  /**
1279
- * Track B4 — Declarative frontend surface. Product code can declare
1280
- * template-owned nav/pages composed from known portal components. The
1281
- * template renders these for non-reserved paths; contractual dashboard
1282
- * sections remain template-owned.
1443
+ * Legacy/internal declarative frontend surface. New product repos customize
1444
+ * the generated `frontend/` project directly; this remains for existing IR
1445
+ * and internal template metadata.
1283
1446
  */
1284
1447
  frontend: frontendManifestSchema.optional(),
1285
1448
  migrations: migrationDeclsSchema.optional(),
@@ -1403,7 +1566,7 @@ var productSpecSchema = z13.object({
1403
1566
  *
1404
1567
  * Once a product has compiled with a `webhooks` block, API mutations
1405
1568
  * on those endpoints fail with `409 MANAGED_BY_CODE` — see
1406
- * `core/src/routes/management-webhooks.ts`. Tie-breaker: product.config.ts
1569
+ * `core/src/routes/management-webhooks.ts`. Tie-breaker: product/product.config.ts
1407
1570
  * wins over API state if an endpoint id appears in both.
1408
1571
  */
1409
1572
  webhooks: webhooksBlockSchema.optional(),
@@ -1990,6 +2153,8 @@ var productSpecV2Schema = z20.object({
1990
2153
  frontend: frontendManifestSchema.optional(),
1991
2154
  migrations: migrationDeclsSchema.optional(),
1992
2155
  resources: countedResourcesSchema,
2156
+ policies: productOperatorPoliciesSchema,
2157
+ customer_context: productCustomerContextSchema.optional(),
1993
2158
  billing: z20.object({
1994
2159
  gracePeriodDays: z20.number().int().nonnegative().default(3),
1995
2160
  // When true (default), a plan limit INCREASE re-projects onto active
@@ -2190,7 +2355,7 @@ function canonicalIrJson(ir) {
2190
2355
  }
2191
2356
 
2192
2357
  // src/version.ts
2193
- var SDK_VERSION = true ? "0.0.0" : "0.0.0-dev";
2358
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
2194
2359
 
2195
2360
  // src/business.ts
2196
2361
  var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
@@ -2239,13 +2404,7 @@ var Business = class {
2239
2404
  [BUSINESS_BRAND] = true;
2240
2405
  name;
2241
2406
  options;
2242
- meters = /* @__PURE__ */ new Map();
2243
- resources = /* @__PURE__ */ new Map();
2244
- plans = /* @__PURE__ */ new Map();
2245
- features = /* @__PURE__ */ new Map();
2246
- policies = /* @__PURE__ */ new Map();
2247
- capabilities = /* @__PURE__ */ new Map();
2248
- migrations = [];
2407
+ graph = new ManifestResourceGraph();
2249
2408
  frontendManifest;
2250
2409
  productPatch = {};
2251
2410
  /** Sugar for binding API routes to features. */
@@ -2265,16 +2424,21 @@ var Business = class {
2265
2424
  }
2266
2425
  this.name = name;
2267
2426
  this.options = options;
2427
+ this.graph.register("product", name, {
2428
+ name,
2429
+ options
2430
+ });
2268
2431
  this.api = {
2269
2432
  route: (match, options2) => {
2270
2433
  const featureKey = keyOf(options2.feature);
2271
- const file = this.features.get(featureKey);
2434
+ const file = this.getFeatureFile(featureKey);
2272
2435
  if (!file) {
2273
2436
  throw new ManifestBuilderError(
2274
2437
  `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
2275
2438
  );
2276
2439
  }
2277
2440
  file.routes.push(this.buildRoute(match, options2));
2441
+ this.syncFeatureGraphNode(file);
2278
2442
  return this;
2279
2443
  }
2280
2444
  };
@@ -2286,6 +2450,7 @@ var Business = class {
2286
2450
  path: item.path,
2287
2451
  ...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
2288
2452
  }));
2453
+ this.syncFrontendGraphNode(manifest);
2289
2454
  return this;
2290
2455
  },
2291
2456
  page: (path, options2) => {
@@ -2310,17 +2475,19 @@ var Business = class {
2310
2475
  }))
2311
2476
  } : {}
2312
2477
  });
2478
+ this.syncFrontendGraphNode(manifest);
2313
2479
  return this;
2314
2480
  },
2315
2481
  manifest: (manifest) => {
2316
2482
  this.frontendManifest = manifest;
2483
+ this.syncFrontendGraphNode(manifest);
2317
2484
  return this;
2318
2485
  }
2319
2486
  };
2320
2487
  this.lifecycle = {
2321
2488
  migration: (id, options2) => {
2322
- this.assertUniqueMigrationId(id);
2323
- this.migrations.push({
2489
+ this.assertNewKey("lifecycle_migration", id, "migration");
2490
+ const migration = {
2324
2491
  id,
2325
2492
  from: this.normalizeMigrationPlanRef(options2.from),
2326
2493
  to: this.normalizeMigrationTargetRef(options2.to),
@@ -2337,12 +2504,26 @@ var Business = class {
2337
2504
  ...pin.notes !== void 0 ? { notes: pin.notes } : {}
2338
2505
  }))
2339
2506
  } : {}
2340
- });
2507
+ };
2508
+ this.graph.register(
2509
+ "lifecycle_migration",
2510
+ id,
2511
+ migration,
2512
+ this.migrationDependsOn(migration)
2513
+ );
2341
2514
  return this;
2342
2515
  },
2343
2516
  migrations: (migrations) => {
2344
2517
  this.assertUniqueMigrationIds(migrations);
2345
- this.migrations = migrations;
2518
+ this.graph.clearKind("lifecycle_migration");
2519
+ for (const migration of migrations) {
2520
+ this.graph.register(
2521
+ "lifecycle_migration",
2522
+ migration.id,
2523
+ migration,
2524
+ this.migrationDependsOn(migration)
2525
+ );
2526
+ }
2346
2527
  return this;
2347
2528
  }
2348
2529
  };
@@ -2352,43 +2533,44 @@ var Business = class {
2352
2533
  return this;
2353
2534
  },
2354
2535
  plan: (spec) => {
2355
- this.assertNewKey(this.plans, spec.key, "plan");
2356
- this.plans.set(spec.key, spec);
2536
+ this.assertNewKey("plan", spec.key, "plan");
2537
+ this.graph.register("plan", spec.key, spec, this.planDependsOn(spec));
2357
2538
  return this;
2358
2539
  },
2359
2540
  routesFile: (file) => {
2360
- this.assertNewKey(this.features, file.feature, "feature");
2361
- this.features.set(file.feature, file);
2541
+ this.assertNewKey("feature", file.feature, "feature");
2542
+ this.registerFeatureFile(file);
2362
2543
  return this;
2363
2544
  },
2364
2545
  policyFile: (file) => {
2365
- this.assertNewKey(this.policies, file.name, "policy");
2366
- this.policies.set(file.name, file);
2546
+ this.assertNewKey("policy", file.name, "policy");
2547
+ this.graph.register("policy", file.name, file);
2367
2548
  return this;
2368
2549
  },
2369
2550
  capabilityFile: (file) => {
2370
- this.assertNewKey(this.capabilities, file.capability, "capability");
2371
- this.capabilities.set(file.capability, file);
2551
+ this.assertNewKey("capability", file.capability, "capability");
2552
+ this.graph.register("capability", file.capability, file);
2372
2553
  return this;
2373
2554
  },
2374
2555
  frontend: (manifest) => {
2375
2556
  this.frontendManifest = manifest;
2557
+ this.syncFrontendGraphNode(manifest);
2376
2558
  return this;
2377
2559
  }
2378
2560
  };
2379
2561
  }
2380
2562
  meter(key, options) {
2381
- this.assertNewKey(this.meters, key, "meter");
2382
- this.meters.set(key, { key, ...options });
2563
+ this.assertNewKey("meter", key, "meter");
2564
+ this.graph.register("meter", key, { key, ...options });
2383
2565
  return { kind: "meter", key };
2384
2566
  }
2385
2567
  resource(name, options = {}) {
2386
- this.assertNewKey(this.resources, name, "resource");
2387
- this.resources.set(name, { name, ...options });
2568
+ this.assertNewKey("counted_resource", name, "resource");
2569
+ this.graph.register("counted_resource", name, { name, ...options });
2388
2570
  return { kind: "resource", key: name };
2389
2571
  }
2390
2572
  capability(key, options = {}) {
2391
- this.assertNewKey(this.capabilities, key, "capability");
2573
+ this.assertNewKey("capability", key, "capability");
2392
2574
  const file = {
2393
2575
  capability: key,
2394
2576
  ...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
@@ -2397,7 +2579,12 @@ var Business = class {
2397
2579
  ...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
2398
2580
  ...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
2399
2581
  };
2400
- this.capabilities.set(key, file);
2582
+ this.graph.register(
2583
+ "capability",
2584
+ key,
2585
+ file,
2586
+ this.capabilityDependsOn(file)
2587
+ );
2401
2588
  return {
2402
2589
  kind: "capability",
2403
2590
  key,
@@ -2409,7 +2596,7 @@ var Business = class {
2409
2596
  };
2410
2597
  }
2411
2598
  feature(key, options = {}) {
2412
- this.assertNewKey(this.features, key, "feature");
2599
+ this.assertNewKey("feature", key, "feature");
2413
2600
  const file = {
2414
2601
  feature: key,
2415
2602
  routes: [],
@@ -2436,30 +2623,34 @@ var Business = class {
2436
2623
  for (const route of options.routes ?? []) {
2437
2624
  file.routes.push(this.buildRoute(route.match, route));
2438
2625
  }
2439
- this.features.set(key, file);
2626
+ this.registerFeatureFile(file);
2440
2627
  const ref = {
2441
2628
  kind: "feature",
2442
2629
  key,
2443
2630
  action: (id, actionOptions) => {
2444
2631
  file.actions ??= [];
2445
- if (file.actions.some((action) => action.id === id)) {
2632
+ if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
2446
2633
  throw new ManifestBuilderError(
2447
2634
  `duplicate action "${id}" \u2014 each action id must be declared once`
2448
2635
  );
2449
2636
  }
2450
- file.actions.push({ id, ...actionOptions });
2637
+ const action = { id, ...actionOptions };
2638
+ file.actions.push(action);
2639
+ this.registerAction(key, action);
2640
+ this.syncFeatureGraphNode(file);
2451
2641
  return { kind: "action", key: id };
2452
2642
  },
2453
2643
  route: (match, routeOptions) => {
2454
2644
  file.routes.push(this.buildRoute(match, routeOptions ?? {}));
2645
+ this.syncFeatureGraphNode(file);
2455
2646
  return ref;
2456
2647
  }
2457
2648
  };
2458
2649
  return ref;
2459
2650
  }
2460
2651
  policy(name, options) {
2461
- this.assertNewKey(this.policies, name, "policy");
2462
- this.policies.set(name, {
2652
+ this.assertNewKey("policy", name, "policy");
2653
+ const file = {
2463
2654
  name,
2464
2655
  type: options.type,
2465
2656
  config: options.config,
@@ -2473,11 +2664,12 @@ var Business = class {
2473
2664
  ...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
2474
2665
  }
2475
2666
  } : {}
2476
- });
2667
+ };
2668
+ this.graph.register("policy", name, file, this.policyDependsOn(file));
2477
2669
  return { kind: "policy", key: name };
2478
2670
  }
2479
2671
  plan(key, options) {
2480
- this.assertNewKey(this.plans, key, "plan");
2672
+ this.assertNewKey("plan", key, "plan");
2481
2673
  const capabilityKeys = (options.capabilities ?? []).map(keyOf);
2482
2674
  const capabilityLimits = {
2483
2675
  ...options.capabilityLimits ?? {}
@@ -2526,20 +2718,37 @@ var Business = class {
2526
2718
  if (mergedCaps.length) {
2527
2719
  spec.capabilities = mergedCaps;
2528
2720
  }
2529
- this.plans.set(key, spec);
2721
+ this.graph.register("plan", key, spec, this.planDependsOn(spec));
2530
2722
  return { kind: "plan", key };
2531
2723
  }
2724
+ use(...modules) {
2725
+ for (const module of modules) {
2726
+ module(this);
2727
+ }
2728
+ return this;
2729
+ }
2730
+ /** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
2731
+ resourceGraph() {
2732
+ return this.graph.snapshot();
2733
+ }
2532
2734
  /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2533
2735
  * with structured issues when the declared state is invalid. */
2534
2736
  toIR() {
2737
+ this.assertGraphDependenciesSatisfied();
2535
2738
  const candidate = {
2536
2739
  irVersion: 1,
2537
2740
  sdkVersion: SDK_VERSION,
2538
2741
  product: this.buildProductSpec(),
2539
- routes: sortBy([...this.features.values()], (file) => file.feature),
2540
- policies: sortBy([...this.policies.values()], (file) => file.name),
2541
- capabilities: sortBy(
2542
- [...this.capabilities.values()],
2742
+ routes: this.graph.sortedValues(
2743
+ "feature",
2744
+ (file) => file.feature
2745
+ ),
2746
+ policies: this.graph.sortedValues(
2747
+ "policy",
2748
+ (file) => file.name
2749
+ ),
2750
+ capabilities: this.graph.sortedValues(
2751
+ "capability",
2543
2752
  (file) => file.capability
2544
2753
  ),
2545
2754
  runtime: { rollout: null, flags: null, migrations: null }
@@ -2567,19 +2776,34 @@ var Business = class {
2567
2776
  ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2568
2777
  },
2569
2778
  metering: {
2570
- meters: sortBy([...this.meters.values()], (meter) => meter.key),
2779
+ meters: this.graph.sortedValues(
2780
+ "meter",
2781
+ (meter) => meter.key
2782
+ ),
2571
2783
  ...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
2572
2784
  },
2573
2785
  ...options.billing !== void 0 ? { billing: options.billing } : {},
2574
- ...this.frontendManifest !== void 0 ? { frontend: this.frontendManifest } : {},
2575
- ...this.migrations.length ? { migrations: this.migrations } : {},
2576
- ...this.resources.size ? {
2577
- resources: sortBy(
2578
- [...this.resources.values()],
2786
+ ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2787
+ ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2788
+ ...this.graph.has("frontend", "manifest") ? {
2789
+ frontend: this.graph.get(
2790
+ "frontend",
2791
+ "manifest"
2792
+ )?.value
2793
+ } : {},
2794
+ ...this.graph.values("lifecycle_migration").length ? {
2795
+ migrations: this.graph.sortedValues(
2796
+ "lifecycle_migration",
2797
+ (migration) => migration.id
2798
+ )
2799
+ } : {},
2800
+ ...this.graph.values("counted_resource").length ? {
2801
+ resources: this.graph.sortedValues(
2802
+ "counted_resource",
2579
2803
  (resource) => resource.name
2580
2804
  )
2581
2805
  } : {},
2582
- plans: sortBy([...this.plans.values()], (plan) => plan.key)
2806
+ plans: this.graph.sortedValues("plan", (plan) => plan.key)
2583
2807
  };
2584
2808
  return deepMerge(
2585
2809
  base,
@@ -2597,6 +2821,7 @@ var Business = class {
2597
2821
  }
2598
2822
  ensureFrontendManifest() {
2599
2823
  this.frontendManifest ??= { version: 1, nav: [], pages: [] };
2824
+ this.syncFrontendGraphNode(this.frontendManifest);
2600
2825
  return this.frontendManifest;
2601
2826
  }
2602
2827
  normalizeMigrationPlanRef(ref) {
@@ -2611,13 +2836,6 @@ var Business = class {
2611
2836
  version: ref.version ?? "head"
2612
2837
  };
2613
2838
  }
2614
- assertUniqueMigrationId(id) {
2615
- if (this.migrations.some((migration) => migration.id === id)) {
2616
- throw new ManifestBuilderError(
2617
- `duplicate migration "${id}" \u2014 each migration id must be declared once`
2618
- );
2619
- }
2620
- }
2621
2839
  assertUniqueMigrationIds(migrations) {
2622
2840
  const seen = /* @__PURE__ */ new Set();
2623
2841
  for (const migration of migrations) {
@@ -2629,30 +2847,154 @@ var Business = class {
2629
2847
  seen.add(migration.id);
2630
2848
  }
2631
2849
  }
2632
- assertNewKey(map, key, label) {
2633
- if (map.has(key)) {
2850
+ assertNewKey(kind, key, label) {
2851
+ if (this.graph.has(kind, key)) {
2634
2852
  throw new ManifestBuilderError(
2635
2853
  `duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
2636
2854
  );
2637
2855
  }
2638
2856
  }
2857
+ assertGraphDependenciesSatisfied() {
2858
+ const missing = this.graph.missingDependencies();
2859
+ if (missing.length === 0) return;
2860
+ const details = missing.slice(0, 8).map(
2861
+ ({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
2862
+ dependency
2863
+ )}`
2864
+ ).join("; ");
2865
+ const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
2866
+ throw new ManifestBuilderError(
2867
+ `manifest has unresolved resource reference(s): ${details}${suffix}`
2868
+ );
2869
+ }
2870
+ getFeatureFile(key) {
2871
+ return this.graph.get("feature", key)?.value ?? null;
2872
+ }
2873
+ registerFeatureFile(file) {
2874
+ this.graph.register(
2875
+ "feature",
2876
+ file.feature,
2877
+ file,
2878
+ this.featureDependsOn(file)
2879
+ );
2880
+ for (const action of file.actions ?? []) {
2881
+ this.registerAction(file.feature, action);
2882
+ }
2883
+ }
2884
+ syncFeatureGraphNode(file) {
2885
+ this.graph.upsert(
2886
+ "feature",
2887
+ file.feature,
2888
+ file,
2889
+ this.featureDependsOn(file)
2890
+ );
2891
+ }
2892
+ registerAction(featureKey, action) {
2893
+ this.assertNewKey("action", action.id, "action");
2894
+ this.graph.register(
2895
+ "action",
2896
+ action.id,
2897
+ action,
2898
+ this.actionDependsOn(featureKey, action)
2899
+ );
2900
+ }
2901
+ syncFrontendGraphNode(manifest) {
2902
+ this.graph.upsert(
2903
+ "frontend",
2904
+ "manifest",
2905
+ manifest,
2906
+ this.frontendDependsOn(manifest)
2907
+ );
2908
+ }
2909
+ capabilityDependsOn(file) {
2910
+ return [
2911
+ ...this.dependenciesFor("feature", file.includes_features),
2912
+ ...this.dependenciesFor("policy", file.includes_policies),
2913
+ ...this.dependenciesFor("capability", file.includes_capabilities)
2914
+ ];
2915
+ }
2916
+ policyDependsOn(file) {
2917
+ return this.dependenciesFor("meter", file.compatible_with?.meters);
2918
+ }
2919
+ featureDependsOn(file) {
2920
+ const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
2921
+ return [
2922
+ ...this.dependenciesFor("policy", file.policies),
2923
+ ...this.dependenciesFor("capability", file.capabilities),
2924
+ ...this.dependenciesFor("plan", file.plans),
2925
+ ...this.dependenciesFor("meter", meterKeys)
2926
+ ];
2927
+ }
2928
+ actionDependsOn(featureKey, action) {
2929
+ return [
2930
+ resourceDependency("feature", featureKey),
2931
+ ...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
2932
+ ];
2933
+ }
2934
+ planDependsOn(plan) {
2935
+ const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
2936
+ const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
2937
+ const pricedMeterDimensions = (plan.meters ?? []).map(
2938
+ (meter) => meter.dimension
2939
+ );
2940
+ const capacityKeys = Object.keys(plan.capability_limits ?? {});
2941
+ return [
2942
+ ...this.dependenciesFor("capability", caps),
2943
+ ...this.existingDependenciesFor("meter", [
2944
+ ...limitDimensions,
2945
+ ...pricedMeterDimensions
2946
+ ]),
2947
+ ...this.existingDependenciesFor("counted_resource", capacityKeys)
2948
+ ];
2949
+ }
2950
+ migrationDependsOn(migration) {
2951
+ return [
2952
+ resourceDependency("plan", migration.from.plan),
2953
+ resourceDependency("plan", migration.to.plan),
2954
+ ...this.dependenciesFor(
2955
+ "plan",
2956
+ migration.pins?.map((pin) => pin.pinTo.plan)
2957
+ )
2958
+ ];
2959
+ }
2960
+ frontendDependsOn(manifest) {
2961
+ return this.dependenciesFor("capability", [
2962
+ ...(manifest.nav ?? []).flatMap(
2963
+ (item) => item.capability ? [item.capability] : []
2964
+ ),
2965
+ ...(manifest.pages ?? []).flatMap((page) => [
2966
+ ...page.capability ? [page.capability] : [],
2967
+ ...(page.components ?? []).flatMap(
2968
+ (component) => component.capability ? [component.capability] : []
2969
+ )
2970
+ ])
2971
+ ]);
2972
+ }
2973
+ dependenciesFor(kind, keys) {
2974
+ return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
2975
+ }
2976
+ existingDependenciesFor(kind, keys) {
2977
+ return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
2978
+ }
2639
2979
  };
2640
2980
  function isBusiness(value) {
2641
2981
  return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
2642
2982
  }
2643
- function business(name, options) {
2644
- return new Business(name, options);
2645
- }
2646
- function sortBy(items, key) {
2647
- return [...items].sort((a, b) => {
2648
- const ka = key(a);
2649
- const kb = key(b);
2650
- return ka < kb ? -1 : ka > kb ? 1 : 0;
2651
- });
2983
+ function business(name, options, configure) {
2984
+ const product2 = new Business(name, options);
2985
+ if (configure) product2.use(configure);
2986
+ return product2;
2652
2987
  }
2653
2988
  function isPlainObject(value) {
2654
2989
  return typeof value === "object" && value !== null && !Array.isArray(value);
2655
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
+ }
2656
2998
  function deepMerge(base, patch) {
2657
2999
  const out = { ...base };
2658
3000
  for (const [key, value] of Object.entries(patch)) {
@@ -2665,6 +3007,12 @@ function deepMerge(base, patch) {
2665
3007
  }
2666
3008
  return out;
2667
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
+ }
2668
3016
 
2669
3017
  // src/price.ts
2670
3018
  function toCents(dollars, label) {