@farthershore/product 0.0.0 → 0.1.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.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,6 +1437,8 @@ 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
1443
  * Track B4 — Declarative frontend surface. Product code can declare
1280
1444
  * template-owned nav/pages composed from known portal components. The
@@ -1990,6 +2154,8 @@ var productSpecV2Schema = z20.object({
1990
2154
  frontend: frontendManifestSchema.optional(),
1991
2155
  migrations: migrationDeclsSchema.optional(),
1992
2156
  resources: countedResourcesSchema,
2157
+ policies: productOperatorPoliciesSchema,
2158
+ customer_context: productCustomerContextSchema.optional(),
1993
2159
  billing: z20.object({
1994
2160
  gracePeriodDays: z20.number().int().nonnegative().default(3),
1995
2161
  // When true (default), a plan limit INCREASE re-projects onto active
@@ -2190,7 +2356,7 @@ function canonicalIrJson(ir) {
2190
2356
  }
2191
2357
 
2192
2358
  // src/version.ts
2193
- var SDK_VERSION = true ? "0.0.0" : "0.0.0-dev";
2359
+ var SDK_VERSION = true ? "0.1.0" : "0.0.0-dev";
2194
2360
 
2195
2361
  // src/business.ts
2196
2362
  var BUSINESS_BRAND = Symbol.for("farthershore.product.business");
@@ -2239,13 +2405,7 @@ var Business = class {
2239
2405
  [BUSINESS_BRAND] = true;
2240
2406
  name;
2241
2407
  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 = [];
2408
+ graph = new ManifestResourceGraph();
2249
2409
  frontendManifest;
2250
2410
  productPatch = {};
2251
2411
  /** Sugar for binding API routes to features. */
@@ -2265,16 +2425,21 @@ var Business = class {
2265
2425
  }
2266
2426
  this.name = name;
2267
2427
  this.options = options;
2428
+ this.graph.register("product", name, {
2429
+ name,
2430
+ options
2431
+ });
2268
2432
  this.api = {
2269
2433
  route: (match, options2) => {
2270
2434
  const featureKey = keyOf(options2.feature);
2271
- const file = this.features.get(featureKey);
2435
+ const file = this.getFeatureFile(featureKey);
2272
2436
  if (!file) {
2273
2437
  throw new ManifestBuilderError(
2274
2438
  `api.route("${match}"): feature "${featureKey}" is not declared \u2014 call business.feature("${featureKey}", \u2026) first`
2275
2439
  );
2276
2440
  }
2277
2441
  file.routes.push(this.buildRoute(match, options2));
2442
+ this.syncFeatureGraphNode(file);
2278
2443
  return this;
2279
2444
  }
2280
2445
  };
@@ -2286,6 +2451,7 @@ var Business = class {
2286
2451
  path: item.path,
2287
2452
  ...item.capability !== void 0 ? { capability: keyOf(item.capability) } : {}
2288
2453
  }));
2454
+ this.syncFrontendGraphNode(manifest);
2289
2455
  return this;
2290
2456
  },
2291
2457
  page: (path, options2) => {
@@ -2310,17 +2476,19 @@ var Business = class {
2310
2476
  }))
2311
2477
  } : {}
2312
2478
  });
2479
+ this.syncFrontendGraphNode(manifest);
2313
2480
  return this;
2314
2481
  },
2315
2482
  manifest: (manifest) => {
2316
2483
  this.frontendManifest = manifest;
2484
+ this.syncFrontendGraphNode(manifest);
2317
2485
  return this;
2318
2486
  }
2319
2487
  };
2320
2488
  this.lifecycle = {
2321
2489
  migration: (id, options2) => {
2322
- this.assertUniqueMigrationId(id);
2323
- this.migrations.push({
2490
+ this.assertNewKey("lifecycle_migration", id, "migration");
2491
+ const migration = {
2324
2492
  id,
2325
2493
  from: this.normalizeMigrationPlanRef(options2.from),
2326
2494
  to: this.normalizeMigrationTargetRef(options2.to),
@@ -2337,12 +2505,26 @@ var Business = class {
2337
2505
  ...pin.notes !== void 0 ? { notes: pin.notes } : {}
2338
2506
  }))
2339
2507
  } : {}
2340
- });
2508
+ };
2509
+ this.graph.register(
2510
+ "lifecycle_migration",
2511
+ id,
2512
+ migration,
2513
+ this.migrationDependsOn(migration)
2514
+ );
2341
2515
  return this;
