@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/README.md +140 -0
- package/dist/bin.js +404 -63
- package/dist/codegen.js +23 -0
- package/dist/index.js +404 -63
- package/dist/types/business.d.ts +28 -9
- package/dist/types/index.d.ts +2 -1
- package/dist/types/ir-types.d.ts +25 -0
- package/dist/types/resource-graph.d.ts +36 -0
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
2327
|
-
|
|
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.
|
|
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(
|
|
2360
|
-
this.
|
|
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(
|
|
2365
|
-
this.
|
|
2546
|
+
this.assertNewKey("feature", file.feature, "feature");
|
|
2547
|
+
this.registerFeatureFile(file);
|
|
2366
2548
|
return this;
|
|
2367
2549
|
},
|
|
2368
2550
|
policyFile: (file) => {
|
|
2369
|
-
this.assertNewKey(
|
|
2370
|
-
this.
|
|
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(
|
|
2375
|
-
this.
|
|
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(
|
|
2386
|
-
this.
|
|
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(
|
|
2391
|
-
this.
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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(
|
|
2466
|
-
|
|
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(
|
|
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.
|
|
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:
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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:
|
|
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
|
-
...
|
|
2579
|
-
...
|
|
2580
|
-
...this.
|
|
2581
|
-
|
|
2582
|
-
|
|
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:
|
|
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(
|
|
2637
|
-
if (
|
|
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) {
|