@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/bin.js CHANGED
@@ -27,6 +27,97 @@ var ManifestBuilderError = class extends Error {
27
27
  }
28
28
  };
29
29
 
30
+ // src/resource-graph.ts
31
+ var ManifestResourceGraph = class {
32
+ nodes = /* @__PURE__ */ new Map();
33
+ declarationOrder = 0;
34
+ register(kind, key, value, dependsOn = []) {
35
+ const id = nodeId(kind, key);
36
+ if (this.nodes.has(id)) {
37
+ throw new Error(`duplicate resource node ${id}`);
38
+ }
39
+ const node = {
40
+ urn: resourceUrn(kind, key),
41
+ kind,
42
+ key,
43
+ value,
44
+ dependsOn: [...new Set(dependsOn)],
45
+ declarationOrder: this.declarationOrder
46
+ };
47
+ this.declarationOrder += 1;
48
+ this.nodes.set(id, node);
49
+ return node;
50
+ }
51
+ upsert(kind, key, value, dependsOn = []) {
52
+ const id = nodeId(kind, key);
53
+ const existing = this.nodes.get(id);
54
+ if (!existing) {
55
+ return this.register(kind, key, value, dependsOn);
56
+ }
57
+ const node = {
58
+ urn: existing.urn,
59
+ kind,
60
+ key,
61
+ value,
62
+ dependsOn: [...new Set(dependsOn)],
63
+ declarationOrder: existing.declarationOrder
64
+ };
65
+ this.nodes.set(id, node);
66
+ return node;
67
+ }
68
+ clearKind(kind) {
69
+ for (const [id, node] of this.nodes.entries()) {
70
+ if (node.kind === kind) this.nodes.delete(id);
71
+ }
72
+ }
73
+ has(kind, key) {
74
+ return this.nodes.has(nodeId(kind, key));
75
+ }
76
+ get(kind, key) {
77
+ return this.nodes.get(nodeId(kind, key)) ?? null;
78
+ }
79
+ values(kind) {
80
+ return [...this.nodes.values()].filter((node) => node.kind === kind).map((node) => node.value);
81
+ }
82
+ sortedValues(kind, key) {
83
+ return sortBy(this.values(kind), key);
84
+ }
85
+ snapshot() {
86
+ return {
87
+ nodes: sortBy([...this.nodes.values()], (node) => node.urn).map(
88
+ ({ value: _value, ...node }) => node
89
+ )
90
+ };
91
+ }
92
+ missingDependencies() {
93
+ const known = new Set([...this.nodes.values()].map((node) => node.urn));
94
+ return [...this.nodes.values()].flatMap(
95
+ (node) => node.dependsOn.filter((dependency) => !known.has(dependency)).map((dependency) => ({
96
+ from: node.urn,
97
+ missing: dependency
98
+ }))
99
+ );
100
+ }
101
+ };
102
+ function resourceUrn(kind, key) {
103
+ return `urn:farthershore:product:${kind}:${encodeURIComponent(
104
+ key
105
+ )}`;
106
+ }
107
+ function resourceDependency(kind, key) {
108
+ return resourceUrn(kind, key);
109
+ }
110
+ function nodeId(kind, key) {
111
+ return `${kind}:${key}`;
112
+ }
113
+ function sortBy(items, key) {
114
+ return [...items].sort((a, b) => {
115
+ const ka = key(a);
116
+ const kb = key(b);
117
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
118
+ });
119
+ }
120
+
30
121
  // ../contracts/dist/plans/limits-schema.js
31
122
  import { z } from "zod";
32
123
  var limitDimensionSchema = z.string().min(1);
@@ -1251,6 +1342,77 @@ var featureCatalogEntrySchema = z13.object({
1251
1342
  plans: z13.array(z13.string().min(1)).min(1).max(20)
1252
1343
  });
1253
1344
  var featureCatalogSchema = z13.record(z13.string().min(1).max(100).regex(/^[a-z0-9_.:-]+$/), featureCatalogEntrySchema);