2342
2516
  },
2343
2517
  migrations: (migrations) => {
2344
2518
  this.assertUniqueMigrationIds(migrations);
2345
- this.migrations = migrations;
2519
+ this.graph.clearKind("lifecycle_migration");
2520
+ for (const migration of migrations) {
2521
+ this.graph.register(
2522
+ "lifecycle_migration",
2523
+ migration.id,
2524
+ migration,
2525
+ this.migrationDependsOn(migration)
2526
+ );
2527
+ }
2346
2528
  return this;
2347
2529
  }
2348
2530
  };
@@ -2352,43 +2534,44 @@ var Business = class {
2352
2534
  return this;
2353
2535
  },
2354
2536
  plan: (spec) => {
2355
- this.assertNewKey(this.plans, spec.key, "plan");
2356
- this.plans.set(spec.key, spec);
2537
+ this.assertNewKey("plan", spec.key, "plan");
2538
+ this.graph.register("plan", spec.key, spec, this.planDependsOn(spec));
2357
2539
  return this;
2358
2540
  },
2359
2541
  routesFile: (file) => {
2360
- this.assertNewKey(this.features, file.feature, "feature");
2361
- this.features.set(file.feature, file);
2542
+ this.assertNewKey("feature", file.feature, "feature");
2543
+ this.registerFeatureFile(file);
2362
2544
  return this;
2363
2545
  },
2364
2546
  policyFile: (file) => {
2365
- this.assertNewKey(this.policies, file.name, "policy");
2366
- this.policies.set(file.name, file);
2547
+ this.assertNewKey("policy", file.name, "policy");
2548
+ this.graph.register("policy", file.name, file);
2367
2549
  return this;
2368
2550
  },
2369
2551
  capabilityFile: (file) => {
2370
- this.assertNewKey(this.capabilities, file.capability, "capability");
2371
- this.capabilities.set(file.capability, file);
2552
+ this.assertNewKey("capability", file.capability, "capability");
2553
+ this.graph.register("capability", file.capability, file);
2372
2554
  return this;
2373
2555
  },
2374
2556
  frontend: (manifest) => {
2375
2557
  this.frontendManifest = manifest;
2558
+ this.syncFrontendGraphNode(manifest);
2376
2559
  return this;
2377
2560
  }
2378
2561
  };
2379
2562
  }
2380
2563
  meter(key, options) {
2381
- this.assertNewKey(this.meters, key, "meter");
2382
- this.meters.set(key, { key, ...options });
2564
+ this.assertNewKey("meter", key, "meter");
2565
+ this.graph.register("meter", key, { key, ...options });
2383
2566
  return { kind: "meter", key };
2384
2567
  }
2385
2568
  resource(name, options = {}) {
2386
- this.assertNewKey(this.resources, name, "resource");
2387
- this.resources.set(name, { name, ...options });
2569
+ this.assertNewKey("counted_resource", name, "resource");
2570
+ this.graph.register("counted_resource", name, { name, ...options });
2388
2571
  return { kind: "resource", key: name };
2389
2572
  }
2390
2573
  capability(key, options = {}) {
2391
- this.assertNewKey(this.capabilities, key, "capability");
2574
+ this.assertNewKey("capability", key, "capability");
2392
2575
  const file = {
2393
2576
  capability: key,
2394
2577
  ...options.description !== void 0 || options.title !== void 0 ? { description: options.description ?? options.title } : {},
@@ -2397,7 +2580,12 @@ var Business = class {
2397
2580
  ...options.includesPolicies?.length ? { includes_policies: options.includesPolicies.map(keyOf) } : {},
2398
2581
  ...options.includesCapabilities?.length ? { includes_capabilities: options.includesCapabilities.map(keyOf) } : {}
2399
2582
  };
2400
- this.capabilities.set(key, file);
2583
+ this.graph.register(
2584
+ "capability",
2585
+ key,
2586
+ file,
2587
+ this.capabilityDependsOn(file)
2588
+ );
2401
2589
  return {
2402
2590
  kind: "capability",
2403
2591
  key,
@@ -2409,7 +2597,7 @@ var Business = class {
2409
2597
  };
2410
2598
  }
2411
2599
  feature(key, options = {}) {
2412
- this.assertNewKey(this.features, key, "feature");
2600
+ this.assertNewKey("feature", key, "feature");
2413
2601
  const file = {
2414
2602
  feature: key,
2415
2603
  routes: [],
@@ -2436,30 +2624,34 @@ var Business = class {
2436
2624
  for (const route of options.routes ?? []) {
2437
2625
  file.routes.push(this.buildRoute(route.match, route));
2438
2626
  }
2439
- this.features.set(key, file);
2627
+ this.registerFeatureFile(file);
2440
2628
  const ref = {
2441
2629
  kind: "feature",
2442
2630
  key,
2443
2631
  action: (id, actionOptions) => {
2444
2632
  file.actions ??= [];
2445
- if (file.actions.some((action) => action.id === id)) {
2633
+ if (file.actions.some((action2) => action2.id === id) || this.graph.has("action", id)) {
2446
2634
  throw new ManifestBuilderError(
2447
2635
  `duplicate action "${id}" \u2014 each action id must be declared once`
2448
2636
  );
2449
2637
  }
2450
- file.actions.push({ id, ...actionOptions });
2638
+ const action = { id, ...actionOptions };
2639
+ file.actions.push(action);
2640
+ this.registerAction(key, action);
2641
+ this.syncFeatureGraphNode(file);
2451
2642
  return { kind: "action", key: id };
2452
2643
  },
2453
2644
  route: (match, routeOptions) => {
2454
2645
  file.routes.push(this.buildRoute(match, routeOptions ?? {}));
2646
+ this.syncFeatureGraphNode(file);
2455
2647
  return ref;
2456
2648
  }
2457
2649
  };
2458
2650
  return ref;
2459
2651
  }
2460
2652
  policy(name, options) {
2461
- this.assertNewKey(this.policies, name, "policy");
2462
- this.policies.set(name, {
2653
+ this.assertNewKey("policy", name, "policy");
2654
+ const file = {
2463
2655
  name,
2464
2656
  type: options.type,
2465
2657
  config: options.config,
@@ -2473,11 +2665,12 @@ var Business = class {
2473
2665
  ...options.compatibleWith.authModes ? { auth_modes: options.compatibleWith.authModes } : {}
2474
2666
  }
2475
2667
  } : {}
2476
- });
2668
+ };
2669
+ this.graph.register("policy", name, file, this.policyDependsOn(file));
2477
2670
  return { kind: "policy", key: name };
2478
2671
  }
2479
2672
  plan(key, options) {
2480
- this.assertNewKey(this.plans, key, "plan");
2673
+ this.assertNewKey("plan", key, "plan");
2481
2674
  const capabilityKeys = (options.capabilities ?? []).map(keyOf);
2482
2675
  const capabilityLimits = {
2483
2676
  ...options.capabilityLimits ?? {}
@@ -2526,20 +2719,31 @@ var Business = class {
2526
2719
  if (mergedCaps.length) {
2527
2720
  spec.capabilities = mergedCaps;
2528
2721
  }
2529
- this.plans.set(key, spec);
2722
+ this.graph.register("plan", key, spec, this.planDependsOn(spec));
2530
2723
  return { kind: "plan", key };
2531
2724
  }
2725
+ /** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
2726
+ resourceGraph() {
2727
+ return this.graph.snapshot();
2728
+ }
2532
2729
  /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2533
2730
  * with structured issues when the declared state is invalid. */
2534
2731
  toIR() {
2732
+ this.assertGraphDependenciesSatisfied();
2535
2733
  const candidate = {
2536
2734
  irVersion: 1,
2537
2735
  sdkVersion: SDK_VERSION,
2538
2736
  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()],
2737
+ routes: this.graph.sortedValues(
2738
+ "feature",
2739
+ (file) => file.feature
2740
+ ),
2741
+ policies: this.graph.sortedValues(
2742
+ "policy",
2743
+ (file) => file.name
2744
+ ),
2745
+ capabilities: this.graph.sortedValues(
2746
+ "capability",
2543
2747
  (file) => file.capability
2544
2748
  ),
2545
2749
  runtime: { rollout: null, flags: null, migrations: null }
@@ -2567,19 +2771,34 @@ var Business = class {
2567
2771
  ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2568
2772
  },
2569
2773
  metering: {
2570
- meters: sortBy([...this.meters.values()], (meter) => meter.key),
2774
+ meters: this.graph.sortedValues(
2775
+ "meter",
2776
+ (meter) => meter.key
2777
+ ),
2571
2778
  ...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
2572
2779
  },
2573
2780
  ...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()],
2781
+ ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2782
+ ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2783
+ ...this.graph.has("frontend", "manifest") ? {
2784
+ frontend: this.graph.get(
2785
+ "frontend",
2786
+ "manifest"
2787
+ )?.value
2788
+ } : {},
2789
+ ...this.graph.values("lifecycle_migration").length ? {
2790
+ migrations: this.graph.sortedValues(
2791
+ "lifecycle_migration",
2792
+ (migration) => migration.id
2793
+ )
2794
+ } : {},
2795
+ ...this.graph.values("counted_resource").length ? {
2796
+ resources: this.graph.sortedValues(
2797
+ "counted_resource",
2579
2798
  (resource) => resource.name
2580
2799
  )
2581
2800
  } : {},
2582
- plans: sortBy([...this.plans.values()], (plan) => plan.key)
2801
+ plans: this.graph.sortedValues("plan", (plan) => plan.key)
2583
2802
  };
2584
2803
  return deepMerge(
2585
2804
  base,
@@ -2597,6 +2816,7 @@ var Business = class {
2597
2816
  }
2598
2817
  ensureFrontendManifest() {
2599
2818
  this.frontendManifest ??= { version: 1, nav: [], pages: [] };
2819
+ this.syncFrontendGraphNode(this.frontendManifest);
2600
2820
  return this.frontendManifest;
2601
2821
  }
2602
2822
  normalizeMigrationPlanRef(ref) {
@@ -2611,13 +2831,6 @@ var Business = class {
2611
2831
  version: ref.version ?? "head"
2612
2832
  };
2613
2833
  }
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
2834
  assertUniqueMigrationIds(migrations) {
2622
2835
  const seen = /* @__PURE__ */ new Set();
2623
2836
  for (const migration of migrations) {
@@ -2629,13 +2842,135 @@ var Business = class {
2629
2842
  seen.add(migration.id);
2630
2843
  }
2631
2844
  }
2632
- assertNewKey(map, key, label) {
2633
- if (map.has(key)) {
2845
+ assertNewKey(kind, key, label) {
2846
+ if (this.graph.has(kind, key)) {
2634
2847
  throw new ManifestBuilderError(
2635
2848
  `duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
2636
2849
  );
2637
2850
  }
2638
2851
  }
2852
+ assertGraphDependenciesSatisfied() {
2853
+ const missing = this.graph.missingDependencies();
2854
+ if (missing.length === 0) return;
2855
+ const details = missing.slice(0, 8).map(
2856
+ ({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
2857
+ dependency
2858
+ )}`
2859
+ ).join("; ");
2860
+ const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
2861
+ throw new ManifestBuilderError(
2862
+ `manifest has unresolved resource reference(s): ${details}${suffix}`
2863
+ );
2864
+ }
2865
+ getFeatureFile(key) {
2866
+ return this.graph.get("feature", key)?.value ?? null;
2867
+ }
2868
+ registerFeatureFile(file) {
2869
+ this.graph.register(
2870
+ "feature",
2871
+ file.feature,
2872
+ file,
2873
+ this.featureDependsOn(file)
2874
+ );
2875
+ for (const action of file.actions ?? []) {
2876
+ this.registerAction(file.feature, action);
2877
+ }
2878
+ }
2879
+ syncFeatureGraphNode(file) {
2880
+ this.graph.upsert(
2881
+ "feature",
2882
+ file.feature,
2883
+ file,
2884
+ this.featureDependsOn(file)
2885
+ );
2886
+ }
2887
+ registerAction(featureKey, action) {
2888
+ this.assertNewKey("action", action.id, "action");
2889
+ this.graph.register(
2890
+ "action",
2891
+ action.id,
2892
+ action,
2893
+ this.actionDependsOn(featureKey, action)
2894
+ );
2895
+ }
2896
+ syncFrontendGraphNode(manifest) {
2897
+ this.graph.upsert(
2898
+ "frontend",
2899
+ "manifest",
2900
+ manifest,
2901
+ this.frontendDependsOn(manifest)
2902
+ );
2903
+ }
2904
+ capabilityDependsOn(file) {
2905
+ return [
2906
+ ...this.dependenciesFor("feature", file.includes_features),
2907
+ ...this.dependenciesFor("policy", file.includes_policies),
2908
+ ...this.dependenciesFor("capability", file.includes_capabilities)
2909
+ ];
2910
+ }
2911
+ policyDependsOn(file) {
2912
+ return this.dependenciesFor("meter", file.compatible_with?.meters);
2913
+ }
2914
+ featureDependsOn(file) {
2915
+ const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
2916
+ return [
2917
+ ...this.dependenciesFor("policy", file.policies),
2918
+ ...this.dependenciesFor("capability", file.capabilities),
2919
+ ...this.dependenciesFor("plan", file.plans),
2920
+ ...this.dependenciesFor("meter", meterKeys)
2921
+ ];
2922
+ }
2923
+ actionDependsOn(featureKey, action) {
2924
+ return [
2925
+ resourceDependency("feature", featureKey),
2926
+ ...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
2927
+ ];
2928
+ }
2929
+ planDependsOn(plan) {
2930
+ const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
2931
+ const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
2932
+ const pricedMeterDimensions = (plan.meters ?? []).map(
2933
+ (meter) => meter.dimension
2934
+ );
2935
+ const capacityKeys = Object.keys(plan.capability_limits ?? {});
2936
+ return [
2937
+ ...this.dependenciesFor("capability", caps),
2938
+ ...this.existingDependenciesFor("meter", [
2939
+ ...limitDimensions,
2940
+ ...pricedMeterDimensions
2941
+ ]),
2942
+ ...this.existingDependenciesFor("counted_resource", capacityKeys)
2943
+ ];
2944
+ }
2945
+ migrationDependsOn(migration) {
2946
+ return [
2947
+ resourceDependency("plan", migration.from.plan),
2948
+ resourceDependency("plan", migration.to.plan),
2949
+ ...this.dependenciesFor(
2950
+ "plan",
2951
+ migration.pins?.map((pin) => pin.pinTo.plan)
2952
+ )
2953
+ ];
2954
+ }
2955
+ frontendDependsOn(manifest) {
2956
+ return this.dependenciesFor("capability", [
2957
+ ...(manifest.nav ?? []).flatMap(
2958
+ (item) => item.capability ? [item.capability] : []
2959
+ ),
2960
+ ...(manifest.pages ?? []).flatMap((page) => [
2961
+ ...page.capability ? [page.capability] : [],
2962
+ ...(page.components ?? []).flatMap(
2963
+ (component) => component.capability ? [component.capability] : []
2964
+ )
2965
+ ])
2966
+ ]);
2967
+ }
2968
+ dependenciesFor(kind, keys) {
2969
+ return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
2970
+ }
2971
+ existingDependenciesFor(kind, keys) {
2972
+ return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
2973
+ }
2639
2974
  };
2640
2975
  function isBusiness(value) {
2641
2976
  return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
@@ -2643,16 +2978,16 @@ function isBusiness(value) {
2643
2978
  function business(name, options) {
2644
2979
  return new Business(name, options);
2645
2980
  }
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
- });
2652
- }
2653
2981
  function isPlainObject(value) {
2654
2982
  return typeof value === "object" && value !== null && !Array.isArray(value);
2655
2983
  }
2984
+ function buildCustomerContext(options) {
2985
+ return {
2986
+ ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
2987
+ ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
2988
+ ...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
2989
+ };
2990
+ }
2656
2991
  function deepMerge(base, patch) {
2657
2992
  const out = { ...base };
2658
2993
  for (const [key, value] of Object.entries(patch)) {
@@ -2665,6 +3000,12 @@ function deepMerge(base, patch) {
2665
3000
  }
2666
3001
  return out;
2667
3002
  }
3003
+ function describeResourceUrn(urn) {
3004
+ const parts = urn.split(":");
3005
+ const kind = parts[3] ?? "resource";
3006
+ const key = parts.slice(4).join(":");
3007
+ return `${kind} "${decodeURIComponent(key)}"`;
3008
+ }
2668
3009
 
2669
3010
  // src/price.ts
2670
3011
  function toCents(dollars, label) {