1345
+ var productCleanupPolicyModeSchema = z13.enum([
1346
+ "report",
1347
+ "pull_request"
1348
+ ]);
1349
+ var productChangeApprovalRiskSchema = z13.enum([
1350
+ "safe",
1351
+ "non_blocking",
1352
+ "economic_risk",
1353
+ "blocking"
1354
+ ]);
1355
+ var productOperatorPoliciesSchema = z13.object({
1356
+ /**
1357
+ * Route cleanup operator. Disabled by default; report-mode is the safe
1358
+ * default so a product can surface zero-traffic runtime-route candidates
1359
+ * before it opts into draft PR mutation.
1360
+ */
1361
+ cleanup: z13.object({
1362
+ enabled: z13.boolean().default(false),
1363
+ mode: productCleanupPolicyModeSchema.default("report")
1364
+ }).default({ enabled: false, mode: "report" }),
1365
+ /**
1366
+ * PR approval thresholds for manifest impact reports. This schema makes
1367
+ * the policy a first-class product-as-code field even while enforcement is
1368
+ * still report/label-only.
1369
+ */
1370
+ change_approval: z13.object({
1371
+ auto_merge_max_risk: z13.enum(["none", "safe", "non_blocking"]).default("none"),
1372
+ require_human_for: z13.array(productChangeApprovalRiskSchema).default(["economic_risk", "blocking"])
1373
+ }).default({
1374
+ auto_merge_max_risk: "none",
1375
+ require_human_for: ["economic_risk", "blocking"]
1376
+ })
1377
+ }).default({
1378
+ cleanup: { enabled: false, mode: "report" },
1379
+ change_approval: {
1380
+ auto_merge_max_risk: "none",
1381
+ require_human_for: ["economic_risk", "blocking"]
1382
+ }
1383
+ });
1384
+ var customerIdentityRequirementSchema = z13.enum([
1385
+ "org_only",
1386
+ "org_and_user"
1387
+ ]);
1388
+ var customerPortalAuthStrategySchema = z13.enum([
1389
+ "clerk",
1390
+ "test-personas"
1391
+ ]);
1392
+ var productCustomerContextSchema = z13.object({
1393
+ /**
1394
+ * Edge credential identity policy. This is intentionally Product-scoped:
1395
+ * B7 keeps Product as the business boundary and avoids customer-side
1396
+ * Workspace/subject models until per-subject entitlements need them.
1397
+ */
1398
+ identity_requirement: customerIdentityRequirementSchema.optional(),
1399
+ /**
1400
+ * Enables signed customer-context tokens (`fsc_*`) without putting the
1401
+ * runtime signing secret in product.config.ts. Core generates/preserves the
1402
+ * secret when this is true and clears it when explicitly false.
1403
+ */
1404
+ context_tokens: z13.object({
1405
+ enabled: z13.boolean().default(true)
1406
+ }).optional(),
1407
+ /**
1408
+ * Portal auth strategy for environment-scoped product applies. Production
1409
+ * portal auth is provisioner-owned; preview/test environments can opt into
1410
+ * test personas through Product-as-Code.
1411
+ */
1412
+ portal_auth: z13.object({
1413
+ strategy: customerPortalAuthStrategySchema
1414
+ }).optional()
1415
+ });
1254
1416
  var productSpecSchema = z13.object({
1255
1417
  product: z13.object({
1256
1418
  name: z13.string().min(1).max(100),
@@ -1282,6 +1444,8 @@ var productSpecSchema = z13.object({
1282
1444
  }).optional(),
1283
1445
  features: featureCatalogSchema.optional(),
1284
1446
  resources: countedResourcesSchema,
1447
+ policies: productOperatorPoliciesSchema,
1448
+ customer_context: productCustomerContextSchema.optional(),
1285
1449
  /**
1286
1450
  * Track B4 — Declarative frontend surface. Product code can declare
1287
1451
  * template-owned nav/pages composed from known portal components. The
@@ -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.1.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,31 @@ 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
+ /** Inspect the SDK-local declaration graph used to emit the Manifest IR. */
2730
+ resourceGraph() {
2731
+ return this.graph.snapshot();
2732
+ }
2536
2733
  /** Assemble + validate the Manifest IR. Throws ManifestValidationError
2537
2734
  * with structured issues when the declared state is invalid. */
2538
2735
  toIR() {
2736
+ this.assertGraphDependenciesSatisfied();
2539
2737
  const candidate = {
2540
2738
  irVersion: 1,
2541
2739
  sdkVersion: SDK_VERSION,
2542
2740
  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()],
2741
+ routes: this.graph.sortedValues(
2742
+ "feature",
2743
+ (file) => file.feature
2744
+ ),
2745
+ policies: this.graph.sortedValues(
2746
+ "policy",
2747
+ (file) => file.name
2748
+ ),
2749
+ capabilities: this.graph.sortedValues(
2750
+ "capability",
2547
2751
  (file) => file.capability
2548
2752
  ),
2549
2753
  runtime: { rollout: null, flags: null, migrations: null }
@@ -2571,19 +2775,34 @@ var Business = class {
2571
2775
  ...options.upstreamAuth !== void 0 ? { upstreamAuth: options.upstreamAuth } : {}
2572
2776
  },
2573
2777
  metering: {
2574
- meters: sortBy([...this.meters.values()], (meter) => meter.key),
2778
+ meters: this.graph.sortedValues(
2779
+ "meter",
2780
+ (meter) => meter.key
2781
+ ),
2575
2782
  ...options.billOn4xx !== void 0 ? { billOn4xx: options.billOn4xx } : {}
2576
2783
  },
2577
2784
  ...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()],
2785
+ ...options.operatorPolicies !== void 0 ? { policies: options.operatorPolicies } : {},
2786
+ ...options.customerContext !== void 0 ? { customer_context: buildCustomerContext(options.customerContext) } : {},
2787
+ ...this.graph.has("frontend", "manifest") ? {
2788
+ frontend: this.graph.get(
2789
+ "frontend",
2790
+ "manifest"
2791
+ )?.value
2792
+ } : {},
2793
+ ...this.graph.values("lifecycle_migration").length ? {
2794
+ migrations: this.graph.sortedValues(
2795
+ "lifecycle_migration",
2796
+ (migration) => migration.id
2797
+ )
2798
+ } : {},
2799
+ ...this.graph.values("counted_resource").length ? {
2800
+ resources: this.graph.sortedValues(
2801
+ "counted_resource",
2583
2802
  (resource) => resource.name
2584
2803
  )
2585
2804
  } : {},
2586
- plans: sortBy([...this.plans.values()], (plan) => plan.key)
2805
+ plans: this.graph.sortedValues("plan", (plan) => plan.key)
2587
2806
  };
2588
2807
  return deepMerge(
2589
2808
  base,
@@ -2601,6 +2820,7 @@ var Business = class {
2601
2820
  }
2602
2821
  ensureFrontendManifest() {
2603
2822
  this.frontendManifest ??= { version: 1, nav: [], pages: [] };
2823
+ this.syncFrontendGraphNode(this.frontendManifest);
2604
2824
  return this.frontendManifest;
2605
2825
  }
2606
2826
  normalizeMigrationPlanRef(ref) {
@@ -2615,13 +2835,6 @@ var Business = class {
2615
2835
  version: ref.version ?? "head"
2616
2836
  };
2617
2837
  }
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
2838
  assertUniqueMigrationIds(migrations) {
2626
2839
  const seen = /* @__PURE__ */ new Set();
2627
2840
  for (const migration of migrations) {
@@ -2633,27 +2846,149 @@ var Business = class {
2633
2846
  seen.add(migration.id);
2634
2847
  }
2635
2848
  }
2636
- assertNewKey(map, key, label) {
2637
- if (map.has(key)) {
2849
+ assertNewKey(kind, key, label) {
2850
+ if (this.graph.has(kind, key)) {
2638
2851
  throw new ManifestBuilderError(
2639
2852
  `duplicate ${label} "${key}" \u2014 each ${label} key must be declared once`
2640
2853
  );
2641
2854
  }
2642
2855
  }
2856
+ assertGraphDependenciesSatisfied() {
2857
+ const missing = this.graph.missingDependencies();
2858
+ if (missing.length === 0) return;
2859
+ const details = missing.slice(0, 8).map(
2860
+ ({ from, missing: dependency }) => `${describeResourceUrn(from)} depends on missing ${describeResourceUrn(
2861
+ dependency
2862
+ )}`
2863
+ ).join("; ");
2864
+ const suffix = missing.length > 8 ? `; plus ${missing.length - 8} more` : "";
2865
+ throw new ManifestBuilderError(
2866
+ `manifest has unresolved resource reference(s): ${details}${suffix}`
2867
+ );
2868
+ }
2869
+ getFeatureFile(key) {
2870
+ return this.graph.get("feature", key)?.value ?? null;
2871
+ }
2872
+ registerFeatureFile(file) {
2873
+ this.graph.register(
2874
+ "feature",
2875
+ file.feature,
2876
+ file,
2877
+ this.featureDependsOn(file)
2878
+ );
2879
+ for (const action of file.actions ?? []) {
2880
+ this.registerAction(file.feature, action);
2881
+ }
2882
+ }
2883
+ syncFeatureGraphNode(file) {
2884
+ this.graph.upsert(
2885
+ "feature",
2886
+ file.feature,
2887
+ file,
2888
+ this.featureDependsOn(file)
2889
+ );
2890
+ }
2891
+ registerAction(featureKey, action) {
2892
+ this.assertNewKey("action", action.id, "action");
2893
+ this.graph.register(
2894
+ "action",
2895
+ action.id,
2896
+ action,
2897
+ this.actionDependsOn(featureKey, action)
2898
+ );
2899
+ }
2900
+ syncFrontendGraphNode(manifest) {
2901
+ this.graph.upsert(
2902
+ "frontend",
2903
+ "manifest",
2904
+ manifest,
2905
+ this.frontendDependsOn(manifest)
2906
+ );
2907
+ }
2908
+ capabilityDependsOn(file) {
2909
+ return [
2910
+ ...this.dependenciesFor("feature", file.includes_features),
2911
+ ...this.dependenciesFor("policy", file.includes_policies),
2912
+ ...this.dependenciesFor("capability", file.includes_capabilities)
2913
+ ];
2914
+ }
2915
+ policyDependsOn(file) {
2916
+ return this.dependenciesFor("meter", file.compatible_with?.meters);
2917
+ }
2918
+ featureDependsOn(file) {
2919
+ const meterKeys = file.routes.flatMap((route) => route.meters ?? []);
2920
+ return [
2921
+ ...this.dependenciesFor("policy", file.policies),
2922
+ ...this.dependenciesFor("capability", file.capabilities),
2923
+ ...this.dependenciesFor("plan", file.plans),
2924
+ ...this.dependenciesFor("meter", meterKeys)
2925
+ ];
2926
+ }
2927
+ actionDependsOn(featureKey, action) {
2928
+ return [
2929
+ resourceDependency("feature", featureKey),
2930
+ ...action.resource ? [resourceDependency("counted_resource", action.resource.resource)] : []
2931
+ ];
2932
+ }
2933
+ planDependsOn(plan) {
2934
+ const caps = Array.isArray(plan.capabilities) ? plan.capabilities.map(String) : [];
2935
+ const limitDimensions = (plan.limits ?? []).map((limit) => limit.dimension);
2936
+ const pricedMeterDimensions = (plan.meters ?? []).map(
2937
+ (meter) => meter.dimension
2938
+ );
2939
+ const capacityKeys = Object.keys(plan.capability_limits ?? {});
2940
+ return [
2941
+ ...this.dependenciesFor("capability", caps),
2942
+ ...this.existingDependenciesFor("meter", [
2943
+ ...limitDimensions,
2944
+ ...pricedMeterDimensions
2945
+ ]),
2946
+ ...this.existingDependenciesFor("counted_resource", capacityKeys)
2947
+ ];
2948
+ }
2949
+ migrationDependsOn(migration) {
2950
+ return [
2951
+ resourceDependency("plan", migration.from.plan),
2952
+ resourceDependency("plan", migration.to.plan),
2953
+ ...this.dependenciesFor(
2954
+ "plan",
2955
+ migration.pins?.map((pin) => pin.pinTo.plan)
2956
+ )
2957
+ ];
2958
+ }
2959
+ frontendDependsOn(manifest) {
2960
+ return this.dependenciesFor("capability", [
2961
+ ...(manifest.nav ?? []).flatMap(
2962
+ (item) => item.capability ? [item.capability] : []
2963
+ ),
2964
+ ...(manifest.pages ?? []).flatMap((page) => [
2965
+ ...page.capability ? [page.capability] : [],
2966
+ ...(page.components ?? []).flatMap(
2967
+ (component) => component.capability ? [component.capability] : []
2968
+ )
2969
+ ])
2970
+ ]);
2971
+ }
2972
+ dependenciesFor(kind, keys) {
2973
+ return [...new Set(keys ?? [])].map((key) => resourceDependency(kind, key));
2974
+ }
2975
+ existingDependenciesFor(kind, keys) {
2976
+ return [...new Set(keys)].filter((key) => this.graph.has(kind, key)).map((key) => resourceDependency(kind, key));
2977
+ }
2643
2978
  };
2644
2979
  function isBusiness(value) {
2645
2980
  return typeof value === "object" && value !== null && value[BUSINESS_BRAND] === true;
2646
2981
  }
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
2982
  function isPlainObject(value) {
2655
2983
  return typeof value === "object" && value !== null && !Array.isArray(value);
2656
2984
  }
2985
+ function buildCustomerContext(options) {
2986
+ return {
2987
+ ...options.identityRequirement !== void 0 ? { identity_requirement: options.identityRequirement } : {},
2988
+ ...options.contextTokens !== void 0 ? { context_tokens: options.contextTokens } : {},
2989
+ ...options.portalAuth !== void 0 ? { portal_auth: options.portalAuth } : {}
2990
+ };
2991
+ }
2657
2992
  function deepMerge(base, patch) {
2658
2993
  const out = { ...base };
2659
2994
  for (const [key, value] of Object.entries(patch)) {
@@ -2666,6 +3001,12 @@ function deepMerge(base, patch) {
2666
3001
  }
2667
3002
  return out;
2668
3003
  }
3004
+ function describeResourceUrn(urn) {
3005
+ const parts = urn.split(":");
3006
+ const kind = parts[3] ?? "resource";
3007
+ const key = parts.slice(4).join(":");
3008
+ return `${kind} "${decodeURIComponent(key)}"`;
3009
+ }
2669
3010
 
2670
3011
  // src/bin.ts
2671
3012
  function parseArgs(argv) {