@unified-product-graph/core 0.33.0 → 0.34.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.cjs CHANGED
@@ -40,10 +40,12 @@ __export(index_exports, {
40
40
  CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA: () => CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA,
41
41
  CONTENT_DEPTH_WARNING_RULES: () => CONTENT_DEPTH_WARNING_RULES,
42
42
  COVERAGE_ANTI_PATTERNS: () => COVERAGE_ANTI_PATTERNS,
43
+ CROSS_EDGE_KEY_ORDER: () => CROSS_EDGE_KEY_ORDER,
43
44
  DEFAULT_ENTITY_EMOJI: () => DEFAULT_ENTITY_EMOJI,
44
45
  DEFERRED_PROPOSED_BY_CONTRACT: () => DEFERRED_PROPOSED_BY_CONTRACT,
45
46
  DEFER_EDGE_PROPERTY_SCHEMA: () => DEFER_EDGE_PROPERTY_SCHEMA,
46
47
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK: () => DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
48
+ EDGE_KEY_ORDER: () => EDGE_KEY_ORDER,
47
49
  ENGINEERING_PLATFORM_PLAYBOOK: () => ENGINEERING_PLATFORM_PLAYBOOK,
48
50
  ENTITY_EMOJI: () => ENTITY_EMOJI,
49
51
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK: () => EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
@@ -111,7 +113,9 @@ __export(index_exports, {
111
113
  UPG_LIFECYCLE_PLANNED_TYPES: () => UPG_LIFECYCLE_PLANNED_TYPES,
112
114
  UPG_META_COUNT: () => UPG_META_COUNT,
113
115
  UPG_MIGRATIONS: () => UPG_MIGRATIONS,
116
+ UPG_PHASE_COUNT: () => UPG_PHASE_COUNT,
114
117
  UPG_PLAYBOOKS: () => UPG_PLAYBOOKS,
118
+ UPG_POLYMORPHIC_EDGE_FAMILIES: () => UPG_POLYMORPHIC_EDGE_FAMILIES,
115
119
  UPG_POLYMORPHIC_EDGE_KEYS: () => UPG_POLYMORPHIC_EDGE_KEYS,
116
120
  UPG_PORTFOLIO_KINDS: () => UPG_PORTFOLIO_KINDS,
117
121
  UPG_PORTFOLIO_SHARED_TYPES: () => UPG_PORTFOLIO_SHARED_TYPES,
@@ -2685,6 +2689,51 @@ var UPG_EDGE_CATALOG = {
2685
2689
  // evidence co-reside), so NOT cross_product_eligible.
2686
2690
  document_contains_quote: { forward_verb: "contains", reverse_verb: "contained_in", classification: "semantic", source_type: "document", target_type: "quote" },
2687
2691
  document_contains_observation: { forward_verb: "contains", reverse_verb: "contained_in", classification: "semantic", source_type: "document", target_type: "observation" },
2692
+ /**
2693
+ * A document EMBEDS this node's live value at a position in its prose.
2694
+ *
2695
+ * @remarks
2696
+ * NOT `describes`. `document_describes_persona` says the document is ABOUT the
2697
+ * persona. Transclusion says the document RENDERS that node where the prose
2698
+ * sits, so the value a reader sees is the node's current value rather than a
2699
+ * copy taken when the sentence was written. A PRD that transcludes a metric is
2700
+ * not "about" the metric, and collapsing the two would lose the only property
2701
+ * transclusion is bought for: current by construction. The nine-member
2702
+ * `document_describes_*` family is untouched and keeps its meaning.
2703
+ *
2704
+ * POLYMORPHIC because the target set is genuinely open. Anything renderable can
2705
+ * be embedded, and the nine enumerated describe-targets are none of `metric`,
2706
+ * `research_study`, `specification` or `architecture_decision`, which is what
2707
+ * 65% of measured documents carrying zero outbound edges were reaching for.
2708
+ *
2709
+ * THE ANCHOR BINDING (normative). `@unified-product-graph/markdown` already
2710
+ * parses an inline reference form and records the source line of every
2711
+ * occurrence: `[[type:id]]`, `[[type:id|label]]`, `[[type:id|k:v|...]]`,
2712
+ * `[[+type:id]]` for creation, and `[[type:id@product]]` across products. The
2713
+ * grammar is published as Appendix F of the specification paper. The rule that
2714
+ * was missing, and is stated here: an anchor appearing in the body of a
2715
+ * `document` IS a transclusion anchor, and a conformant parser WRITES this edge
2716
+ * beside it. Anchor and edge are one fact recorded twice, so they must be
2717
+ * written together or they drift.
2718
+ *
2719
+ * NO POSITION PROPERTY, deliberately. The obvious `anchor_line` is the most
2720
+ * volatile value a text document has: every insertion above moves every anchor
2721
+ * below, so a stored line number is wrong after the next paragraph and nothing
2722
+ * reports the drift. The anchor IS the position, and it lives in the prose
2723
+ * where ordinary editing moves it for free. What the law asks is that the edge
2724
+ * be written BESIDE the anchor, which is a write-time discipline rather than a
2725
+ * stored field.
2726
+ *
2727
+ * `deliberate_only`, and the 0.33.0 trap is why that is safe here. Flagging a
2728
+ * widened edge `deliberate_only` silently switched five adapters off in the
2729
+ * last release, two of them under a green suite. The rule that came out of it:
2730
+ * an adapter reading an explicit field the SOURCE stores carries an authored
2731
+ * fact rather than an inferred one and must emit deliberate-only edges
2732
+ * EXPLICITLY. A `[[type:id]]` anchor is as explicit as a source gets, because a
2733
+ * person typed it, so the markdown emitter keys on the ANCHOR and never on the
2734
+ * `node` wildcard. No generic pair-resolution path may ever produce one.
2735
+ */
2736
+ document_transcludes_node: { forward_verb: "transcludes", reverse_verb: "transcluded_in", classification: "semantic", source_type: "document", target_type: "node", deliberate_only: true },
2688
2737
  // 5.3 Customer Education & Training Domain
2689
2738
  product_educates_via_education_program: { forward_verb: "educates_via", reverse_verb: "educates", classification: "hierarchy", source_type: "product", target_type: "education_program" },
2690
2739
  education_program_teaches_via_tutorial: { forward_verb: "teaches_via", reverse_verb: "teaches", classification: "hierarchy", source_type: "education_program", target_type: "tutorial" },
@@ -2750,10 +2799,10 @@ var UPG_EDGE_CATALOG = {
2750
2799
  // and G2b is not engaged.
2751
2800
  // Membership is UNQUALIFIED: no role property on the edge. `role` is already an
2752
2801
  // entity type with team_staffed_with_role, so a role string here would be a
2753
- // second role model beside a node type. The consequence is stated rather than
2754
- // hidden: the graph can say a team has a slot and that a person is on the team,
2755
- // and not that the person fills the slot. `person_holds_role` is banked, with
2756
- // its condition the first field graph carrying both where the join is asked for.
2802
+ // second role model beside a node type. The consequence that used to follow is
2803
+ // now closed rather than accepted: the graph can say a team has a slot, that a
2804
+ // person is on the team, AND that the person fills the slot, the last through
2805
+ // `person_holds_role` (0.34.0) rather than through a qualifier here.
2757
2806
  //
2758
2807
  // Neither is cross_product_eligible: `person` is not portfolio_shared, so the
2759
2808
  // 0.18.0 both-endpoints gate rejects the flag regardless of any team-scoping
@@ -2774,6 +2823,41 @@ var UPG_EDGE_CATALOG = {
2774
2823
  // the person / team / department triangle is decided once rather than three
2775
2824
  // times.
2776
2825
  person_reports_to_person: { forward_verb: "reports_to", reverse_verb: "has_report", classification: "semantic", source_type: "person", target_type: "person" },
2826
+ // MINTED 0.34.0 on a grammar-versus-catalog inconsistency readable entirely
2827
+ // inside this package, NOT on the field condition the 0.33.0 bank named. That
2828
+ // condition is still unmet and saying so is the honest framing: both org edges
2829
+ // exist in exactly one graph and it is the saturation fixture, whose person set
2830
+ // contains teams mistyped as people. What replaced it is stronger on its own
2831
+ // terms because it is checkable and adoption-independent:
2832
+ //
2833
+ // `role`'s lifecycle terminates at `filled` and `vacant`, and `filled` is
2834
+ // described "Role is staffed." `team_staffed_with_role` runs team -> slot.
2835
+ // `person_member_of_team` runs person -> team. NOTHING connected a person to
2836
+ // a role. So a graph could legally mark a role `filled` and no edge in the
2837
+ // catalog could say by whom.
2838
+ //
2839
+ // A terminal lifecycle phase that no edge can substantiate is dead schema, and
2840
+ // it is worse than a phantom edge name: a phantom is inert, while `filled` is a
2841
+ // value graphs will actually carry. Same class as the anti-pattern entry that
2842
+ // instructed users toward an edge the catalog refused, which is what minted
2843
+ // `person_reports_to_person` one release ago.
2844
+ //
2845
+ // `semantic`, not `cross-domain`. `person`, `role` and `team` are one domain
2846
+ // (domains.ts, team_org), and T1.7 forbids new same-domain cross-domain edges.
2847
+ // This is the third member of that triangle and it is decided on exactly the
2848
+ // ground the other two were decided on mid-build at 0.33.0.
2849
+ //
2850
+ // Direction is person -> role, matching both 0.33.0 person edges, so the
2851
+ // containment reading stays UNAVAILABLE rather than merely unchosen and
2852
+ // UPG_VALID_CHILDREN is untouched. `role_filled_by_person` would source the
2853
+ // edge on the slot, which reads as the slot owning the person.
2854
+ //
2855
+ // Unqualified, for the reason the membership edge above gives: `role` is a type,
2856
+ // not a string, so a qualifier here would be a second role model.
2857
+ //
2858
+ // Not cross_product_eligible: `person` is not portfolio_shared, so the 0.18.0
2859
+ // both-endpoints gate rejects the flag regardless.
2860
+ person_holds_role: { forward_verb: "holds", reverse_verb: "held_by", classification: "semantic", source_type: "person", target_type: "role" },
2777
2861
  team_staffed_with_role: { forward_verb: "staffed_with", reverse_verb: "staffed_in", classification: "hierarchy", source_type: "team", target_type: "role" },
2778
2862
  team_targets_team_okr: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "team", target_type: "team_okr" },
2779
2863
  team_reflects_in_retrospective: { forward_verb: "reflects_in", reverse_verb: "reflects", classification: "hierarchy", source_type: "team", target_type: "retrospective" },
@@ -4144,8 +4228,51 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
4144
4228
  // Project membership (0.33.0, widened from project_delivers_epic):
4145
4229
  // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
4146
4230
  // bug}. Same construction and same reasoning as the cadence edge above.
4147
- "project_delivers_work_item"
4231
+ "project_delivers_work_item",
4232
+ // Document transclusion (0.34.0): a document embeds any renderable node's live
4233
+ // value at a position in its prose. Open by nature rather than by an
4234
+ // enumeration nobody wanted to write out.
4235
+ "document_transcludes_node"
4148
4236
  ];
4237
+ var UPG_POLYMORPHIC_EDGE_FAMILIES = {
4238
+ "universal semantic verbs": ["node_informs_node", "node_constrains_node", "node_inspires_node"],
4239
+ "decision-to-anything": ["decision_influences_node", "decision_constrained_by_node", "decision_produces_node"],
4240
+ "universal ownership": [
4241
+ "node_owned_by_team",
4242
+ "node_owned_by_role",
4243
+ "node_owned_by_stakeholder",
4244
+ "node_owned_by_department",
4245
+ "node_owned_by_person"
4246
+ ],
4247
+ "universal architecture references": ["node_belongs_to_bounded_context"],
4248
+ "framework exercises": ["framework_exercise_includes_node"],
4249
+ "universal classification": ["node_classified_as_classification_value"],
4250
+ "work-item issue links": [
4251
+ "work_item_blocks_work_item",
4252
+ "work_item_relates_to_work_item",
4253
+ "work_item_duplicates_work_item"
4254
+ ],
4255
+ "workspace provenance": ["workspace_produced_node"],
4256
+ "canvas arrangement and published-view focus": ["workspace_arranges_node", "composition_focuses_node"],
4257
+ "benchmark subject": ["eval_benchmark_measures_node"],
4258
+ // 0.32.0. Assignment is its OWN family and deliberately not a member of
4259
+ // 'universal ownership': keeping them apart in this partition is the same
4260
+ // ruling the edge itself makes, so a future author who merges them here is
4261
+ // making that decision visibly rather than by tidying a list.
4262
+ "universal assignment": ["node_assigned_to_person"],
4263
+ "capture subject": ["capture_renders_node"],
4264
+ "cadence scheduling": ["planning_cycle_schedules_work_item"],
4265
+ // 0.33.0. Project membership is its own family and deliberately not a member
4266
+ // of 'cadence scheduling', even though both range over the same work-item set:
4267
+ // a cycle SCHEDULES work in time and a project REFERENCES the work it delivers,
4268
+ // and merging them here would quietly assert those are one relation.
4269
+ "project membership": ["project_delivers_work_item"],
4270
+ // 0.34.0. Its own family and deliberately not merged with the nine-member
4271
+ // `document_describes_*` set, which is not polymorphic at all: those are
4272
+ // enumerated edges saying a document is ABOUT a thing. Transclusion says it
4273
+ // RENDERS the thing, and the difference is the whole reason the edge exists.
4274
+ "document transclusion": ["document_transcludes_node"]
4275
+ };
4149
4276
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
4150
4277
  function isPolymorphicEdge(key) {
4151
4278
  const def = UPG_EDGE_CATALOG[key];
@@ -4163,1273 +4290,132 @@ function getEdgePropertySchema(type) {
4163
4290
  return def?.property_schema;
4164
4291
  }
4165
4292
 
4166
- // src/catalog/legacy-product-stages.ts
4167
- var LEGACY_PRODUCT_STAGES = Object.freeze({
4168
- idea: "concept"
4169
- });
4170
- function migrateProductStage(stage) {
4171
- if (stage === void 0 || stage === null) {
4172
- return void 0;
4173
- }
4174
- if (typeof stage !== "string") {
4175
- return stage;
4176
- }
4177
- const lower = stage.toLowerCase();
4178
- const mapped = LEGACY_PRODUCT_STAGES[lower];
4179
- return mapped ?? stage;
4180
- }
4181
- function isLegacyProductStage(stage) {
4182
- return typeof stage === "string" && stage.toLowerCase() in LEGACY_PRODUCT_STAGES;
4183
- }
4184
-
4185
- // src/shapes/base-node.ts
4186
- var BASE_NODE_FIELD_PRESENCE = {
4187
- id: true,
4188
- type: true,
4189
- title: true,
4190
- slug: true,
4191
- aliases: true,
4192
- key: true,
4193
- description: true,
4194
- tags: true,
4195
- status: true,
4196
- archived: true,
4197
- archived_at: true,
4198
- source_id: true,
4199
- source_type: true,
4200
- mapping_confidence: true,
4201
- external_tool: true,
4202
- external_ref: true,
4203
- external_id: true,
4204
- external_links: true,
4205
- created_at: true,
4206
- updated_at: true,
4207
- properties: true
4293
+ // src/grammar/lifecycles.ts
4294
+ var PRODUCT_LIFECYCLE = {
4295
+ entity_type: "product",
4296
+ initial_phase: "concept",
4297
+ terminal_phases: ["sunset"],
4298
+ phases: [
4299
+ {
4300
+ id: "concept",
4301
+ status_category: "triage",
4302
+ label: "Concept",
4303
+ description: "Napkin idea. Problem shape and solution sketch, pre-validation.",
4304
+ transitions_to: ["validation", "build", "beta", "launch", "growth", "mature", "sunset"]
4305
+ },
4306
+ {
4307
+ id: "validation",
4308
+ status_category: "started",
4309
+ label: "Validation",
4310
+ description: "Testing demand. User conversations and experiments pressure-test assumptions before build commits.",
4311
+ transitions_to: ["build", "beta", "launch", "growth", "mature", "sunset"]
4312
+ },
4313
+ {
4314
+ id: "build",
4315
+ status_category: "started",
4316
+ label: "Build",
4317
+ description: "Actively developing v1. Core functionality is being authored, pre-user.",
4318
+ transitions_to: ["beta", "launch", "growth", "mature", "sunset"]
4319
+ },
4320
+ {
4321
+ id: "beta",
4322
+ status_category: "started",
4323
+ label: "Beta",
4324
+ description: "Early users, iterating. Feature-complete enough to learn from, still changing weekly.",
4325
+ transitions_to: ["launch", "growth", "mature", "maintenance", "sunset"]
4326
+ },
4327
+ {
4328
+ id: "launch",
4329
+ status_category: "started",
4330
+ label: "Launch",
4331
+ description: "Generally available. Announced to the target audience, open past beta access.",
4332
+ transitions_to: ["growth", "mature", "maintenance", "sunset"]
4333
+ },
4334
+ {
4335
+ id: "growth",
4336
+ status_category: "started",
4337
+ label: "Growth",
4338
+ description: "Scaling users and revenue. Product fit validated; focus is acquisition, retention, expansion.",
4339
+ transitions_to: ["mature", "maintenance", "sunset"]
4340
+ },
4341
+ {
4342
+ id: "mature",
4343
+ status_category: "started",
4344
+ label: "Mature",
4345
+ description: "Stable. Growth levelled; investment weights toward optimisation, efficiency, and durability.",
4346
+ transitions_to: ["maintenance", "sunset"]
4347
+ },
4348
+ {
4349
+ id: "maintenance",
4350
+ status_category: "started",
4351
+ label: "Maintenance",
4352
+ description: "Sustaining. Minimal investment keeps the product running for existing users. Can transition back to `mature` on a strategic revival.",
4353
+ transitions_to: ["mature", "sunset"]
4354
+ },
4355
+ {
4356
+ id: "sunset",
4357
+ status_category: "completed",
4358
+ label: "Sunset",
4359
+ description: "Winding down or retired. Existing users are being migrated or offboarded.",
4360
+ transitions_to: []
4361
+ }
4362
+ ]
4208
4363
  };
4209
- var UPG_BASE_NODE_FIELDS = Object.freeze(
4210
- Object.keys(BASE_NODE_FIELD_PRESENCE)
4211
- );
4212
- var UPG_BASE_NODE_FIELD_SET = new Set(UPG_BASE_NODE_FIELDS);
4213
- var UPG_BASE_NODE_SPECIAL_MERGE_FIELDS = /* @__PURE__ */ new Set([
4214
- "id",
4215
- "type",
4216
- "slug",
4217
- "aliases",
4218
- "properties"
4219
- ]);
4220
-
4221
- // src/shapes/document.ts
4222
- var UPG_CROSS_ONLY_EDGE_TYPES = [
4223
- // Peer overlap: "these two products share / overlap on this thing." Symmetric,
4224
- // no canonical instance_of identity. shares_job / shares_need are the job/need
4225
- // siblings of the persona/competitor/metric peers (connective layer, 0.13.1).
4226
- "shares_persona",
4227
- "shares_competitor",
4228
- "shares_metric",
4229
- "shares_job",
4230
- "shares_need",
4231
- // Product-to-product relationships.
4232
- "depends_on_product",
4233
- // runtime dependency (built-on / dogfood), not containment
4234
- "cannibalises",
4235
- "succeeds",
4236
- "hosts",
4237
- // host runs the hosted product inside itself (container -> contained)
4238
- // OKR / measurement rollup primitives: the cascade ACROSS products (subordinate
4239
- // -> superior). `contributes_to` rolls a product strategy entity up to a higher-
4240
- // level one; `rolls_up_to` feeds a product metric into a higher-level north-star.
4241
- "contributes_to",
4242
- "rolls_up_to",
4243
- // Canonical instance + area-to-audience: the target lives in the portfolio
4244
- // `registry` section (`registry/{node_id}`). `instance_of` is same-type
4245
- // canonical-to-instance; the area edges are minted via `link_area_to_audience`.
4246
- "instance_of",
4247
- "area_serves_persona",
4248
- "area_targets_market_segment",
4249
- // Foundations (0.9.12): a product-graph entity links to a canonical specification
4250
- // or primitive in the registry (`registry/{node_id}` target, same shape as
4251
- // instance_of). Registry-internal spec-to-spec links are catalog edges, not these.
4252
- "product_implements_specification",
4253
- "product_exposes_specification",
4254
- "feature_conforms_to_specification",
4255
- "api_contract_speaks_specification",
4256
- "product_exposes_primitive",
4257
- "feature_manipulates_primitive",
4258
- "primitive_stored_as_data_type"
4259
- ];
4260
- var UPG_CROSS_EDGE_TYPES = [
4261
- ...UPG_CROSS_ONLY_EDGE_TYPES,
4262
- ...UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES
4263
- ];
4264
- var REGISTRY_PRODUCT_ID = "registry";
4265
- var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
4266
-
4267
- // src/registry/entity-type-resolution.ts
4268
- var TYPES = getTypes();
4269
- var TYPES_SET = new Set(TYPES);
4270
- var UnknownEntityTypeError = class extends Error {
4271
- suggestions;
4272
- rawType;
4273
- constructor(rawType, suggestions) {
4274
- const suffix = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
4275
- super(`Unknown entity type: "${rawType}".${suffix}`);
4276
- this.name = "UnknownEntityTypeError";
4277
- this.rawType = rawType;
4278
- this.suggestions = suggestions;
4279
- }
4364
+ var NEED_LIFECYCLE = {
4365
+ entity_type: "need",
4366
+ initial_phase: "raw",
4367
+ terminal_phases: ["prioritized"],
4368
+ phases: [
4369
+ {
4370
+ id: "raw",
4371
+ status_category: "triage",
4372
+ label: "Raw",
4373
+ description: "Captured from an interview, observation, or support ticket. Articulation may be rough; may duplicate an existing need.",
4374
+ transitions_to: ["validated"]
4375
+ },
4376
+ {
4377
+ id: "validated",
4378
+ status_category: "started",
4379
+ label: "Validated",
4380
+ description: "Corroborated by multiple data points or targeted research. The team agrees it is a real, recurring need.",
4381
+ transitions_to: ["prioritized"]
4382
+ },
4383
+ {
4384
+ id: "prioritized",
4385
+ status_category: "completed",
4386
+ label: "Prioritized",
4387
+ description: "Ranked against other needs. Ready to inform opportunity definition and solution exploration.",
4388
+ transitions_to: []
4389
+ }
4390
+ ]
4280
4391
  };
4281
- function withinEditDistance1(a, b) {
4282
- if (a === b) return true;
4283
- const al = a.length;
4284
- const bl = b.length;
4285
- if (Math.abs(al - bl) > 1) return false;
4286
- let i = 0;
4287
- let j = 0;
4288
- let edits = 0;
4289
- while (i < al && j < bl) {
4290
- if (a[i] !== b[j]) {
4291
- if (++edits > 1) return false;
4292
- if (al > bl) i++;
4293
- else if (bl > al) j++;
4294
- else {
4295
- i++;
4296
- j++;
4297
- }
4298
- } else {
4299
- i++;
4300
- j++;
4392
+ var OPPORTUNITY_LIFECYCLE = {
4393
+ entity_type: "opportunity",
4394
+ initial_phase: "identified",
4395
+ terminal_phases: [],
4396
+ phases: [
4397
+ {
4398
+ id: "identified",
4399
+ status_category: "triage",
4400
+ label: "Identified",
4401
+ description: "A problem worth solving has been named. Sources include needs, insights, market trends, or competitive signals. Sizing and validation come next.",
4402
+ transitions_to: ["validated", "deferred"]
4403
+ },
4404
+ {
4405
+ id: "validated",
4406
+ status_category: "started",
4407
+ label: "Validated",
4408
+ description: "Research and evidence support pursuit. The problem is real, frequent, and painful enough to justify investment.",
4409
+ transitions_to: ["deferred"]
4410
+ },
4411
+ {
4412
+ id: "deferred",
4413
+ status_category: "backlog",
4414
+ label: "Deferred",
4415
+ description: "Acknowledged but parked. Capacity, priority, or strategic timing reasons. Can be reopened.",
4416
+ transitions_to: ["identified"]
4301
4417
  }
4302
- }
4303
- if (i < al || j < bl) edits++;
4304
- return edits <= 1;
4305
- }
4306
- function resolveEntityType(rawType) {
4307
- if (typeof rawType !== "string" || rawType.length === 0) {
4308
- throw new UnknownEntityTypeError(String(rawType ?? ""), []);
4309
- }
4310
- if (TYPES_SET.has(rawType)) {
4311
- return { canonical: rawType };
4312
- }
4313
- const replacement = getReplacementType(rawType);
4314
- if (replacement && TYPES_SET.has(replacement)) {
4315
- return { canonical: replacement, alias: { from: rawType, to: replacement } };
4316
- }
4317
- const suggestions = [];
4318
- for (const t of TYPES) {
4319
- if (withinEditDistance1(t, rawType)) suggestions.push(t);
4320
- if (suggestions.length >= 5) break;
4321
- }
4322
- throw new UnknownEntityTypeError(rawType, suggestions);
4323
- }
4324
-
4325
- // src/grammar/hierarchy.ts
4326
- var UPG_VALID_CHILDREN = {
4327
- // ── Foundations (0.9.12): registry canonicals that self-nest. A
4328
- // specification extends a specification (a dialect of a parent spec); a
4329
- // primitive composes primitives (a block contains spans). These back the
4330
- // hierarchy-classified specification_extends_specification /
4331
- // primitive_composes_primitive catalog edges.
4332
- specification: ["specification"],
4333
- primitive: ["primitive"],
4334
- // An operating_lifecycle (e.g. the content-ops lifecycle) contains its ordered
4335
- // operating_stage children. Backs the operating_lifecycle_contains_operating_stage edge.
4336
- operating_lifecycle: ["operating_stage"],
4337
- // ── Product (root) ─────────────────────────────────────────────────────────
4338
- product: [
4339
- // Strategy
4340
- "vision",
4341
- "outcome",
4342
- "objective",
4343
- "metric",
4344
- "decision",
4345
- "constraint",
4346
- // User
4347
- "persona",
4348
- // Discovery (via outcome children)
4349
- // Validation (via hypothesis children)
4350
- // Market Intelligence
4351
- "competitor",
4352
- "market_trend",
4353
- "market_segment",
4354
- "competitive_analysis",
4355
- // User Research
4356
- "research_study",
4357
- // Experience Design
4358
- "user_journey",
4359
- "user_flow",
4360
- "wireframe",
4361
- "screen",
4362
- // Design System
4363
- "design_system",
4364
- "design_component",
4365
- // Brand Identity
4366
- "brand_identity",
4367
- // Product Specification
4368
- "feature",
4369
- "feature_area",
4370
- "release",
4371
- "roadmap",
4372
- "roadmap_theme",
4373
- "planning_cycle",
4374
- // Configuration (0.30.0): the levers whose values select which surface tree
4375
- // the product renders. Few per product, and top-level because an axis is a
4376
- // product-wide fact rather than a child of any one feature or surface.
4377
- "configuration_axis",
4378
- // Classification (0.32.0): the product's own taxonomies — the grouped-label
4379
- // case, one named group over a set of values. Same reasoning as the
4380
- // configuration axis above, and until now the only parent a
4381
- // classification_axis could have was a competitive_analysis.
4382
- "classification_axis",
4383
- // Engineering
4384
- "bounded_context",
4385
- "code_repository",
4386
- "integration_pattern",
4387
- "external_api",
4388
- "data_flow",
4389
- // Growth
4390
- "funnel",
4391
- "acquisition_channel",
4392
- "cohort",
4393
- "behavioral_segment",
4394
- "growth_loop",
4395
- "attribution_model",
4396
- // Business Model
4397
- "business_model",
4398
- // Go-To-Market
4399
- "gtm_strategy",
4400
- // Team & Organisation
4401
- // `person` is containment-free (see UPG_CONTAINMENT_FREE_TYPES below)
4402
- // (referenced via edges, not nested under product)
4403
- "team",
4404
- "stakeholder",
4405
- "department",
4406
- // Data & Analytics
4407
- "data_source",
4408
- "event_schema",
4409
- "dashboard",
4410
- "data_domain",
4411
- "glossary_term",
4412
- // Content & Knowledge
4413
- "content_piece",
4414
- "knowledge_base_article",
4415
- "brand_asset",
4416
- "document",
4417
- "documentation_template",
4418
- // Legal
4419
- "legal_entity",
4420
- "privacy_policy",
4421
- // Compliance
4422
- "compliance_requirement",
4423
- "risk",
4424
- "data_contract",
4425
- "audit_log_policy",
4426
- "compliance_framework",
4427
- // DevOps & Platform
4428
- "service_level_objective",
4429
- "incident",
4430
- "runbook",
4431
- "monitor",
4432
- "ci_pipeline",
4433
- "release_strategy",
4434
- "on_call_rotation",
4435
- "infrastructure_component",
4436
- // Security
4437
- "threat_model",
4438
- "security_control",
4439
- "security_policy",
4440
- "penetration_test",
4441
- "security_review",
4442
- "data_classification",
4443
- "access_policy",
4444
- // Accessibility
4445
- "a11y_standard",
4446
- "a11y_audit",
4447
- "a11y_annotation",
4448
- // Quality Assurance
4449
- "test_plan",
4450
- "test_suite",
4451
- "qa_session",
4452
- "test_coverage_report",
4453
- "test_environment",
4454
- // Customer Feedback
4455
- "feedback_program",
4456
- "user_advisory_board",
4457
- "beta_program",
4458
- // Pricing & Packaging
4459
- "pricing_strategy",
4460
- // AI & Machine Learning
4461
- "ai_model",
4462
- "model_comparison",
4463
- // Workflows & Agents
4464
- "workflow_template",
4465
- "agent_definition",
4466
- // Sales & Revenue
4467
- "pipeline_sales",
4468
- "account",
4469
- "lead",
4470
- "subscription",
4471
- "forecast",
4472
- // Program Management
4473
- // `milestone` is a SECOND, shallower parent here (Portfolio Phase 2,
4474
- // 2026-08-14), alongside its existing `project` parent: a product can own a
4475
- // milestone outright with no program/project above it. Required by the
4476
- // hierarchy-orphan guard now that `product_targets_milestone` exists.
4477
- "program",
4478
- "milestone",
4479
- // Marketing
4480
- "marketing_strategy",
4481
- "press_release",
4482
- "event",
4483
- "community_initiative",
4484
- // Customer Success
4485
- "support_ticket",
4486
- "customer_feedback",
4487
- "churn_reason",
4488
- "customer_health_score",
4489
- "playbook",
4490
- "service_level_agreement",
4491
- "success_milestone",
4492
- "service_blueprint",
4493
- "nps_campaign",
4494
- // Localisation
4495
- "locale",
4496
- "locale_config",
4497
- // Customer Education
4498
- "education_program",
4499
- // Partners & Ecosystem
4500
- "partner_program",
4501
- "api_ecosystem",
4502
- "developer_portal",
4503
- // Portfolio
4504
- // (UPG-677) product no longer lists product_area as a child: the
4505
- // product↔product_area containment is the single direction
4506
- // product_area_contains_product (product_area → product). The inverse
4507
- // product_categorised_in_product_area edge was collapsed.
4508
- // Workspace
4509
- "workspace"
4510
- ],
4511
- // ── Metric & Outcome hierarchy ──────────────────────────────────────────────
4512
- outcome: ["metric", "opportunity", "feature"],
4513
- // ── User & Jobs hierarchy ───────────────────────────────────────────────────
4514
- persona: ["job", "need", "switching_cost", "desired_outcome"],
4515
- job: ["need", "desired_outcome", "job_step"],
4516
- // ── Discovery hierarchy ─────────────────────────────────────────────────────
4517
- opportunity: ["solution", "design_concept", "feasibility_study", "design_sprint"],
4518
- // solutions now propose hypothesis (the templated
4519
- // belief), not the legacy hypothesis type.
4520
- solution: ["hypothesis", "prototype", "metric"],
4521
- // ── Validation hierarchy ────────────────────────────────────────────────────
4522
- // (UPG-664) The validation chain is a single-parent line:
4523
- // hypothesis ▷ experiment_plan ▷ experiment ▷ experiment_run.
4524
- // hypothesis is canonical (hypothesis_evidence collapsed into evidence;
4525
- // relationship expressed via hypothesis_has_evidence edge; evidence.direction
4526
- // carries supports/refutes/neutral semantics). `test_plan` re-homed to the
4527
- // QA/testing domain (UPG-678), so it is no longer a hypothesis child.
4528
- hypothesis: ["experiment_plan", "research_plan", "evidence"],
4529
- // experiment_plan is the canonical validation PLAN (graduated stable,
4530
- // UPG-664). It designs the experiment it produces; the plan owns the
4531
- // experiment in the containment line. It also retains experiment_run as a
4532
- // child for the v0.2.6 split-migration path
4533
- // (`experiment_plan_ran_as_experiment_run`), where a legacy experiment routes
4534
- // directly to a plan + run pair.
4535
- experiment_plan: ["experiment", "experiment_run"],
4536
- // experiment is the canonical structured test. It owns its run(s) and the
4537
- // learning/evidence it produces. The optional experiment_run child captures
4538
- // the multi-run / replication case.
4539
- experiment: ["experiment_run", "learning", "evidence"],
4540
- // experiment_run carries learning/evidence/metric children (run produces
4541
- // evidence) and self-nests for replications/multi-armed runs (V9: this is
4542
- // where self-nesting genuinely lives, not on `experiment`).
4543
- experiment_run: ["learning", "evidence", "metric", "experiment_run"],
4544
- // ── OKR hierarchy ──────────────────────────────────────────────────────────
4545
- // strategic_question (0.17.4): an open coordination question raised under an
4546
- // objective (objective_raises_strategic_question, hierarchy).
4547
- objective: ["key_result", "metric", "strategic_question"],
4548
- key_result: ["metric"],
4549
- // ── Strategic cascade: vision → mission → strategic_pillar → strategic_theme →
4550
- // { initiative, objective } ───────────────────────────────────────────────
4551
- //
4552
- // strategic_pillar vs strategic_theme (UPG-692 T3.3 — distinct cascade levels,
4553
- // NOT duplicates; sharpened rather than merged):
4554
- // - strategic_pillar = a DURABLE structural division of the strategy. A
4555
- // standing area the org organises around for years (e.g. "Structured
4556
- // content", "Developer experience"). Few, long-lived; time_horizon is
4557
- // multi-year / open-ended; carries its own `success_indicator`.
4558
- // - strategic_theme = a TIME-BOUND thrust WITHIN a pillar. The current bet
4559
- // for a period (e.g. "Go AI-native this year"). More numerous, rotates;
4560
- // time_horizon is bounded (Q / FY); has NO `success_indicator` because it
4561
- // is measured through its child objectives (strategic_theme_contains_objective).
4562
- // That success-measurement asymmetry is the load-bearing distinction.
4563
- vision: ["mission"],
4564
- mission: ["strategic_pillar"],
4565
- // 0.20.1: metric added as a strategic_pillar child, backing
4566
- // strategic_pillar_measured_by_metric (hierarchy) — the pillar's own
4567
- // north-star, mirrored from objective: ['key_result', 'metric', ...] one
4568
- // level down the cascade.
4569
- strategic_pillar: ["strategic_theme", "capability", "value_stream", "decision", "metric"],
4570
- // v0.5.4 (UPG-511): objectives are the specific quarterly bets *within* a
4571
- // strategic theme. The theme is the multi-quarter focus area; objectives are
4572
- // subordinate. Pairs with `strategic_theme_contains_objective` in the edge
4573
- // catalog (hierarchy, strategic_theme → objective, reverse_verb: rolls_up_to;
4574
- // renamed from objective_rolls_up_to_strategic_theme in UPG-676).
4575
- strategic_theme: ["initiative", "objective"],
4576
- initiative: ["assumption", "strategic_question"],
4577
- // v0.5.2 (UPG-528): capability decomposes into sub-capabilities (Wardley
4578
- // value-chain spine) and is realised by user-facing features. Pairs with
4579
- // `capability_depends_on_capability` and `capability_implemented_by_feature`
4580
- // in the edge catalogue. The self-loop guard (UPG-520) keeps A → A refused;
4581
- // A → B between distinct capabilities is the supported decomposition.
4582
- capability: ["capability", "feature"],
4583
- // ── Market hierarchy ────────────────────────────────────────────────────────
4584
- // competitive_analysis hosts classification axes (its dimensions);
4585
- // each axis hosts its values; values are reusable across the row and column
4586
- // dossiers of a 2-axis matrix.
4587
- competitive_analysis: ["competitor", "market_trend", "market_segment", "classification_axis"],
4588
- competitor: ["competitor_feature", "competitor_signal"],
4589
- classification_axis: ["classification_value"],
4590
- // ── UX Research hierarchy ───────────────────────────────────────────────────
4591
- research_study: [
4592
- "participant",
4593
- "observation",
4594
- "affinity_cluster",
4595
- "research_question",
4596
- "interview_guide",
4597
- "insight",
4598
- "survey_response"
4599
- ],
4600
- observation: ["quote"],
4601
- // F6 (UPG-672): affinity_cluster owns the observations it groups, in addition
4602
- // to the insights it synthesises. Pairs with `affinity_cluster_groups_observation`
4603
- // in the edge catalog (hierarchy). An observation can be both directly owned by
4604
- // its research_study and grouped under an affinity_cluster (multi-parent grammar).
4605
- affinity_cluster: ["insight", "observation"],
4606
- // insight refines into insight; a raw insight can be distilled
4607
- // into a more specific insight (refines_into chain).
4608
- // v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
4609
- // parallel to `observation: ['quote']`. Research-synthesis canon.
4610
- // Pairs with `insight_evidenced_by_quote` in the edge catalog.
4611
- // ── Design hierarchy ────────────────────────────────────────────────────────
4612
- // (UPG-663, v0.9.2) A user_journey owns its steps (the stable 0.1.0 spine)
4613
- // and carries phases as a non-owning band overlay. A journey_phase is NO
4614
- // LONGER a containment parent of journey_step: it SPANS steps via the
4615
- // `journey_phase_spans_journey_step` edge (mirroring the marketing
4616
- // `customer_journey_stage_spans_journey_step` precedent), so each step has
4617
- // exactly one containment parent and the journey has one canonical step list.
4618
- user_journey: ["journey_step", "journey_phase"],
4619
- journey_step: ["journey_action"],
4620
- // v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
4621
- // parallel to `observation: ['quote']`. Research-synthesis canon
4622
- // (insight ← quote evidencing) extends quote's hierarchy parents to
4623
- // both observation (raw-capture parent) and insight (synthesis parent).
4624
- // Pairs with `insight_evidenced_by_quote` in the edge catalog.
4625
- insight: ["design_question", "insight", "quote"],
4626
- // v0.5.2 (UPG-528): need anchors a Wardley value chain; capabilities
4627
- // fulfil the need at the chain top. Cross-domain hierarchy (need ∈ Design,
4628
- // capability ∈ Strategy) is intentional: Wardley analysis starts from a
4629
- // user need and decomposes into the capabilities required to fulfil it.
4630
- need: ["design_question", "capability"],
4631
- design_question: ["design_concept"],
4632
- design_concept: ["prototype", "wireframe"],
4633
- design_component: [
4634
- "design_token",
4635
- "design_pattern",
4636
- "design_guideline",
4637
- "interaction_spec",
4638
- "design_component"
4639
- ],
4640
- // self-nesting for atomic hierarchy (organism → molecule → atom)
4641
- prototype: ["annotation"],
4642
- // ── Brand hierarchy ─────────────────────────────────────────────────────────
4643
- brand_identity: [
4644
- "brand_colour",
4645
- "brand_typography",
4646
- "brand_asset",
4647
- "brand_voice",
4648
- "brand_logo",
4649
- "brand_imagery"
4650
- ],
4651
- // ── Product Specification hierarchy ─────────────────────────────────────────
4652
- // story_task collapsed into canonical task. feature now owns
4653
- // task directly; story-derived tasks implement user_story via
4654
- // task_implements_user_story edge.
4655
- feature: ["epic", "bug", "task"],
4656
- feature_area: ["feature", "feature_area"],
4657
- // epic owns user_story (the templated promise) and, mirroring feature, may
4658
- // also directly contain bug/task — heterogeneous imported tickets that belong
4659
- // to one epic rather than the feature as a whole (feedback df99026a).
4660
- epic: ["user_story", "bug", "task"],
4661
- user_story: ["acceptance_criterion"],
4662
- task: ["task"],
4663
- // planning_cycle (0.20.0): the cadence axis self-nests (a program-increment
4664
- // contains iterations; a cycle contains its cooldown) via
4665
- // planning_cycle_contains_planning_cycle. It does NOT contain user_story as a
4666
- // child: scheduling work into a cycle is the semantic, deliberate-only
4667
- // planning_cycle_schedules_work_item edge, so the item keeps its feature/epic
4668
- // containment parent and is merely referenced by the cycle.
4669
- planning_cycle: ["planning_cycle"],
4670
- // ── Engineering hierarchy ───────────────────────────────────────────────────
4671
- bounded_context: [
4672
- "service",
4673
- "domain_event",
4674
- "decision",
4675
- "data_model",
4676
- "aggregate",
4677
- "read_model",
4678
- "code_repository",
4679
- "integration_pattern",
4680
- "external_api",
4681
- "data_flow",
4682
- // v0.5.7 (UPG-528 Part 2c): BCs publish api_contracts as their "published
4683
- // language" (DDD canon). api_contract has two valid hierarchy parents now:
4684
- // service (per-service exposure) AND bounded_context (BC-level surface).
4685
- "api_contract"
4686
- ],
4687
- service: [
4688
- "api_contract",
4689
- "technical_debt_item",
4690
- "feature_flag",
4691
- "deployment",
4692
- "api_endpoint",
4693
- "database_schema",
4694
- "queue_topic",
4695
- "build_artifact",
4696
- "library_dependency"
4697
- ],
4698
- // v0.5.1 (UPG-517 C2): contracts contain their endpoints. Service still
4699
- // serves endpoints directly (legacy / non-contract endpoints); both
4700
- // parent-child paths coexist.
4701
- api_contract: ["api_endpoint"],
4702
- decision: ["technical_debt_item"],
4703
- aggregate: ["domain_entity", "value_object", "command"],
4704
- // ── Growth hierarchy ────────────────────────────────────────────────────────
4705
- funnel: ["funnel_step"],
4706
- acquisition_channel: ["growth_campaign"],
4707
- growth_campaign: ["experiment_plan", "variant"],
4708
- // ── Business Model hierarchy ─────────────────────────────────────────────────
4709
- business_model: [
4710
- "value_proposition",
4711
- "revenue_stream",
4712
- "cost_structure",
4713
- "unit_economics",
4714
- "partnership",
4715
- "key_resource",
4716
- "key_activity",
4717
- "customer_relationship",
4718
- "distribution_channel"
4719
- ],
4720
- revenue_stream: ["pricing_tier"],
4721
- // ── Go-To-Market hierarchy ───────────────────────────────────────────────────
4722
- gtm_strategy: [
4723
- "ideal_customer_profile",
4724
- "positioning",
4725
- "launch",
4726
- "content_strategy",
4727
- "sales_motion",
4728
- "competitive_battle_card",
4729
- "demand_gen_program",
4730
- "territory"
4731
- ],
4732
- positioning: ["messaging", "objection", "proof_point"],
4733
- value_proposition: ["objection", "proof_point"],
4734
- competitive_battle_card: ["objection"],
4735
- objection: ["rebuttal"],
4736
- rebuttal: ["proof_point"],
4737
- // ── Team & Organisation hierarchy ───────────────────────────────────────────
4738
- department: ["team", "stakeholder"],
4739
- team: [
4740
- "role",
4741
- "team_okr",
4742
- "retrospective",
4743
- "dependency",
4744
- "skill",
4745
- "ceremony",
4746
- "capacity_plan",
4747
- "decision",
4748
- // Self-nesting (0.17.2): a parent team contains its sub-teams / squads, one
4749
- // level below department_contains_team. Backs team_contains_team. Mirrors
4750
- // feature_area_contains_feature_area (same-type child listed last).
4751
- "team"
4752
- ],
4753
- // ── Data & Analytics hierarchy ───────────────────────────────────────────────
4754
- data_source: ["metric", "data_pipeline", "data_lineage", "event_schema"],
4755
- // UPG-685 T0.4 (0.13.0 Wave 1): `outcome` removed as a metric child. It closed a
4756
- // containment cycle (outcome ⊃ metric ⊃ outcome) and was backed only by the CAUSAL
4757
- // `metric_drives_outcome` edge, not a hierarchy edge — the "metric leads to outcome"
4758
- // meaning lives correctly on that causal edge. The real containment (outcome ⊃ metric)
4759
- // is kept. Sub-metric trees (metric ⊃ metric) stay.
4760
- metric: ["metric", "data_quality_rule", "metric_quality_assessment"],
4761
- data_domain: ["data_product", "data_source", "glossary_term", "data_model", "dashboard"],
4762
- dashboard: ["report", "experiment_run"],
4763
- // ── Content & Knowledge hierarchy ────────────────────────────────────────────
4764
- // release contains features and bugs (GitHub milestone→issue import).
4765
- release: ["changelog", "feature", "bug"],
4766
- content_strategy: ["content_calendar", "content_theme"],
4767
- content_calendar: [
4768
- "content_theme",
4769
- "content_piece",
4770
- "knowledge_base_article",
4771
- "brand_asset",
4772
- "document",
4773
- "documentation_template"
4774
- ],
4775
- // ── Operations & CS hierarchy ────────────────────────────────────────────────
4776
- service_blueprint: [
4777
- "user_flow",
4778
- "playbook",
4779
- "service_level_agreement",
4780
- "customer_health_score",
4781
- "support_ticket",
4782
- "customer_feedback"
4783
- ],
4784
- customer_health_score: ["nps_campaign", "success_milestone"],
4785
- customer_feedback: ["churn_reason"],
4786
- user_flow: ["screen", "customer_journey_stage"],
4787
- customer_journey_stage: ["touchpoint"],
4788
- // ── Legal, Compliance & Risk hierarchy ──────────────────────────────────────
4789
- legal_entity: ["ip_asset", "contract"],
4790
- contract: ["contract_clause"],
4791
- compliance_framework: [
4792
- "security_audit",
4793
- "compliance_requirement",
4794
- "privacy_policy",
4795
- "audit_log_policy",
4796
- "risk",
4797
- "data_contract",
4798
- "legal_entity"
4799
- ],
4800
- // ── DevOps & Platform hierarchy ──────────────────────────────────────────────
4801
- infrastructure_component: [
4802
- "service_level_objective",
4803
- "monitor",
4804
- "ci_pipeline",
4805
- "incident",
4806
- "runbook",
4807
- "release_strategy",
4808
- "on_call_rotation"
4809
- ],
4810
- service_level_objective: ["service_level_indicator", "error_budget"],
4811
- incident: ["postmortem"],
4812
- monitor: ["alert_rule"],
4813
- ci_pipeline: ["build_artifact"],
4814
- // ── Security hierarchy ───────────────────────────────────────────────────────
4815
- security_policy: [
4816
- "security_control",
4817
- "access_policy",
4818
- "data_classification",
4819
- "threat_model",
4820
- "security_review",
4821
- "incident"
4822
- ],
4823
- security_review: ["penetration_test"],
4824
- threat_model: ["threat", "vulnerability"],
4825
- // ── Sales & Revenue hierarchy ────────────────────────────────────────────────
4826
- pipeline_sales: ["pipeline_stage", "lead", "account", "forecast", "subscription"],
4827
- account: ["contact", "deal"],
4828
- deal: ["quote_document"],
4829
- subscription: ["invoice"],
4830
- // ── Program Management hierarchy ─────────────────────────────────────────────
4831
- program: ["project", "risk_register", "change_request", "resource_allocation", "status_report"],
4832
- project: ["milestone", "deliverable"],
4833
- risk_register: ["risk"],
4834
- // ── Accessibility hierarchy ──────────────────────────────────────────────────
4835
- a11y_standard: ["a11y_guideline", "a11y_audit", "a11y_annotation"],
4836
- a11y_audit: ["a11y_issue"],
4837
- // ── Product Specification expansion ─────────────────────────────────────────
4838
- roadmap: ["roadmap_item", "roadmap_theme", "release"],
4839
- roadmap_theme: ["feature"],
4840
- // ── Unified Context Layer hierarchy ─────────────────────────────────────────
4841
- design_system: [
4842
- "design_component",
4843
- "design_token",
4844
- "design_guideline",
4845
- "brand_identity",
4846
- "user_journey",
4847
- "user_flow",
4848
- "insight"
4849
- ],
4850
- screen: ["screen_state", "screen", "surface", "design_component", "wireframe"],
4851
- // self-nesting + surfaces + components + wireframes
4852
- // surface (0.27.0): the place inside a screen. Self-nests, so a shell holds
4853
- // panes, a pane holds regions, a region holds slots. A surface is reached
4854
- // through the screen that renders it (`screen: [... 'surface']`), never as a
4855
- // top-level product child: every UPG_VALID_CHILDREN pair must be backed by a
4856
- // hierarchy-classified edge (guardrail G2b), and the ratified ten do not
4857
- // include a product-to-surface verb. An app-shell surface therefore hangs off
4858
- // the screen that mounts it.
4859
- //
4860
- // NOTE: `surface_kind` narrows this further than UPG_VALID_CHILDREN can
4861
- // express. UPG_VALID_CHILDREN is keyed on entity TYPE, so it can only say
4862
- // "a surface may contain a surface"; the kind ordering (shell > tool > pane >
4863
- // region > slot / gutter / action_bar, with overlay and ambient parented by
4864
- // whatever raises them) is a property-level rule documented on `SurfaceKind`
4865
- // and enforced by validators, not by this table. Precedent: the same split
4866
- // applies to `planning_cycle`, whose `cadence_kind` narrows its self-nesting.
4867
- surface: ["surface", "design_component"],
4868
- // ── Marketing & Communications hierarchy ────────────────────────────────────
4869
- marketing_strategy: [
4870
- "marketing_channel",
4871
- "seo_keyword",
4872
- "press_release",
4873
- "event",
4874
- "community_initiative"
4875
- ],
4876
- marketing_channel: ["marketing_campaign_plan"],
4877
- marketing_campaign_plan: ["email_sequence", "social_post", "ad_creative"],
4878
- // ── Localisation & i18n hierarchy ───────────────────────────────────────────
4879
- locale: ["translation_bundle", "cultural_adaptation", "regional_pricing", "locale_config"],
4880
- translation_bundle: ["translation_key"],
4881
- // ── Customer Education & Training hierarchy ──────────────────────────────────
4882
- education_program: [
4883
- "tutorial",
4884
- "walkthrough",
4885
- "webinar",
4886
- "certification",
4887
- "help_video",
4888
- "learning_path"
4889
- ],
4890
- // a learning_path can include its terminal certification.
4891
- learning_path: ["tutorial", "certification"],
4892
- // ── Quality Assurance & Testing hierarchy ────────────────────────────────────
4893
- // (UPG-678) test_plan re-homed validation → QA. It is the QA planning layer:
4894
- // the verification approach (scope, environments, pass criteria) that the
4895
- // test suites execute. A test_plan groups the suites that carry it out.
4896
- test_plan: ["test_suite", "test_environment"],
4897
- test_suite: [
4898
- "test_case",
4899
- "regression_test",
4900
- "qa_session",
4901
- "test_coverage_report",
4902
- "test_environment",
4903
- "test_result"
4904
- ],
4905
- test_case: ["test_result"],
4906
- qa_session: ["bug"],
4907
- // ── Partner & Ecosystem Management hierarchy ─────────────────────────────────
4908
- partner_program: [
4909
- "partner_tier",
4910
- "integration_partner",
4911
- "partner_revenue_share",
4912
- "api_ecosystem",
4913
- "developer_portal"
4914
- ],
4915
- api_ecosystem: ["marketplace_listing"],
4916
- // ── Feedback & Voice of Customer hierarchy ───────────────────────────────────
4917
- feedback_program: [
4918
- "feature_request",
4919
- "nps_campaign",
4920
- "feedback_theme",
4921
- "user_advisory_board",
4922
- "beta_program"
4923
- ],
4924
- feature_request: ["feedback_vote"],
4925
- // v0.5.8 (UPG-528 Part 2d): CABs convene as quarterly ceremonies;
4926
- // CAB owns its meeting cadence (Stettler, Moore CAB playbook canon).
4927
- // Pairs with `user_advisory_board_convenes_as_ceremony` in the edge
4928
- // catalog. ceremony now has two valid hierarchy parents (team for sprint
4929
- // ceremonies, user_advisory_board for CAB meetings).
4930
- user_advisory_board: ["ceremony"],
4931
- // ── Pricing & Packaging hierarchy ────────────────────────────────────────────
4932
- pricing_strategy: [
4933
- "experiment_plan",
4934
- "pricing_tier",
4935
- "discount_strategy",
4936
- "trial_config",
4937
- "paywall"
4938
- ],
4939
- // ── AI/ML Operations hierarchy ───────────────────────────────────────────────
4940
- // (UPG-665) The prompt abstraction is corrected: ai_model → prompt_template →
4941
- // prompt_version (a model has templates; each template has versions). The
4942
- // model no longer owns prompt_version directly.
4943
- ai_model: [
4944
- "prompt_template",
4945
- "eval_benchmark",
4946
- "ai_cost_tracker",
4947
- "hallucination_report",
4948
- "ai_guardrail",
4949
- "model_comparison",
4950
- "ai_experiment",
4951
- "ai_dataset",
4952
- "ai_trace"
4953
- ],
4954
- // prompt_template owns its prompt_versions the way a file owns its commits.
4955
- prompt_template: ["prompt_version"],
4956
- // v0.5.7 (UPG-528 Part 2c): benchmarks define the metric set they measure
4957
- // (HELM, MLPerf, BIG-bench all spec a metric list). metric already has many
4958
- // hierarchy parents (outcome, objective, key_result, solution, data_source).
4959
- eval_benchmark: ["eval_run", "metric"],
4960
- // ai_trace spawns child traces for sub-calls (tool calls, chained
4961
- // prompts). Self-nesting enables call-tree representation.
4962
- ai_trace: ["ai_trace"],
4963
- // ── Agentic Workflows & Process hierarchy ────────────────────────────────────
4964
- workflow_template: ["workflow_run", "review_gate", "agent_task"],
4965
- workflow_run: ["workflow_artifact"],
4966
- agent_definition: ["agent_session", "agent_skill", "agent_hook", "workflow_template", "agent_task"],
4967
- // v0.5.8 (UPG-528 Part 2d): review gates also vet research insights
4968
- // (ResearchOps insight-review pattern). Pairs with `review_gate_vets_insight`
4969
- // in the edge catalog. The gate now owns two child types: approval records
4970
- // for workflow gates, insights for research-democratisation gates.
4971
- review_gate: ["approval_record", "insight"],
4972
- // ── Investigation hierarchy ──────────────────────────────────────────────────
4973
- investigation: ["symptom", "root_cause"],
4974
- root_cause: ["fix"],
4975
- // ── Portfolio layer hierarchy ────────────────────────────────────────────────
4976
- // organization → product_area is the org-axis anchor (who owns what);
4977
- // organization → portfolio is the strategic-axis anchor (where you invest).
4978
- // organization → workspace (WS3, 2026-07-05): org-altitude workspace anchor
4979
- // (organization_thinks_in_workspace) — a workspace scoped to org altitude
4980
- // lives in the same portfolio-level graph as its organization node.
4981
- organization: ["portfolio", "product_area", "workspace"],
4982
- // portfolios can nest (multi-level investment structures).
4983
- portfolio: ["product", "portfolio"],
4984
- // product_area is the organisational container for products and can
4985
- // nest (parent → sub-area). product_area also groups features; the
4986
- // "Studio area owns 6 features" mental model. product_area → workspace
4987
- // (WS3, 2026-07-05): plane-altitude workspace anchor
4988
- // (product_area_thinks_in_workspace) — "plane" is Entopo's app-level name
4989
- // for product_area, not a distinct spec entity.
4990
- product_area: ["product", "product_area", "feature", "workspace"]
4991
- };
4992
- function getValidChildren(entityType) {
4993
- return UPG_VALID_CHILDREN[entityType] ?? [];
4994
- }
4995
- function canBeChildOf(childType, parentType) {
4996
- return getValidChildren(parentType).includes(childType);
4997
- }
4998
- var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
4999
- "person",
5000
- // A framework_exercise has no structural parent: it anchors to the entities
5001
- // it scores via the `framework_exercise_includes_node` edge, not by
5002
- // containment. Modelling it as a product child would force a containment edge
5003
- // and read against the whole point of the exercise model (relational, not
5004
- // hierarchical). Same posture as `person`. See ADR 2026-06-02-framework-exercises.
5005
- "framework_exercise",
5006
- // A composition has no structural parent for the same reason a
5007
- // framework_exercise does not: it anchors to what it SHOWS, relationally, via
5008
- // `composition_focuses_node`. Modelling it as a product child would force a
5009
- // containment edge that reads against the point — a published view is not a
5010
- // part of the product, it is a lens onto one. This is also what keeps the
5011
- // type to two edges rather than three: no provenance edge is minted (that is
5012
- // `workspace_produced_node`, reused) and no parent edge is needed.
5013
- "composition",
5014
- // A capture has no structural parent for the same reason: it anchors to what
5015
- // it RENDERS, via `capture_renders_node`. Giving it a product parent would
5016
- // claim a capture is part of the product rather than a picture of one, and
5017
- // would mint a containment edge whose only job is to satisfy the hierarchy.
5018
- "capture"
5019
- ]);
5020
- function isContainmentFreeType(entityType) {
5021
- return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
5022
- }
5023
-
5024
- // src/grammar/projection.ts
5025
- var VARIES_BY_EDGE = "surface_varies_by_configuration_axis";
5026
- var PRESENT_UNDER_PROPERTY = "present_under";
5027
- var ACTIVE_WHEN_PROPERTY = "active_when";
5028
- var QUALIFIABLE_EDGE_TYPES = [
5029
- "surface_contains_surface",
5030
- "feature_occupies_surface"
5031
- ];
5032
- function readStringArray(properties, key) {
5033
- const raw = properties?.[key];
5034
- if (!Array.isArray(raw)) return void 0;
5035
- return raw.filter((v) => typeof v === "string");
5036
- }
5037
- function readActiveWhen(edge) {
5038
- const raw = edge.properties?.[ACTIVE_WHEN_PROPERTY];
5039
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
5040
- const axis = raw.axis;
5041
- const values = readStringArray(raw, "values");
5042
- if (typeof axis !== "string" || axis.length === 0 || !values || values.length === 0) {
5043
- return void 0;
5044
- }
5045
- return { axis, values };
5046
- }
5047
- function projectGraph(nodes, edges, configuration) {
5048
- const axes = Object.keys(configuration);
5049
- if (axes.length === 0) {
5050
- return {
5051
- nodes: [...nodes],
5052
- edges: [...edges],
5053
- excluded_node_ids: [],
5054
- deactivated_edge_count: 0,
5055
- dangling_edge_count: 0
5056
- };
5057
- }
5058
- const excluded = /* @__PURE__ */ new Set();
5059
- for (const edge of edges) {
5060
- if (edge.type !== VARIES_BY_EDGE) continue;
5061
- const chosen = configuration[edge.target];
5062
- if (chosen === void 0) continue;
5063
- const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
5064
- if (!presentUnder) continue;
5065
- if (!presentUnder.includes(chosen)) excluded.add(edge.source);
5066
- }
5067
- const survivingNodes = excluded.size === 0 ? [...nodes] : nodes.filter((n) => !excluded.has(n.id));
5068
- let deactivated = 0;
5069
- let dangling = 0;
5070
- const survivingEdges = [];
5071
- for (const edge of edges) {
5072
- const qualifier = readActiveWhen(edge);
5073
- if (qualifier) {
5074
- const chosen = configuration[qualifier.axis];
5075
- if (chosen !== void 0 && !qualifier.values.includes(chosen)) {
5076
- deactivated++;
5077
- continue;
5078
- }
5079
- }
5080
- if (excluded.has(edge.source) || excluded.has(edge.target)) {
5081
- dangling++;
5082
- continue;
5083
- }
5084
- survivingEdges.push(edge);
5085
- }
5086
- return {
5087
- nodes: survivingNodes,
5088
- edges: survivingEdges,
5089
- excluded_node_ids: [...excluded].sort(),
5090
- deactivated_edge_count: deactivated,
5091
- dangling_edge_count: dangling
5092
- };
5093
- }
5094
- function enumerateProjections(nodes) {
5095
- const out = [
5096
- { configuration: {} }
5097
- ];
5098
- for (const node of nodes) {
5099
- if (node.type !== "configuration_axis") continue;
5100
- const values = readStringArray(node.properties, "values");
5101
- if (!values) continue;
5102
- for (const value of values) {
5103
- out.push({ axis: node.id, value, configuration: { [node.id]: value } });
5104
- }
5105
- }
5106
- return out;
5107
- }
5108
-
5109
- // src/grammar/configuration-drift.ts
5110
- var ALTERNATES_EDGE = "surface_alternates_with_surface";
5111
- var PARENT_EDGE_TYPES = /* @__PURE__ */ new Set(["surface_contains_surface", "screen_renders_surface"]);
5112
- function edgeLabel(edge) {
5113
- return edge.id ?? `${edge.source} -> ${edge.target}`;
5114
- }
5115
- function checkConfigurationDrift(nodes, edges) {
5116
- const findings = [];
5117
- const axisValues = /* @__PURE__ */ new Map();
5118
- for (const node of nodes) {
5119
- if (node.type !== "configuration_axis") continue;
5120
- const values = readStringArray(node.properties, "values");
5121
- if (!values || values.length === 0) {
5122
- findings.push({
5123
- kind: "axis_values_empty",
5124
- severity: "error",
5125
- node_id: node.id,
5126
- axis_id: node.id,
5127
- message: "Configuration axis declares no values. An axis with no values selects nothing, so no projection can be taken along it."
5128
- });
5129
- axisValues.set(node.id, []);
5130
- continue;
5131
- }
5132
- axisValues.set(node.id, values);
5133
- const rawDefault = node.properties?.default_value;
5134
- if (typeof rawDefault === "string" && rawDefault.length > 0 && !values.includes(rawDefault)) {
5135
- findings.push({
5136
- kind: "axis_default_not_a_value",
5137
- severity: "error",
5138
- node_id: node.id,
5139
- axis_id: node.id,
5140
- message: `default_value "${rawDefault}" is not one of the axis values [${values.join(", ")}]. It names a value of this axis, so it has to be one the axis declares.`
5141
- });
5142
- }
5143
- }
5144
- for (const edge of edges) {
5145
- if (edge.type !== VARIES_BY_EDGE) continue;
5146
- const values = axisValues.get(edge.target);
5147
- const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
5148
- if (!presentUnder || presentUnder.length === 0) {
5149
- findings.push({
5150
- kind: "present_under_empty",
5151
- severity: "error",
5152
- edge_id: edge.id,
5153
- node_id: edge.source,
5154
- axis_id: edge.target,
5155
- message: `${edgeLabel(edge)} declares variance without a non-empty present_under. A surface that exists under no configuration should be deleted rather than declared; a surface that exists under all of them should carry no varies_by edge at all.`
5156
- });
5157
- continue;
5158
- }
5159
- if (!values) continue;
5160
- for (const value of presentUnder) {
5161
- if (!values.includes(value)) {
5162
- findings.push({
5163
- kind: "present_under_unknown_value",
5164
- severity: "error",
5165
- edge_id: edge.id,
5166
- node_id: edge.source,
5167
- axis_id: edge.target,
5168
- message: `present_under names "${value}", which the axis does not declare. Its values are [${values.join(", ")}].`
5169
- });
5170
- }
5171
- }
5172
- }
5173
- for (const edge of edges) {
5174
- const hasQualifierKey = edge.properties !== void 0 && Object.prototype.hasOwnProperty.call(edge.properties, ACTIVE_WHEN_PROPERTY);
5175
- if (!hasQualifierKey) continue;
5176
- if (edge.type !== void 0 && !QUALIFIABLE_EDGE_TYPES.includes(edge.type)) {
5177
- findings.push({
5178
- kind: "qualifier_on_illegal_edge",
5179
- severity: "error",
5180
- edge_id: edge.id,
5181
- message: `active_when is not legal on ${edge.type}. The qualifier is scoped to [${QUALIFIABLE_EDGE_TYPES.join(", ")}] deliberately: conditional composition is the question this release answers, and a general modality system is not.`
5182
- });
5183
- continue;
5184
- }
5185
- const qualifier = readActiveWhen(edge);
5186
- if (!qualifier) {
5187
- const rawQualifier = edge.properties?.[ACTIVE_WHEN_PROPERTY];
5188
- const rawValues = rawQualifier && typeof rawQualifier === "object" && !Array.isArray(rawQualifier) ? rawQualifier.values : void 0;
5189
- if (Array.isArray(rawValues) && rawValues.length === 0) {
5190
- findings.push({
5191
- kind: "qualifier_values_empty",
5192
- severity: "error",
5193
- edge_id: edge.id,
5194
- message: `${edgeLabel(edge)} carries an active_when with an empty values list. Read literally that would remove the relationship from every configuration, which is a way to delete an edge by writing a property; it is treated as absent instead. To say a relationship never holds, delete the edge.`
5195
- });
5196
- continue;
5197
- }
5198
- findings.push({
5199
- kind: "qualifier_axis_unresolved",
5200
- severity: "error",
5201
- edge_id: edge.id,
5202
- message: `${edgeLabel(edge)} carries a malformed active_when. It must be an object with an axis id and a non-empty values array; anything else reads as absent, which silently makes the relationship invariant.`
5203
- });
5204
- continue;
5205
- }
5206
- const values = axisValues.get(qualifier.axis);
5207
- if (!values) {
5208
- findings.push({
5209
- kind: "qualifier_axis_unresolved",
5210
- severity: "error",
5211
- edge_id: edge.id,
5212
- axis_id: qualifier.axis,
5213
- message: `active_when names axis "${qualifier.axis}", which is not a configuration_axis node in this graph.`
5214
- });
5215
- continue;
5216
- }
5217
- for (const value of qualifier.values) {
5218
- if (!values.includes(value)) {
5219
- findings.push({
5220
- kind: "qualifier_unknown_value",
5221
- severity: "error",
5222
- edge_id: edge.id,
5223
- axis_id: qualifier.axis,
5224
- message: `active_when names value "${value}", which axis "${qualifier.axis}" does not declare. Its values are [${values.join(", ")}].`
5225
- });
5226
- }
5227
- }
5228
- }
5229
- const varianceBySource = /* @__PURE__ */ new Map();
5230
- for (const edge of edges) {
5231
- if (edge.type !== VARIES_BY_EDGE) continue;
5232
- const values = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY) ?? [];
5233
- const list = varianceBySource.get(edge.source) ?? [];
5234
- list.push({ axis: edge.target, values });
5235
- varianceBySource.set(edge.source, list);
5236
- }
5237
- for (const edge of edges) {
5238
- if (edge.type !== ALTERNATES_EDGE) continue;
5239
- const left = varianceBySource.get(edge.source) ?? [];
5240
- const right = varianceBySource.get(edge.target) ?? [];
5241
- if (left.length === 0 || right.length === 0) {
5242
- findings.push({
5243
- kind: "alternation_axis_mismatch",
5244
- severity: "error",
5245
- edge_id: edge.id,
5246
- message: `${edgeLabel(edge)} declares an alternation, but at least one endpoint declares no variance at all. Two surfaces that are both always present are not alternatives; they co-exist.`
5247
- });
5248
- continue;
5249
- }
5250
- const sharedAxes = left.filter((l) => right.some((r) => r.axis === l.axis)).map((l) => l.axis);
5251
- if (sharedAxes.length === 0) {
5252
- findings.push({
5253
- kind: "alternation_axis_mismatch",
5254
- severity: "error",
5255
- edge_id: edge.id,
5256
- message: `${edgeLabel(edge)} declares an alternation between surfaces that vary on different axes. Alternatives are selected by ONE lever; surfaces on different levers can both be present at once.`
5257
- });
5258
- continue;
5259
- }
5260
- for (const axis of sharedAxes) {
5261
- const leftValues = left.find((l) => l.axis === axis)?.values ?? [];
5262
- const rightValues = right.find((r) => r.axis === axis)?.values ?? [];
5263
- const overlap = leftValues.filter((v) => rightValues.includes(v));
5264
- if (overlap.length > 0) {
5265
- findings.push({
5266
- kind: "alternation_overlap",
5267
- severity: "error",
5268
- edge_id: edge.id,
5269
- axis_id: axis,
5270
- message: `${edgeLabel(edge)} declares an alternation, but both surfaces are present under [${overlap.join(", ")}] on axis "${axis}". They co-exist there, so the graph is asserting "one of these" and "both, together" about the same pair.`
5271
- });
5272
- }
5273
- }
5274
- }
5275
- const parentsByChild = /* @__PURE__ */ new Map();
5276
- for (const edge of edges) {
5277
- if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
5278
- parentsByChild.set(edge.target, (parentsByChild.get(edge.target) ?? 0) + 1);
5279
- }
5280
- }
5281
- for (const [axisId, values] of axisValues) {
5282
- for (const value of values) {
5283
- const projected = projectGraph(nodes, edges, { [axisId]: value });
5284
- const survivingParents = /* @__PURE__ */ new Map();
5285
- for (const edge of projected.edges) {
5286
- if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
5287
- survivingParents.set(edge.target, (survivingParents.get(edge.target) ?? 0) + 1);
5288
- }
5289
- }
5290
- for (const node of projected.nodes) {
5291
- if (node.type !== "surface") continue;
5292
- if ((parentsByChild.get(node.id) ?? 0) === 0) continue;
5293
- if ((survivingParents.get(node.id) ?? 0) > 0) continue;
5294
- findings.push({
5295
- kind: "orphaned_under_projection",
5296
- severity: "warning",
5297
- node_id: node.id,
5298
- axis_id: axisId,
5299
- message: `Surface is present under ${axisId} = "${value}" but every containment parent it has is absent there. Either it needs a parent in that configuration, or it should declare that it is absent from it too.`
5300
- });
5301
- }
5302
- }
5303
- }
5304
- return findings;
5305
- }
5306
-
5307
- // src/grammar/lifecycles.ts
5308
- var PRODUCT_LIFECYCLE = {
5309
- entity_type: "product",
5310
- initial_phase: "concept",
5311
- terminal_phases: ["sunset"],
5312
- phases: [
5313
- {
5314
- id: "concept",
5315
- status_category: "triage",
5316
- label: "Concept",
5317
- description: "Napkin idea. Problem shape and solution sketch, pre-validation.",
5318
- transitions_to: ["validation", "build", "beta", "launch", "growth", "mature", "sunset"]
5319
- },
5320
- {
5321
- id: "validation",
5322
- status_category: "started",
5323
- label: "Validation",
5324
- description: "Testing demand. User conversations and experiments pressure-test assumptions before build commits.",
5325
- transitions_to: ["build", "beta", "launch", "growth", "mature", "sunset"]
5326
- },
5327
- {
5328
- id: "build",
5329
- status_category: "started",
5330
- label: "Build",
5331
- description: "Actively developing v1. Core functionality is being authored, pre-user.",
5332
- transitions_to: ["beta", "launch", "growth", "mature", "sunset"]
5333
- },
5334
- {
5335
- id: "beta",
5336
- status_category: "started",
5337
- label: "Beta",
5338
- description: "Early users, iterating. Feature-complete enough to learn from, still changing weekly.",
5339
- transitions_to: ["launch", "growth", "mature", "maintenance", "sunset"]
5340
- },
5341
- {
5342
- id: "launch",
5343
- status_category: "started",
5344
- label: "Launch",
5345
- description: "Generally available. Announced to the target audience, open past beta access.",
5346
- transitions_to: ["growth", "mature", "maintenance", "sunset"]
5347
- },
5348
- {
5349
- id: "growth",
5350
- status_category: "started",
5351
- label: "Growth",
5352
- description: "Scaling users and revenue. Product fit validated; focus is acquisition, retention, expansion.",
5353
- transitions_to: ["mature", "maintenance", "sunset"]
5354
- },
5355
- {
5356
- id: "mature",
5357
- status_category: "started",
5358
- label: "Mature",
5359
- description: "Stable. Growth levelled; investment weights toward optimisation, efficiency, and durability.",
5360
- transitions_to: ["maintenance", "sunset"]
5361
- },
5362
- {
5363
- id: "maintenance",
5364
- status_category: "started",
5365
- label: "Maintenance",
5366
- description: "Sustaining. Minimal investment keeps the product running for existing users. Can transition back to `mature` on a strategic revival.",
5367
- transitions_to: ["mature", "sunset"]
5368
- },
5369
- {
5370
- id: "sunset",
5371
- status_category: "completed",
5372
- label: "Sunset",
5373
- description: "Winding down or retired. Existing users are being migrated or offboarded.",
5374
- transitions_to: []
5375
- }
5376
- ]
5377
- };
5378
- var NEED_LIFECYCLE = {
5379
- entity_type: "need",
5380
- initial_phase: "raw",
5381
- terminal_phases: ["prioritized"],
5382
- phases: [
5383
- {
5384
- id: "raw",
5385
- status_category: "triage",
5386
- label: "Raw",
5387
- description: "Captured from an interview, observation, or support ticket. Articulation may be rough; may duplicate an existing need.",
5388
- transitions_to: ["validated"]
5389
- },
5390
- {
5391
- id: "validated",
5392
- status_category: "started",
5393
- label: "Validated",
5394
- description: "Corroborated by multiple data points or targeted research. The team agrees it is a real, recurring need.",
5395
- transitions_to: ["prioritized"]
5396
- },
5397
- {
5398
- id: "prioritized",
5399
- status_category: "completed",
5400
- label: "Prioritized",
5401
- description: "Ranked against other needs. Ready to inform opportunity definition and solution exploration.",
5402
- transitions_to: []
5403
- }
5404
- ]
5405
- };
5406
- var OPPORTUNITY_LIFECYCLE = {
5407
- entity_type: "opportunity",
5408
- initial_phase: "identified",
5409
- terminal_phases: [],
5410
- phases: [
5411
- {
5412
- id: "identified",
5413
- status_category: "triage",
5414
- label: "Identified",
5415
- description: "A problem worth solving has been named. Sources include needs, insights, market trends, or competitive signals. Sizing and validation come next.",
5416
- transitions_to: ["validated", "deferred"]
5417
- },
5418
- {
5419
- id: "validated",
5420
- status_category: "started",
5421
- label: "Validated",
5422
- description: "Research and evidence support pursuit. The problem is real, frequent, and painful enough to justify investment.",
5423
- transitions_to: ["deferred"]
5424
- },
5425
- {
5426
- id: "deferred",
5427
- status_category: "backlog",
5428
- label: "Deferred",
5429
- description: "Acknowledged but parked. Capacity, priority, or strategic timing reasons. Can be reopened.",
5430
- transitions_to: ["identified"]
5431
- }
5432
- ]
4418
+ ]
5433
4419
  };
5434
4420
  var SOLUTION_LIFECYCLE = {
5435
4421
  entity_type: "solution",
@@ -7303,533 +6289,1694 @@ var TEMPLATE_LIFECYCLES = [
7303
6289
  // directly above, and `monitor` and `alert_rule` are the same kind of thing:
7304
6290
  // something that runs continuously, can be paused, and is eventually retired.
7305
6291
  //
7306
- // NOT lifecycle-free. The `user_story` precedent for lifecycle-freedom is the
7307
- // Statement/Implementation split, where the paired `task` carries the
7308
- // lifecycle. There is no paired entity here, so lifecycle-freedom would leave
7309
- // `agent_skill` unable to say it is disabled and would entrench
7310
- // `AgentHookProperties.hook_status` (now @deprecated) as the permanent status
7311
- // axis, which is the *_status shadow Pattern D collapsed fourteen times.
7312
- fromTemplate("agent_skill", OPERATIONAL_TEMPLATE),
7313
- fromTemplate("agent_hook", OPERATIONAL_TEMPLATE),
7314
- fromTemplate("on_call_rotation", OPERATIONAL_TEMPLATE),
7315
- fromTemplate("security_audit", OPERATIONAL_TEMPLATE),
7316
- fromTemplate("penetration_test", OPERATIONAL_TEMPLATE),
7317
- fromTemplate("security_review", OPERATIONAL_TEMPLATE),
7318
- fromTemplate("qa_session", OPERATIONAL_TEMPLATE),
7319
- fromTemplate("a11y_audit", OPERATIONAL_TEMPLATE),
7320
- fromTemplate("test_environment", OPERATIONAL_TEMPLATE),
7321
- fromTemplate("workspace", OPERATIONAL_TEMPLATE),
7322
- fromTemplate("pricing_strategy", OPERATIONAL_TEMPLATE),
7323
- fromTemplate("discount_strategy", OPERATIONAL_TEMPLATE),
7324
- // ── Phase C: APPROVAL (proposed / reviewing / approved / rejected / deprecated)
7325
- fromTemplate("data_contract", APPROVAL_TEMPLATE),
7326
- fromTemplate("audit_log_policy", APPROVAL_TEMPLATE),
7327
- fromTemplate("access_policy", APPROVAL_TEMPLATE),
7328
- fromTemplate("privacy_policy", APPROVAL_TEMPLATE),
7329
- // ── Phase C: MATURITY (alpha / beta / ga / deprecated) ──────────────────────
7330
- // screen removed (UPG-690 Q3, 0.21.0): hand-authored SCREEN_LIFECYCLE below —
7331
- // the build-pipeline IS the real lifecycle, MATURITY was the mis-fit.
7332
- fromTemplate("design_system", MATURITY_TEMPLATE),
7333
- fromTemplate("design_component", MATURITY_TEMPLATE),
7334
- fromTemplate("design_pattern", MATURITY_TEMPLATE),
7335
- fromTemplate("brand_logo", MATURITY_TEMPLATE),
7336
- fromTemplate("certification", MATURITY_TEMPLATE),
7337
- fromTemplate("data_source", MATURITY_TEMPLATE),
7338
- fromTemplate("data_product", MATURITY_TEMPLATE),
7339
- fromTemplate("infrastructure_component", MATURITY_TEMPLATE),
7340
- // ── Phase C: WORK_ITEM (todo / in_progress / in_review / done) ──────────────
7341
- fromTemplate("agent_task", WORK_ITEM_TEMPLATE),
7342
- // agent_skill and agent_hook moved WORK_ITEM -> OPERATIONAL at 0.33.0; see the
7343
- // OPERATIONAL block below. agent_task stays: a task IS a work item.
7344
- // UPG-690 (0.21.0): task (exact match) + epic (todo→in_progress→done ⊆ WORK_ITEM,
7345
- // in_review becomes available — non-breaking widening) folded off hand-authored.
7346
- fromTemplate("task", WORK_ITEM_TEMPLATE),
7347
- fromTemplate("epic", WORK_ITEM_TEMPLATE),
7348
- // ── Phase B: RISK_ITEM (identified → assessed → mitigated/accepted/closed) ──
7349
- fromTemplate("threat", RISK_ITEM_TEMPLATE),
7350
- fromTemplate("risk", RISK_ITEM_TEMPLATE),
7351
- fromTemplate("compliance_requirement", RISK_ITEM_TEMPLATE),
7352
- // ── Phase B: SALES_DEAL (qualified → proposal → negotiation → won/lost) ─────
7353
- fromTemplate("deal", SALES_DEAL_TEMPLATE),
7354
- // ── STUDY (planned → running → analysing → complete / abandoned) ───────────
7355
- // UPG-690 (0.21.0): time-boxed investigations folded off bespoke singletons.
7356
- // `model_comparison` stays bespoke — its publish/archive tail is a weak fit.
7357
- fromTemplate("experiment", STUDY_TEMPLATE),
7358
- fromTemplate("experiment_run", STUDY_TEMPLATE),
7359
- fromTemplate("research_study", STUDY_TEMPLATE),
7360
- fromTemplate("design_sprint", STUDY_TEMPLATE),
7361
- fromTemplate("feasibility_study", STUDY_TEMPLATE),
7362
- fromTemplate("ai_experiment", STUDY_TEMPLATE),
7363
- fromTemplate("eval_run", STUDY_TEMPLATE),
7364
- // ── VALIDATION (untested → testing → validated / invalidated → archived) ───
7365
- // UPG-690 (0.21.0): claims-tested-to-a-verdict folded off bespoke singletons.
7366
- // `business_model` stays bespoke — its `pivoted` terminal is a strategic
7367
- // outcome, not a verdict.
7368
- fromTemplate("assumption", VALIDATION_TEMPLATE),
7369
- fromTemplate("prototype", VALIDATION_TEMPLATE),
7370
- fromTemplate("value_proposition", VALIDATION_TEMPLATE),
7371
- fromTemplate("hypothesis", VALIDATION_TEMPLATE),
7372
- // ── INCIDENT (open → triaged → in_progress → resolved / closed / wont_fix) ─
7373
- // UPG-690 (0.21.0): triage-to-resolution defect/ticket family folded off
7374
- // bespoke singletons. `incident` stays bespoke (richer SRE flow).
7375
- fromTemplate("support_ticket", INCIDENT_TEMPLATE),
7376
- fromTemplate("bug", INCIDENT_TEMPLATE),
7377
- fromTemplate("a11y_issue", INCIDENT_TEMPLATE),
7378
- fromTemplate("vulnerability", INCIDENT_TEMPLATE),
7379
- fromTemplate("hallucination_report", INCIDENT_TEMPLATE),
7380
- fromTemplate("technical_debt_item", INCIDENT_TEMPLATE),
7381
- fromTemplate("customer_feedback", INCIDENT_TEMPLATE)
7382
- ];
7383
- var FRAMEWORK_EXERCISE_LIFECYCLE = {
7384
- entity_type: "framework_exercise",
7385
- initial_phase: "draft",
7386
- terminal_phases: ["archived"],
7387
- phases: [
7388
- {
7389
- id: "draft",
7390
- status_category: "started",
7391
- label: "Draft",
7392
- description: "The exercise has been created and entities pulled into scope, but the framework's inputs have not all been filled in yet.",
7393
- transitions_to: ["active", "archived"]
7394
- },
7395
- {
7396
- id: "active",
7397
- status_category: "started",
7398
- label: "Active",
7399
- description: "The current, authoritative run of its framework. Its include edges carry the live results consumers read, rank, and render by.",
7400
- transitions_to: ["archived"]
7401
- },
7402
- {
7403
- id: "archived",
7404
- status_category: "completed",
7405
- label: "Archived",
7406
- description: "A past run, retained for provenance. Still queryable but superseded by a newer exercise and hidden from default views. Can be revived to active.",
7407
- transitions_to: ["active"]
7408
- }
7409
- ]
7410
- };
7411
- var COMPOSITION_LIFECYCLE = {
7412
- entity_type: "composition",
7413
- initial_phase: "draft",
7414
- terminal_phases: ["archived"],
7415
- phases: [
7416
- {
7417
- id: "draft",
7418
- status_category: "started",
7419
- label: "Draft",
7420
- description: "Being arranged. It has a slug reserved and members placed, but has never been published, so nothing resolves at its address yet.",
7421
- transitions_to: ["published", "archived"]
7422
- },
7423
- {
7424
- id: "published",
7425
- status_category: "completed",
7426
- label: "Published",
7427
- description: "Live at its slug and linkable. Republishing the same slug stays in this phase and bumps the revision rather than moving the composition.",
7428
- transitions_to: ["archived", "draft"]
7429
- },
7430
- {
7431
- id: "archived",
7432
- status_category: "completed",
7433
- label: "Archived",
7434
- description: "Withdrawn from the published set but retained, so an existing link resolves to a view marked out of date rather than to nothing. Can be republished.",
7435
- transitions_to: ["published", "draft"]
7436
- }
7437
- ]
7438
- };
7439
- var SPECIFICATION_LIFECYCLE = {
7440
- entity_type: "specification",
7441
- initial_phase: "draft",
7442
- terminal_phases: ["superseded"],
7443
- phases: [
7444
- {
7445
- id: "draft",
7446
- status_category: "started",
7447
- label: "Draft",
7448
- description: "Being authored. Not yet adopted as the canonical specification.",
7449
- transitions_to: ["active", "superseded"]
7450
- },
7451
- {
7452
- id: "active",
7453
- status_category: "started",
7454
- label: "Active",
7455
- description: "In force. The current authoritative version products implement and conform to.",
7456
- transitions_to: ["deprecated", "superseded"]
7457
- },
7458
- {
7459
- id: "deprecated",
7460
- status_category: "started",
7461
- label: "Deprecated",
7462
- description: "Still valid but no longer recommended; a successor exists or is imminent.",
7463
- transitions_to: ["superseded"]
7464
- },
7465
- {
7466
- id: "superseded",
7467
- status_category: "completed",
7468
- label: "Superseded",
7469
- description: "Replaced by a newer specification. Retained for historical reference.",
7470
- transitions_to: []
7471
- }
7472
- ]
7473
- };
7474
- var PLANNING_CYCLE_LIFECYCLE = {
7475
- entity_type: "planning_cycle",
7476
- initial_phase: "planned",
7477
- terminal_phases: ["closed"],
7478
- phases: [
7479
- { id: "planned", status_category: "unstarted", label: "Planned", description: "The cycle is being shaped: dates set, goal written, work scheduled into it. It has not started yet.", transitions_to: ["active"] },
7480
- { id: "active", status_category: "started", label: "Active", description: "The team is inside the interval; work is flowing through it.", transitions_to: ["closed"] },
7481
- { id: "closed", status_category: "completed", label: "Closed", description: "The interval has ended. Scheduled work has shipped, carried over, or been dropped. May reopen if the team extends or reruns it.", transitions_to: ["active"] }
7482
- ]
7483
- };
7484
- var UPG_LIFECYCLES = [
7485
- // Product (root)
7486
- PRODUCT_LIFECYCLE,
7487
- // Foundations (0.9.12)
7488
- SPECIFICATION_LIFECYCLE,
7489
- // Discovery & Validation
7490
- NEED_LIFECYCLE,
7491
- OPPORTUNITY_LIFECYCLE,
7492
- SOLUTION_LIFECYCLE,
7493
- EXPERIMENT_PLAN_LIFECYCLE,
7494
- RESEARCH_PLAN_LIFECYCLE,
7495
- FEEDBACK_PROGRAM_LIFECYCLE,
7496
- FEATURE_REQUEST_LIFECYCLE,
7497
- BETA_PROGRAM_LIFECYCLE,
7498
- USER_ADVISORY_BOARD_LIFECYCLE,
7499
- // Strategy & Product Specification
7500
- OUTCOME_LIFECYCLE,
7501
- VISION_LIFECYCLE,
7502
- MISSION_LIFECYCLE,
7503
- CAPABILITY_LIFECYCLE,
7504
- OBJECTIVE_LIFECYCLE,
7505
- KEY_RESULT_LIFECYCLE,
7506
- STRATEGIC_THEME_LIFECYCLE,
7507
- INITIATIVE_LIFECYCLE,
7508
- STRATEGIC_PILLAR_LIFECYCLE,
7509
- STRATEGIC_QUESTION_LIFECYCLE,
7510
- FEATURE_AREA_LIFECYCLE,
7511
- FEATURE_LIFECYCLE,
7512
- // user_story is lifecycle-free (declared in UPG_LIFECYCLE_FREE_TYPES below).
7513
- // story_task lifecycle removed v0.4.0; collapsed into task. task + epic are
7514
- // now WORK_ITEM-template-derived (UPG-690, 0.21.0) — see TEMPLATE_LIFECYCLES.
7515
- RELEASE_LIFECYCLE,
7516
- ROADMAP_ITEM_LIFECYCLE,
7517
- PLANNING_CYCLE_LIFECYCLE,
7518
- IP_ASSET_LIFECYCLE,
7519
- CONTRACT_LIFECYCLE,
7520
- DESIGN_CONCEPT_LIFECYCLE,
7521
- BRAND_IDENTITY_LIFECYCLE,
7522
- SCREEN_LIFECYCLE,
7523
- SURFACE_LIFECYCLE,
7524
- // Engineering & Operations
7525
- SERVICE_LIFECYCLE,
7526
- DEPLOYMENT_LIFECYCLE,
7527
- FEATURE_FLAG_LIFECYCLE,
7528
- INVESTIGATION_LIFECYCLE,
7529
- EXTERNAL_API_LIFECYCLE,
7530
- DATABASE_SCHEMA_LIFECYCLE,
7531
- INCIDENT_LIFECYCLE,
7532
- SECURITY_CONTROL_LIFECYCLE,
7533
- DATA_PIPELINE_LIFECYCLE,
7534
- AI_MODEL_LIFECYCLE,
7535
- WORKFLOW_RUN_LIFECYCLE,
7536
- AGENT_DEFINITION_LIFECYCLE,
7537
- AGENT_SESSION_LIFECYCLE,
7538
- REVIEW_GATE_LIFECYCLE,
7539
- TEST_SUITE_LIFECYCLE,
7540
- TEST_CASE_LIFECYCLE,
7541
- // ── Phase B hand-authored ─────────────────────────────────────────
7542
- // Research + Validation
7543
- INSIGHT_LIFECYCLE,
7544
- RESEARCH_QUESTION_LIFECYCLE,
7545
- INTERVIEW_GUIDE_LIFECYCLE,
7546
- TEST_PLAN_LIFECYCLE,
7547
- // AI workflow
7548
- AI_DATASET_LIFECYCLE,
7549
- AI_GUARDRAIL_LIFECYCLE,
7550
- MODEL_COMPARISON_LIFECYCLE,
7551
- PROMPT_VERSION_LIFECYCLE,
7552
- EVAL_BENCHMARK_LIFECYCLE,
7553
- // Business Model
7554
- BUSINESS_MODEL_LIFECYCLE,
7555
- REVENUE_STREAM_LIFECYCLE,
7556
- // Customer Success
7557
- CUSTOMER_HEALTH_SCORE_LIFECYCLE,
7558
- PLAYBOOK_LIFECYCLE,
7559
- // Team & Organisation
7560
- TEAM_OKR_LIFECYCLE,
7561
- RETROSPECTIVE_LIFECYCLE,
7562
- DEPENDENCY_LIFECYCLE,
7563
- ROLE_LIFECYCLE,
7564
- CAPACITY_PLAN_LIFECYCLE,
7565
- // Product & Growth
7566
- VARIANT_LIFECYCLE,
7567
- GROWTH_CAMPAIGN_LIFECYCLE,
7568
- // Workspace
7569
- FRAMEWORK_EXERCISE_LIFECYCLE,
7570
- COMPOSITION_LIFECYCLE,
7571
- // ── Template-generated lifecycles ─────────────────────────────────
7572
- ...TEMPLATE_LIFECYCLES
7573
- ];
7574
- function getLifecycleForType(entityType) {
7575
- return UPG_LIFECYCLES.find((l) => l.entity_type === entityType);
7576
- }
7577
- var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
7578
- // ── Foundations (3 of 4): a primitive either exists or is deprecated; no
7579
- // phase ladder (like metric). `operating_lifecycle`/`operating_stage` are
7580
- // static ordered reference data (the loop is a `cyclic` flag, not a state
7581
- // machine). `specification` carries a lifecycle. ──
7582
- "primitive",
7583
- "operating_lifecycle",
7584
- "operating_stage",
7585
- // ── User (5): personas and JTBD atoms are reference data ──────────────────
7586
- "persona",
7587
- "job",
7588
- "job_step",
7589
- "desired_outcome",
7590
- "switching_cost",
7591
- // ── Market Intelligence (7 of 8): competitor landscape is reference data;
7592
- // competitor_signal is an append-only dated event (no phase ladder);
7593
- // competitive_analysis shipped PUBLISHING lifecycle in Phase C;
7594
- // classification_axis and classification_value are reference taxonomy
7595
- // atoms with no state machine. ─
7596
- "competitor",
7597
- "competitor_feature",
7598
- "competitor_signal",
7599
- "market_trend",
7600
- "market_segment",
7601
- "classification_axis",
7602
- "classification_value",
7603
- // ── Product Specification (1): a configuration_axis is a structural
7604
- // DECLARATION (a lever and its closed value set), not a thing that
7605
- // progresses through states. Same reading as classification_axis above.
7606
- "configuration_axis",
7607
- // ── User Research (5 of 8): participants, quotes, observations,
7608
- // clusters, survey responses are immutable data points ────────────────
7609
- "participant",
7610
- "observation",
7611
- "quote",
7612
- "affinity_cluster",
7613
- "survey_response",
7614
- // ── UX Design (3 of 8): journey structural atoms ──────────────────────────
7615
- "journey_step",
7616
- "journey_phase",
7617
- "journey_action",
7618
- "screen_state",
7619
- // ── Design System (3 of 7): tokens and annotations are atomic values ────
7620
- "design_token",
7621
- "annotation",
7622
- // ── Brand (4 of 6): colour/typography/imagery/voice are reference atoms ──
7623
- "brand_colour",
7624
- "brand_typography",
7625
- "brand_imagery",
7626
- // ── Product Specification (4 of 5): criteria, grouping, historical, +
7627
- // user_story (the templated promise is a stable design artefact; the paired
7628
- // task carries the lifecycle). Re-canon story_statement → user_story at
7629
- // v0.7.0/UPG-571. ─
7630
- "acceptance_criterion",
7631
- "changelog",
7632
- "roadmap_theme",
7633
- "user_story",
7634
- // ── Strategy: metric is a measurement definition; metric_quality_assessment
7635
- // is a point-in-time snapshot; value_stream is a mapped flow;
7636
- // constraint carries its own `constraint_status` enum (binding/advisory/
7637
- // lifted) as a property; base-node `status` not used.
7638
- // vision/mission/capability/outcome get lifecycles in Phase B or C ─────
7639
- "metric",
7640
- "metric_quality_assessment",
7641
- "value_stream",
7642
- "constraint",
7643
- // ── Engineering (15 of 17): DDD modelling primitives + infra references.
7644
- // root_cause / symptom / fix are investigation findings, not workflows
7645
- // (investigation itself has a lifecycle) ────────────────────────────────
7646
- "aggregate",
7647
- "domain_entity",
7648
- "value_object",
7649
- "command",
7650
- "read_model",
7651
- "domain_event",
7652
- "bounded_context",
7653
- "api_endpoint",
7654
- "queue_topic",
7655
- "build_artifact",
7656
- "code_repository",
7657
- "library_dependency",
7658
- "integration_pattern",
7659
- "data_flow",
7660
- "root_cause",
7661
- "symptom",
7662
- "fix",
7663
- // ── Business Model (6 of 9): BMC cells are structural frames ────────────
7664
- "cost_structure",
7665
- "unit_economics",
7666
- "key_resource",
7667
- "key_activity",
7668
- "customer_relationship",
7669
- "distribution_channel",
7670
- // ── Go-To-Market (8 of 13): positioning/ICP/territory/sales_motion are
7671
- // configuration; objection/rebuttal/proof_point are catalog entries ───
7672
- "ideal_customer_profile",
7673
- "positioning",
7674
- "sales_motion",
7675
- "territory",
7676
- "objection",
7677
- "rebuttal",
7678
- "proof_point",
7679
- // ── Team & Organisation (7 of 11): org structural atoms ───────────────────
7680
- "team",
7681
- "stakeholder",
6292
+ // NOT lifecycle-free. The `user_story` precedent for lifecycle-freedom is the
6293
+ // Statement/Implementation split, where the paired `task` carries the
6294
+ // lifecycle. There is no paired entity here, so lifecycle-freedom would leave
6295
+ // `agent_skill` unable to say it is disabled and would entrench
6296
+ // `AgentHookProperties.hook_status` (now @deprecated) as the permanent status
6297
+ // axis, which is the *_status shadow Pattern D collapsed fourteen times.
6298
+ fromTemplate("agent_skill", OPERATIONAL_TEMPLATE),
6299
+ fromTemplate("agent_hook", OPERATIONAL_TEMPLATE),
6300
+ fromTemplate("on_call_rotation", OPERATIONAL_TEMPLATE),
6301
+ fromTemplate("security_audit", OPERATIONAL_TEMPLATE),
6302
+ fromTemplate("penetration_test", OPERATIONAL_TEMPLATE),
6303
+ fromTemplate("security_review", OPERATIONAL_TEMPLATE),
6304
+ fromTemplate("qa_session", OPERATIONAL_TEMPLATE),
6305
+ fromTemplate("a11y_audit", OPERATIONAL_TEMPLATE),
6306
+ fromTemplate("test_environment", OPERATIONAL_TEMPLATE),
6307
+ fromTemplate("workspace", OPERATIONAL_TEMPLATE),
6308
+ fromTemplate("pricing_strategy", OPERATIONAL_TEMPLATE),
6309
+ fromTemplate("discount_strategy", OPERATIONAL_TEMPLATE),
6310
+ // ── Phase C: APPROVAL (proposed / reviewing / approved / rejected / deprecated)
6311
+ fromTemplate("data_contract", APPROVAL_TEMPLATE),
6312
+ fromTemplate("audit_log_policy", APPROVAL_TEMPLATE),
6313
+ fromTemplate("access_policy", APPROVAL_TEMPLATE),
6314
+ fromTemplate("privacy_policy", APPROVAL_TEMPLATE),
6315
+ // ── Phase C: MATURITY (alpha / beta / ga / deprecated) ──────────────────────
6316
+ // screen removed (UPG-690 Q3, 0.21.0): hand-authored SCREEN_LIFECYCLE below —
6317
+ // the build-pipeline IS the real lifecycle, MATURITY was the mis-fit.
6318
+ fromTemplate("design_system", MATURITY_TEMPLATE),
6319
+ fromTemplate("design_component", MATURITY_TEMPLATE),
6320
+ fromTemplate("design_pattern", MATURITY_TEMPLATE),
6321
+ fromTemplate("brand_logo", MATURITY_TEMPLATE),
6322
+ fromTemplate("certification", MATURITY_TEMPLATE),
6323
+ fromTemplate("data_source", MATURITY_TEMPLATE),
6324
+ fromTemplate("data_product", MATURITY_TEMPLATE),
6325
+ fromTemplate("infrastructure_component", MATURITY_TEMPLATE),
6326
+ // ── Phase C: WORK_ITEM (todo / in_progress / in_review / done) ──────────────
6327
+ fromTemplate("agent_task", WORK_ITEM_TEMPLATE),
6328
+ // agent_skill and agent_hook moved WORK_ITEM -> OPERATIONAL at 0.33.0; see the
6329
+ // OPERATIONAL block below. agent_task stays: a task IS a work item.
6330
+ // UPG-690 (0.21.0): task (exact match) + epic (todo→in_progress→done ⊆ WORK_ITEM,
6331
+ // in_review becomes available — non-breaking widening) folded off hand-authored.
6332
+ fromTemplate("task", WORK_ITEM_TEMPLATE),
6333
+ fromTemplate("epic", WORK_ITEM_TEMPLATE),
6334
+ // ── Phase B: RISK_ITEM (identified → assessed → mitigated/accepted/closed) ──
6335
+ fromTemplate("threat", RISK_ITEM_TEMPLATE),
6336
+ fromTemplate("risk", RISK_ITEM_TEMPLATE),
6337
+ fromTemplate("compliance_requirement", RISK_ITEM_TEMPLATE),
6338
+ // ── Phase B: SALES_DEAL (qualified → proposal → negotiation → won/lost) ─────
6339
+ fromTemplate("deal", SALES_DEAL_TEMPLATE),
6340
+ // ── STUDY (planned → running → analysing → complete / abandoned) ───────────
6341
+ // UPG-690 (0.21.0): time-boxed investigations folded off bespoke singletons.
6342
+ // `model_comparison` stays bespoke — its publish/archive tail is a weak fit.
6343
+ fromTemplate("experiment", STUDY_TEMPLATE),
6344
+ fromTemplate("experiment_run", STUDY_TEMPLATE),
6345
+ fromTemplate("research_study", STUDY_TEMPLATE),
6346
+ fromTemplate("design_sprint", STUDY_TEMPLATE),
6347
+ fromTemplate("feasibility_study", STUDY_TEMPLATE),
6348
+ fromTemplate("ai_experiment", STUDY_TEMPLATE),
6349
+ fromTemplate("eval_run", STUDY_TEMPLATE),
6350
+ // ── VALIDATION (untested → testing → validated / invalidated → archived) ───
6351
+ // UPG-690 (0.21.0): claims-tested-to-a-verdict folded off bespoke singletons.
6352
+ // `business_model` stays bespoke — its `pivoted` terminal is a strategic
6353
+ // outcome, not a verdict.
6354
+ fromTemplate("assumption", VALIDATION_TEMPLATE),
6355
+ fromTemplate("prototype", VALIDATION_TEMPLATE),
6356
+ fromTemplate("value_proposition", VALIDATION_TEMPLATE),
6357
+ fromTemplate("hypothesis", VALIDATION_TEMPLATE),
6358
+ // ── INCIDENT (open → triaged → in_progress → resolved / closed / wont_fix) ─
6359
+ // UPG-690 (0.21.0): triage-to-resolution defect/ticket family folded off
6360
+ // bespoke singletons. `incident` stays bespoke (richer SRE flow).
6361
+ fromTemplate("support_ticket", INCIDENT_TEMPLATE),
6362
+ fromTemplate("bug", INCIDENT_TEMPLATE),
6363
+ fromTemplate("a11y_issue", INCIDENT_TEMPLATE),
6364
+ fromTemplate("vulnerability", INCIDENT_TEMPLATE),
6365
+ fromTemplate("hallucination_report", INCIDENT_TEMPLATE),
6366
+ fromTemplate("technical_debt_item", INCIDENT_TEMPLATE),
6367
+ fromTemplate("customer_feedback", INCIDENT_TEMPLATE)
6368
+ ];
6369
+ var FRAMEWORK_EXERCISE_LIFECYCLE = {
6370
+ entity_type: "framework_exercise",
6371
+ initial_phase: "draft",
6372
+ terminal_phases: ["archived"],
6373
+ phases: [
6374
+ {
6375
+ id: "draft",
6376
+ status_category: "started",
6377
+ label: "Draft",
6378
+ description: "The exercise has been created and entities pulled into scope, but the framework's inputs have not all been filled in yet.",
6379
+ transitions_to: ["active", "archived"]
6380
+ },
6381
+ {
6382
+ id: "active",
6383
+ status_category: "started",
6384
+ label: "Active",
6385
+ description: "The current, authoritative run of its framework. Its include edges carry the live results consumers read, rank, and render by.",
6386
+ transitions_to: ["archived"]
6387
+ },
6388
+ {
6389
+ id: "archived",
6390
+ status_category: "completed",
6391
+ label: "Archived",
6392
+ description: "A past run, retained for provenance. Still queryable but superseded by a newer exercise and hidden from default views. Can be revived to active.",
6393
+ transitions_to: ["active"]
6394
+ }
6395
+ ]
6396
+ };
6397
+ var COMPOSITION_LIFECYCLE = {
6398
+ entity_type: "composition",
6399
+ initial_phase: "draft",
6400
+ terminal_phases: ["archived"],
6401
+ phases: [
6402
+ {
6403
+ id: "draft",
6404
+ status_category: "started",
6405
+ label: "Draft",
6406
+ description: "Being arranged. It has a slug reserved and members placed, but has never been published, so nothing resolves at its address yet.",
6407
+ transitions_to: ["published", "archived"]
6408
+ },
6409
+ {
6410
+ id: "published",
6411
+ status_category: "completed",
6412
+ label: "Published",
6413
+ description: "Live at its slug and linkable. Republishing the same slug stays in this phase and bumps the revision rather than moving the composition.",
6414
+ transitions_to: ["archived", "draft"]
6415
+ },
6416
+ {
6417
+ id: "archived",
6418
+ status_category: "completed",
6419
+ label: "Archived",
6420
+ description: "Withdrawn from the published set but retained, so an existing link resolves to a view marked out of date rather than to nothing. Can be republished.",
6421
+ transitions_to: ["published", "draft"]
6422
+ }
6423
+ ]
6424
+ };
6425
+ var SPECIFICATION_LIFECYCLE = {
6426
+ entity_type: "specification",
6427
+ initial_phase: "draft",
6428
+ terminal_phases: ["superseded"],
6429
+ phases: [
6430
+ {
6431
+ id: "draft",
6432
+ status_category: "started",
6433
+ label: "Draft",
6434
+ description: "Being authored. Not yet adopted as the canonical specification.",
6435
+ transitions_to: ["active", "superseded"]
6436
+ },
6437
+ {
6438
+ id: "active",
6439
+ status_category: "started",
6440
+ label: "Active",
6441
+ description: "In force. The current authoritative version products implement and conform to.",
6442
+ transitions_to: ["deprecated", "superseded"]
6443
+ },
6444
+ {
6445
+ id: "deprecated",
6446
+ status_category: "started",
6447
+ label: "Deprecated",
6448
+ description: "Still valid but no longer recommended; a successor exists or is imminent.",
6449
+ transitions_to: ["superseded"]
6450
+ },
6451
+ {
6452
+ id: "superseded",
6453
+ status_category: "completed",
6454
+ label: "Superseded",
6455
+ description: "Replaced by a newer specification. Retained for historical reference.",
6456
+ transitions_to: []
6457
+ }
6458
+ ]
6459
+ };
6460
+ var PLANNING_CYCLE_LIFECYCLE = {
6461
+ entity_type: "planning_cycle",
6462
+ initial_phase: "planned",
6463
+ terminal_phases: ["closed"],
6464
+ phases: [
6465
+ { id: "planned", status_category: "unstarted", label: "Planned", description: "The cycle is being shaped: dates set, goal written, work scheduled into it. It has not started yet.", transitions_to: ["active"] },
6466
+ { id: "active", status_category: "started", label: "Active", description: "The team is inside the interval; work is flowing through it.", transitions_to: ["closed"] },
6467
+ { id: "closed", status_category: "completed", label: "Closed", description: "The interval has ended. Scheduled work has shipped, carried over, or been dropped. May reopen if the team extends or reruns it.", transitions_to: ["active"] }
6468
+ ]
6469
+ };
6470
+ var UPG_LIFECYCLES = [
6471
+ // Product (root)
6472
+ PRODUCT_LIFECYCLE,
6473
+ // Foundations (0.9.12)
6474
+ SPECIFICATION_LIFECYCLE,
6475
+ // Discovery & Validation
6476
+ NEED_LIFECYCLE,
6477
+ OPPORTUNITY_LIFECYCLE,
6478
+ SOLUTION_LIFECYCLE,
6479
+ EXPERIMENT_PLAN_LIFECYCLE,
6480
+ RESEARCH_PLAN_LIFECYCLE,
6481
+ FEEDBACK_PROGRAM_LIFECYCLE,
6482
+ FEATURE_REQUEST_LIFECYCLE,
6483
+ BETA_PROGRAM_LIFECYCLE,
6484
+ USER_ADVISORY_BOARD_LIFECYCLE,
6485
+ // Strategy & Product Specification
6486
+ OUTCOME_LIFECYCLE,
6487
+ VISION_LIFECYCLE,
6488
+ MISSION_LIFECYCLE,
6489
+ CAPABILITY_LIFECYCLE,
6490
+ OBJECTIVE_LIFECYCLE,
6491
+ KEY_RESULT_LIFECYCLE,
6492
+ STRATEGIC_THEME_LIFECYCLE,
6493
+ INITIATIVE_LIFECYCLE,
6494
+ STRATEGIC_PILLAR_LIFECYCLE,
6495
+ STRATEGIC_QUESTION_LIFECYCLE,
6496
+ FEATURE_AREA_LIFECYCLE,
6497
+ FEATURE_LIFECYCLE,
6498
+ // user_story is lifecycle-free (declared in UPG_LIFECYCLE_FREE_TYPES below).
6499
+ // story_task lifecycle removed v0.4.0; collapsed into task. task + epic are
6500
+ // now WORK_ITEM-template-derived (UPG-690, 0.21.0) — see TEMPLATE_LIFECYCLES.
6501
+ RELEASE_LIFECYCLE,
6502
+ ROADMAP_ITEM_LIFECYCLE,
6503
+ PLANNING_CYCLE_LIFECYCLE,
6504
+ IP_ASSET_LIFECYCLE,
6505
+ CONTRACT_LIFECYCLE,
6506
+ DESIGN_CONCEPT_LIFECYCLE,
6507
+ BRAND_IDENTITY_LIFECYCLE,
6508
+ SCREEN_LIFECYCLE,
6509
+ SURFACE_LIFECYCLE,
6510
+ // Engineering & Operations
6511
+ SERVICE_LIFECYCLE,
6512
+ DEPLOYMENT_LIFECYCLE,
6513
+ FEATURE_FLAG_LIFECYCLE,
6514
+ INVESTIGATION_LIFECYCLE,
6515
+ EXTERNAL_API_LIFECYCLE,
6516
+ DATABASE_SCHEMA_LIFECYCLE,
6517
+ INCIDENT_LIFECYCLE,
6518
+ SECURITY_CONTROL_LIFECYCLE,
6519
+ DATA_PIPELINE_LIFECYCLE,
6520
+ AI_MODEL_LIFECYCLE,
6521
+ WORKFLOW_RUN_LIFECYCLE,
6522
+ AGENT_DEFINITION_LIFECYCLE,
6523
+ AGENT_SESSION_LIFECYCLE,
6524
+ REVIEW_GATE_LIFECYCLE,
6525
+ TEST_SUITE_LIFECYCLE,
6526
+ TEST_CASE_LIFECYCLE,
6527
+ // ── Phase B hand-authored ─────────────────────────────────────────
6528
+ // Research + Validation
6529
+ INSIGHT_LIFECYCLE,
6530
+ RESEARCH_QUESTION_LIFECYCLE,
6531
+ INTERVIEW_GUIDE_LIFECYCLE,
6532
+ TEST_PLAN_LIFECYCLE,
6533
+ // AI workflow
6534
+ AI_DATASET_LIFECYCLE,
6535
+ AI_GUARDRAIL_LIFECYCLE,
6536
+ MODEL_COMPARISON_LIFECYCLE,
6537
+ PROMPT_VERSION_LIFECYCLE,
6538
+ EVAL_BENCHMARK_LIFECYCLE,
6539
+ // Business Model
6540
+ BUSINESS_MODEL_LIFECYCLE,
6541
+ REVENUE_STREAM_LIFECYCLE,
6542
+ // Customer Success
6543
+ CUSTOMER_HEALTH_SCORE_LIFECYCLE,
6544
+ PLAYBOOK_LIFECYCLE,
6545
+ // Team & Organisation
6546
+ TEAM_OKR_LIFECYCLE,
6547
+ RETROSPECTIVE_LIFECYCLE,
6548
+ DEPENDENCY_LIFECYCLE,
6549
+ ROLE_LIFECYCLE,
6550
+ CAPACITY_PLAN_LIFECYCLE,
6551
+ // Product & Growth
6552
+ VARIANT_LIFECYCLE,
6553
+ GROWTH_CAMPAIGN_LIFECYCLE,
6554
+ // Workspace
6555
+ FRAMEWORK_EXERCISE_LIFECYCLE,
6556
+ COMPOSITION_LIFECYCLE,
6557
+ // ── Template-generated lifecycles ─────────────────────────────────
6558
+ ...TEMPLATE_LIFECYCLES
6559
+ ];
6560
+ function getLifecycleForType(entityType) {
6561
+ return UPG_LIFECYCLES.find((l) => l.entity_type === entityType);
6562
+ }
6563
+ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
6564
+ // ── Foundations (3 of 4): a primitive either exists or is deprecated; no
6565
+ // phase ladder (like metric). `operating_lifecycle`/`operating_stage` are
6566
+ // static ordered reference data (the loop is a `cyclic` flag, not a state
6567
+ // machine). `specification` carries a lifecycle. ──
6568
+ "primitive",
6569
+ "operating_lifecycle",
6570
+ "operating_stage",
6571
+ // ── User (5): personas and JTBD atoms are reference data ──────────────────
6572
+ "persona",
6573
+ "job",
6574
+ "job_step",
6575
+ "desired_outcome",
6576
+ "switching_cost",
6577
+ // ── Market Intelligence (7 of 8): competitor landscape is reference data;
6578
+ // competitor_signal is an append-only dated event (no phase ladder);
6579
+ // competitive_analysis shipped PUBLISHING lifecycle in Phase C;
6580
+ // classification_axis and classification_value are reference taxonomy
6581
+ // atoms with no state machine. ─
6582
+ "competitor",
6583
+ "competitor_feature",
6584
+ "competitor_signal",
6585
+ "market_trend",
6586
+ "market_segment",
6587
+ "classification_axis",
6588
+ "classification_value",
6589
+ // ── Product Specification (1): a configuration_axis is a structural
6590
+ // DECLARATION (a lever and its closed value set), not a thing that
6591
+ // progresses through states. Same reading as classification_axis above.
6592
+ "configuration_axis",
6593
+ // ── User Research (5 of 8): participants, quotes, observations,
6594
+ // clusters, survey responses are immutable data points ────────────────
6595
+ "participant",
6596
+ "observation",
6597
+ "quote",
6598
+ "affinity_cluster",
6599
+ "survey_response",
6600
+ // ── UX Design (3 of 8): journey structural atoms ──────────────────────────
6601
+ "journey_step",
6602
+ "journey_phase",
6603
+ "journey_action",
6604
+ "screen_state",
6605
+ // ── Design System (3 of 7): tokens and annotations are atomic values ────
6606
+ "design_token",
6607
+ "annotation",
6608
+ // ── Brand (4 of 6): colour/typography/imagery/voice are reference atoms ──
6609
+ "brand_colour",
6610
+ "brand_typography",
6611
+ "brand_imagery",
6612
+ // ── Product Specification (4 of 5): criteria, grouping, historical, +
6613
+ // user_story (the templated promise is a stable design artefact; the paired
6614
+ // task carries the lifecycle). Re-canon story_statement → user_story at
6615
+ // v0.7.0/UPG-571. ─
6616
+ "acceptance_criterion",
6617
+ "changelog",
6618
+ "roadmap_theme",
6619
+ "user_story",
6620
+ // ── Strategy: metric is a measurement definition; metric_quality_assessment
6621
+ // is a point-in-time snapshot; value_stream is a mapped flow;
6622
+ // constraint carries its own `constraint_status` enum (binding/advisory/
6623
+ // lifted) as a property; base-node `status` not used.
6624
+ // vision/mission/capability/outcome get lifecycles in Phase B or C ─────
6625
+ "metric",
6626
+ "metric_quality_assessment",
6627
+ "value_stream",
6628
+ "constraint",
6629
+ // ── Engineering (15 of 17): DDD modelling primitives + infra references.
6630
+ // root_cause / symptom / fix are investigation findings, not workflows
6631
+ // (investigation itself has a lifecycle) ────────────────────────────────
6632
+ "aggregate",
6633
+ "domain_entity",
6634
+ "value_object",
6635
+ "command",
6636
+ "read_model",
6637
+ "domain_event",
6638
+ "bounded_context",
6639
+ "api_endpoint",
6640
+ "queue_topic",
6641
+ "build_artifact",
6642
+ "code_repository",
6643
+ "library_dependency",
6644
+ "integration_pattern",
6645
+ "data_flow",
6646
+ "root_cause",
6647
+ "symptom",
6648
+ "fix",
6649
+ // ── Business Model (6 of 9): BMC cells are structural frames ────────────
6650
+ "cost_structure",
6651
+ "unit_economics",
6652
+ "key_resource",
6653
+ "key_activity",
6654
+ "customer_relationship",
6655
+ "distribution_channel",
6656
+ // ── Go-To-Market (8 of 13): positioning/ICP/territory/sales_motion are
6657
+ // configuration; objection/rebuttal/proof_point are catalog entries ───
6658
+ "ideal_customer_profile",
6659
+ "positioning",
6660
+ "sales_motion",
6661
+ "territory",
6662
+ "objection",
6663
+ "rebuttal",
6664
+ "proof_point",
6665
+ // ── Team & Organisation (7 of 11): org structural atoms ───────────────────
6666
+ "team",
6667
+ "stakeholder",
6668
+ "person",
6669
+ "department",
6670
+ "skill",
6671
+ "ceremony",
6672
+ // ── Data & Analytics (4 of 10): lineage and domain containers, plus
6673
+ // atomic definitions ────────────────────────────────────────────────────
6674
+ "data_lineage",
6675
+ "data_domain",
6676
+ "data_quality_rule",
6677
+ // ── AI (3 of 10): traces and cost trackers are append-only; model
6678
+ // comparison snapshots are immutable ───────────────────────────────────
6679
+ "ai_cost_tracker",
6680
+ "ai_trace",
6681
+ // ── Automation (2 of 6): approvals and artifacts are ledger entries ──────
6682
+ "approval_record",
6683
+ "workflow_artifact",
6684
+ // ── Growth (7 of 9): funnels and attribution are structural definitions;
6685
+ // cohorts/segments are computed snapshots ────────────────────────────
6686
+ "funnel",
6687
+ "funnel_step",
6688
+ "acquisition_channel",
6689
+ "cohort",
6690
+ "behavioral_segment",
6691
+ "growth_loop",
6692
+ "attribution_model",
6693
+ // ── Sales (5 of 6): pipeline structure + accounts/contacts + forecast
6694
+ // snapshots. Only `deal` has a lifecycle (Phase B SALES_DEAL template) ─
6695
+ "account",
6696
+ "contact",
6697
+ "pipeline_sales",
6698
+ "pipeline_stage",
6699
+ "forecast",
6700
+ // ── Customer Success (5 of 9): journey structure + derived values ────────
6701
+ "customer_journey_stage",
6702
+ "touchpoint",
6703
+ "churn_reason",
6704
+ "service_level_agreement",
6705
+ // ── DevOps (5 of 10): indicators and budgets are measurement references;
6706
+ // strategies and pipelines are configuration ──────────────────────────
6707
+ "service_level_indicator",
6708
+ "service_level_objective",
6709
+ "error_budget",
6710
+ "ci_pipeline",
6711
+ "release_strategy",
6712
+ // ── Security (1 of 5): data classification is a reference label ──────────
6713
+ "data_classification",
6714
+ // ── Accessibility (2 of 4): standards and annotations ─────────────────────
6715
+ "a11y_standard",
6716
+ "a11y_annotation",
6717
+ // ── Testing (3 of 5): regression tests are reference definitions; test
6718
+ // results and coverage reports are immutable records ───────────────────
6719
+ "regression_test",
6720
+ "test_coverage_report",
6721
+ "test_result",
6722
+ // ── Customer Feedback (2 of 3): themes and votes are atomic data points ──
6723
+ "feedback_theme",
6724
+ "feedback_vote",
6725
+ // ── Pricing (2 of 4): trial and paywall are configuration ─────────────────
6726
+ "trial_config",
6727
+ "paywall",
6728
+ // ── Content (2 of 5): calendars and themes are planning configuration ────
6729
+ "content_calendar",
6730
+ "content_theme",
6731
+ // ── Legal (2 of 3): legal_entity is an org; contract_clause is an atom
6732
+ // of a contract (which has its own lifecycle) ───────────────────────────
6733
+ "legal_entity",
6734
+ "contract_clause",
6735
+ // ── Portfolio (3 of 3): structural containers ─────────────────────────────
6736
+ "organization",
6737
+ "portfolio",
6738
+ "product_area",
6739
+ // ── Program Management (2 of 3): allocations and registers are tracking
6740
+ // containers ────────────────────────────────────────────────────────────
6741
+ "resource_allocation",
6742
+ "risk_register",
6743
+ // ── Localisation (4 of 5): translation keys, configs, and regional
6744
+ // adaptations are reference data ────────────────────────────────────────
6745
+ "translation_key",
6746
+ "locale_config",
6747
+ "cultural_adaptation",
6748
+ "regional_pricing",
6749
+ // ── Marketing (1 of 4): SEO keywords are tracked reference terms ─────────
6750
+ "seo_keyword",
6751
+ // ── Ecosystem (2 of 2): partner tiers and rev-share terms are config ─────
6752
+ "partner_tier",
6753
+ "partner_revenue_share",
6754
+ // ── Validation (2 of 4): evidence + learning are data points captured
6755
+ // from experiment_runs (which have lifecycles); hypothesis_evidence was
6756
+ // lifecycle-free here but is deprecated at v0.4.0 ──────────────────────────────────────
6757
+ "evidence",
6758
+ "learning",
6759
+ // ── Workspace (1 of 4): a capture is a fact about a moment, not a workflow.
6760
+ // It is taken, and from then on it either still matches its subject or it
6761
+ // does not — which is what `content_hash` answers, not a phase. ─────────
6762
+ "capture"
6763
+ ]);
6764
+ function isLifecycleFreeType(entityType) {
6765
+ return UPG_LIFECYCLE_FREE_TYPES.has(entityType);
6766
+ }
6767
+ var UPG_LIFECYCLE_PLANNED_TYPES = /* @__PURE__ */ new Set([
6768
+ // Phase B cleared this set entirely. Every type that was triaged
6769
+ // into Phase B/C/D under now either has a lifecycle in
6770
+ // `UPG_LIFECYCLES` or is documented as lifecycle-free in
6771
+ // `UPG_LIFECYCLE_FREE_TYPES`.
6772
+ //
6773
+ // Reintroduce entries here when a new entity type is added to the spec
6774
+ // and decided to be lifecycle-bearing but its phases have not yet been
6775
+ // designed. The `10-lifecycle-coverage` audit will surface uncovered
6776
+ // active types until they're either implemented or moved to the
6777
+ // free-types set.
6778
+ ]);
6779
+ function isLifecyclePlannedType(entityType) {
6780
+ return UPG_LIFECYCLE_PLANNED_TYPES.has(entityType);
6781
+ }
6782
+ function getLifecycleRenderShape(entityType) {
6783
+ const lifecycle = getLifecycleForType(entityType);
6784
+ if (!lifecycle) return null;
6785
+ const terminalSet = new Set(lifecycle.terminal_phases);
6786
+ const phaseIndex = new Map(lifecycle.phases.map((p, i) => [p.id, i]));
6787
+ const states = lifecycle.phases.map((phase) => ({
6788
+ id: phase.id,
6789
+ label: phase.label,
6790
+ description: phase.description,
6791
+ terminal: terminalSet.has(phase.id)
6792
+ }));
6793
+ const transitions = [];
6794
+ for (const phase of lifecycle.phases) {
6795
+ const fromTerminal = terminalSet.has(phase.id);
6796
+ for (const to of phase.transitions_to) {
6797
+ const toTerminal = terminalSet.has(to);
6798
+ let kind;
6799
+ if (fromTerminal && !toTerminal) {
6800
+ kind = "reopen";
6801
+ } else if (toTerminal && !fromTerminal) {
6802
+ kind = "terminal";
6803
+ } else {
6804
+ const fromIdx = phaseIndex.get(phase.id) ?? 0;
6805
+ const toIdx = phaseIndex.get(to) ?? 0;
6806
+ kind = toIdx < fromIdx ? "backward" : "forward";
6807
+ }
6808
+ transitions.push({ from: phase.id, to, kind });
6809
+ }
6810
+ }
6811
+ return {
6812
+ entity_type: lifecycle.entity_type,
6813
+ ...lifecycle.template_id !== void 0 && { template_id: lifecycle.template_id },
6814
+ states,
6815
+ transitions,
6816
+ initial_state: lifecycle.initial_phase
6817
+ };
6818
+ }
6819
+
6820
+ // src/catalog/legacy-product-stages.ts
6821
+ var LEGACY_PRODUCT_STAGES = Object.freeze({
6822
+ idea: "concept"
6823
+ });
6824
+ function migrateProductStage(stage) {
6825
+ if (stage === void 0 || stage === null) {
6826
+ return void 0;
6827
+ }
6828
+ if (typeof stage !== "string") {
6829
+ return stage;
6830
+ }
6831
+ const lower = stage.toLowerCase();
6832
+ const mapped = LEGACY_PRODUCT_STAGES[lower];
6833
+ return mapped ?? stage;
6834
+ }
6835
+ function isLegacyProductStage(stage) {
6836
+ return typeof stage === "string" && stage.toLowerCase() in LEGACY_PRODUCT_STAGES;
6837
+ }
6838
+
6839
+ // src/shapes/base-node.ts
6840
+ var BASE_NODE_FIELD_PRESENCE = {
6841
+ id: true,
6842
+ type: true,
6843
+ title: true,
6844
+ slug: true,
6845
+ aliases: true,
6846
+ key: true,
6847
+ description: true,
6848
+ tags: true,
6849
+ status: true,
6850
+ archived: true,
6851
+ archived_at: true,
6852
+ source_id: true,
6853
+ source_type: true,
6854
+ mapping_confidence: true,
6855
+ external_tool: true,
6856
+ external_ref: true,
6857
+ external_id: true,
6858
+ external_links: true,
6859
+ created_at: true,
6860
+ updated_at: true,
6861
+ properties: true
6862
+ };
6863
+ var UPG_BASE_NODE_FIELDS = Object.freeze(
6864
+ Object.keys(BASE_NODE_FIELD_PRESENCE)
6865
+ );
6866
+ var UPG_BASE_NODE_FIELD_SET = new Set(UPG_BASE_NODE_FIELDS);
6867
+ var UPG_BASE_NODE_SPECIAL_MERGE_FIELDS = /* @__PURE__ */ new Set([
6868
+ "id",
6869
+ "type",
6870
+ "slug",
6871
+ "aliases",
6872
+ "properties"
6873
+ ]);
6874
+
6875
+ // src/shapes/document.ts
6876
+ var UPG_CROSS_ONLY_EDGE_TYPES = [
6877
+ // Peer overlap: "these two products share / overlap on this thing." Symmetric,
6878
+ // no canonical instance_of identity. shares_job / shares_need are the job/need
6879
+ // siblings of the persona/competitor/metric peers (connective layer, 0.13.1).
6880
+ "shares_persona",
6881
+ "shares_competitor",
6882
+ "shares_metric",
6883
+ "shares_job",
6884
+ "shares_need",
6885
+ // Product-to-product relationships.
6886
+ "depends_on_product",
6887
+ // runtime dependency (built-on / dogfood), not containment
6888
+ "cannibalises",
6889
+ "succeeds",
6890
+ "hosts",
6891
+ // host runs the hosted product inside itself (container -> contained)
6892
+ // OKR / measurement rollup primitives: the cascade ACROSS products (subordinate
6893
+ // -> superior). `contributes_to` rolls a product strategy entity up to a higher-
6894
+ // level one; `rolls_up_to` feeds a product metric into a higher-level north-star.
6895
+ "contributes_to",
6896
+ "rolls_up_to",
6897
+ // Canonical instance + area-to-audience: the target lives in the portfolio
6898
+ // `registry` section (`registry/{node_id}`). `instance_of` is same-type
6899
+ // canonical-to-instance; the area edges are minted via `link_area_to_audience`.
6900
+ "instance_of",
6901
+ "area_serves_persona",
6902
+ "area_targets_market_segment",
6903
+ // Foundations (0.9.12): a product-graph entity links to a canonical specification
6904
+ // or primitive. Registry-internal spec-to-spec links are catalog edges, not these.
6905
+ //
6906
+ // CORRECTED 0.34.0 — the target contract. This comment said `registry/{node_id}`,
6907
+ // "same shape as instance_of". Every live instance disagrees: all three in the
6908
+ // field target `{product_id}/{node_id}`, and the portfolio's `registry` section
6909
+ // is EMPTY. A reader following the old sentence would look for a registry entry
6910
+ // that has never existed. Both target forms are legitimate — a specification held
6911
+ // as a canonical in the registry, or one held inside the product graph that owns
6912
+ // it — and the field has so far only produced the second.
6913
+ //
6914
+ // THE GAP THIS TIER LEAVES, restated 0.34.0 because the 0.33.0 wording named a
6915
+ // case no graph instantiates. It is NOT "a specification in a graph with no
6916
+ // portfolio around it". It is the HOLDER PRODUCT, and it is live today: the
6917
+ // product graph that HOLDS a specification node has three sibling products
6918
+ // reaching it through this tier and no way to state its own relation to it,
6919
+ // because the cross tier requires two distinct products and there is no
6920
+ // within-graph conformance edge. Not closed in 0.34.0. The verb the live case
6921
+ // wants is `defines` rather than `conforms_to` — a product does not conform to
6922
+ // the specification it authors — and one instance of evidence is not enough to
6923
+ // mint a catalog edge in the area that cost 0.33.0 its shape mid-build. The
6924
+ // condition is the second holder-product instance, or any single-product graph
6925
+ // modelling a specification it conforms to.
6926
+ "product_implements_specification",
6927
+ "product_exposes_specification",
6928
+ "feature_conforms_to_specification",
6929
+ "api_contract_speaks_specification",
6930
+ "product_exposes_primitive",
6931
+ "feature_manipulates_primitive",
6932
+ "primitive_stored_as_data_type"
6933
+ ];
6934
+ var UPG_CROSS_EDGE_TYPES = [
6935
+ ...UPG_CROSS_ONLY_EDGE_TYPES,
6936
+ ...UPG_CROSS_ELIGIBLE_CATALOG_EDGE_TYPES
6937
+ ];
6938
+ var REGISTRY_PRODUCT_ID = "registry";
6939
+ var UPG_PORTFOLIO_KINDS = ["owned", "watched", "strategic", "internal", "gtm"];
6940
+
6941
+ // src/registry/entity-type-resolution.ts
6942
+ var TYPES = getTypes();
6943
+ var TYPES_SET = new Set(TYPES);
6944
+ var UnknownEntityTypeError = class extends Error {
6945
+ suggestions;
6946
+ rawType;
6947
+ constructor(rawType, suggestions) {
6948
+ const suffix = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
6949
+ super(`Unknown entity type: "${rawType}".${suffix}`);
6950
+ this.name = "UnknownEntityTypeError";
6951
+ this.rawType = rawType;
6952
+ this.suggestions = suggestions;
6953
+ }
6954
+ };
6955
+ function withinEditDistance1(a, b) {
6956
+ if (a === b) return true;
6957
+ const al = a.length;
6958
+ const bl = b.length;
6959
+ if (Math.abs(al - bl) > 1) return false;
6960
+ let i = 0;
6961
+ let j = 0;
6962
+ let edits = 0;
6963
+ while (i < al && j < bl) {
6964
+ if (a[i] !== b[j]) {
6965
+ if (++edits > 1) return false;
6966
+ if (al > bl) i++;
6967
+ else if (bl > al) j++;
6968
+ else {
6969
+ i++;
6970
+ j++;
6971
+ }
6972
+ } else {
6973
+ i++;
6974
+ j++;
6975
+ }
6976
+ }
6977
+ if (i < al || j < bl) edits++;
6978
+ return edits <= 1;
6979
+ }
6980
+ function resolveEntityType(rawType) {
6981
+ if (typeof rawType !== "string" || rawType.length === 0) {
6982
+ throw new UnknownEntityTypeError(String(rawType ?? ""), []);
6983
+ }
6984
+ if (TYPES_SET.has(rawType)) {
6985
+ return { canonical: rawType };
6986
+ }
6987
+ const replacement = getReplacementType(rawType);
6988
+ if (replacement && TYPES_SET.has(replacement)) {
6989
+ return { canonical: replacement, alias: { from: rawType, to: replacement } };
6990
+ }
6991
+ const suggestions = [];
6992
+ for (const t of TYPES) {
6993
+ if (withinEditDistance1(t, rawType)) suggestions.push(t);
6994
+ if (suggestions.length >= 5) break;
6995
+ }
6996
+ throw new UnknownEntityTypeError(rawType, suggestions);
6997
+ }
6998
+
6999
+ // src/grammar/hierarchy.ts
7000
+ var UPG_VALID_CHILDREN = {
7001
+ // ── Foundations (0.9.12): registry canonicals that self-nest. A
7002
+ // specification extends a specification (a dialect of a parent spec); a
7003
+ // primitive composes primitives (a block contains spans). These back the
7004
+ // hierarchy-classified specification_extends_specification /
7005
+ // primitive_composes_primitive catalog edges.
7006
+ specification: ["specification"],
7007
+ primitive: ["primitive"],
7008
+ // An operating_lifecycle (e.g. the content-ops lifecycle) contains its ordered
7009
+ // operating_stage children. Backs the operating_lifecycle_contains_operating_stage edge.
7010
+ operating_lifecycle: ["operating_stage"],
7011
+ // ── Product (root) ─────────────────────────────────────────────────────────
7012
+ product: [
7013
+ // Strategy
7014
+ "vision",
7015
+ "outcome",
7016
+ "objective",
7017
+ "metric",
7018
+ "decision",
7019
+ "constraint",
7020
+ // User
7021
+ "persona",
7022
+ // Discovery (via outcome children)
7023
+ // Validation (via hypothesis children)
7024
+ // Market Intelligence
7025
+ "competitor",
7026
+ "market_trend",
7027
+ "market_segment",
7028
+ "competitive_analysis",
7029
+ // User Research
7030
+ "research_study",
7031
+ // Experience Design
7032
+ "user_journey",
7033
+ "user_flow",
7034
+ "wireframe",
7035
+ "screen",
7036
+ // Design System
7037
+ "design_system",
7038
+ "design_component",
7039
+ // Brand Identity
7040
+ "brand_identity",
7041
+ // Product Specification
7042
+ "feature",
7043
+ "feature_area",
7044
+ "release",
7045
+ "roadmap",
7046
+ "roadmap_theme",
7047
+ "planning_cycle",
7048
+ // Configuration (0.30.0): the levers whose values select which surface tree
7049
+ // the product renders. Few per product, and top-level because an axis is a
7050
+ // product-wide fact rather than a child of any one feature or surface.
7051
+ "configuration_axis",
7052
+ // Classification (0.32.0): the product's own taxonomies — the grouped-label
7053
+ // case, one named group over a set of values. Same reasoning as the
7054
+ // configuration axis above, and until now the only parent a
7055
+ // classification_axis could have was a competitive_analysis.
7056
+ "classification_axis",
7057
+ // Engineering
7058
+ "bounded_context",
7059
+ "code_repository",
7060
+ "integration_pattern",
7061
+ "external_api",
7062
+ "data_flow",
7063
+ // Growth
7064
+ "funnel",
7065
+ "acquisition_channel",
7066
+ "cohort",
7067
+ "behavioral_segment",
7068
+ "growth_loop",
7069
+ "attribution_model",
7070
+ // Business Model
7071
+ "business_model",
7072
+ // Go-To-Market
7073
+ "gtm_strategy",
7074
+ // Team & Organisation
7075
+ // `person` is containment-free (see UPG_CONTAINMENT_FREE_TYPES below)
7076
+ // (referenced via edges, not nested under product)
7077
+ "team",
7078
+ "stakeholder",
7079
+ "department",
7080
+ // Data & Analytics
7081
+ "data_source",
7082
+ "event_schema",
7083
+ "dashboard",
7084
+ "data_domain",
7085
+ "glossary_term",
7086
+ // Content & Knowledge
7087
+ "content_piece",
7088
+ "knowledge_base_article",
7089
+ "brand_asset",
7090
+ "document",
7091
+ "documentation_template",
7092
+ // Legal
7093
+ "legal_entity",
7094
+ "privacy_policy",
7095
+ // Compliance
7096
+ "compliance_requirement",
7097
+ "risk",
7098
+ "data_contract",
7099
+ "audit_log_policy",
7100
+ "compliance_framework",
7101
+ // DevOps & Platform
7102
+ "service_level_objective",
7103
+ "incident",
7104
+ "runbook",
7105
+ "monitor",
7106
+ "ci_pipeline",
7107
+ "release_strategy",
7108
+ "on_call_rotation",
7109
+ "infrastructure_component",
7110
+ // Security
7111
+ "threat_model",
7112
+ "security_control",
7113
+ "security_policy",
7114
+ "penetration_test",
7115
+ "security_review",
7116
+ "data_classification",
7117
+ "access_policy",
7118
+ // Accessibility
7119
+ "a11y_standard",
7120
+ "a11y_audit",
7121
+ "a11y_annotation",
7122
+ // Quality Assurance
7123
+ "test_plan",
7124
+ "test_suite",
7125
+ "qa_session",
7126
+ "test_coverage_report",
7127
+ "test_environment",
7128
+ // Customer Feedback
7129
+ "feedback_program",
7130
+ "user_advisory_board",
7131
+ "beta_program",
7132
+ // Pricing & Packaging
7133
+ "pricing_strategy",
7134
+ // AI & Machine Learning
7135
+ "ai_model",
7136
+ "model_comparison",
7137
+ // Workflows & Agents
7138
+ "workflow_template",
7139
+ "agent_definition",
7140
+ // Sales & Revenue
7141
+ "pipeline_sales",
7142
+ "account",
7143
+ "lead",
7144
+ "subscription",
7145
+ "forecast",
7146
+ // Program Management
7147
+ // `milestone` is a SECOND, shallower parent here (Portfolio Phase 2,
7148
+ // 2026-08-14), alongside its existing `project` parent: a product can own a
7149
+ // milestone outright with no program/project above it. Required by the
7150
+ // hierarchy-orphan guard now that `product_targets_milestone` exists.
7151
+ "program",
7152
+ "milestone",
7153
+ // Marketing
7154
+ "marketing_strategy",
7155
+ "press_release",
7156
+ "event",
7157
+ "community_initiative",
7158
+ // Customer Success
7159
+ "support_ticket",
7160
+ "customer_feedback",
7161
+ "churn_reason",
7162
+ "customer_health_score",
7163
+ "playbook",
7164
+ "service_level_agreement",
7165
+ "success_milestone",
7166
+ "service_blueprint",
7167
+ "nps_campaign",
7168
+ // Localisation
7169
+ "locale",
7170
+ "locale_config",
7171
+ // Customer Education
7172
+ "education_program",
7173
+ // Partners & Ecosystem
7174
+ "partner_program",
7175
+ "api_ecosystem",
7176
+ "developer_portal",
7177
+ // Portfolio
7178
+ // (UPG-677) product no longer lists product_area as a child: the
7179
+ // product↔product_area containment is the single direction
7180
+ // product_area_contains_product (product_area → product). The inverse
7181
+ // product_categorised_in_product_area edge was collapsed.
7182
+ // Workspace
7183
+ "workspace"
7184
+ ],
7185
+ // ── Metric & Outcome hierarchy ──────────────────────────────────────────────
7186
+ outcome: ["metric", "opportunity", "feature"],
7187
+ // ── User & Jobs hierarchy ───────────────────────────────────────────────────
7188
+ persona: ["job", "need", "switching_cost", "desired_outcome"],
7189
+ job: ["need", "desired_outcome", "job_step"],
7190
+ // ── Discovery hierarchy ─────────────────────────────────────────────────────
7191
+ opportunity: ["solution", "design_concept", "feasibility_study", "design_sprint"],
7192
+ // solutions now propose hypothesis (the templated
7193
+ // belief), not the legacy hypothesis type.
7194
+ solution: ["hypothesis", "prototype", "metric"],
7195
+ // ── Validation hierarchy ────────────────────────────────────────────────────
7196
+ // (UPG-664) The validation chain is a single-parent line:
7197
+ // hypothesis ▷ experiment_plan ▷ experiment ▷ experiment_run.
7198
+ // hypothesis is canonical (hypothesis_evidence collapsed into evidence;
7199
+ // relationship expressed via hypothesis_has_evidence edge; evidence.direction
7200
+ // carries supports/refutes/neutral semantics). `test_plan` re-homed to the
7201
+ // QA/testing domain (UPG-678), so it is no longer a hypothesis child.
7202
+ hypothesis: ["experiment_plan", "research_plan", "evidence"],
7203
+ // experiment_plan is the canonical validation PLAN (graduated stable,
7204
+ // UPG-664). It designs the experiment it produces; the plan owns the
7205
+ // experiment in the containment line. It also retains experiment_run as a
7206
+ // child for the v0.2.6 split-migration path
7207
+ // (`experiment_plan_ran_as_experiment_run`), where a legacy experiment routes
7208
+ // directly to a plan + run pair.
7209
+ experiment_plan: ["experiment", "experiment_run"],
7210
+ // experiment is the canonical structured test. It owns its run(s) and the
7211
+ // learning/evidence it produces. The optional experiment_run child captures
7212
+ // the multi-run / replication case.
7213
+ experiment: ["experiment_run", "learning", "evidence"],
7214
+ // experiment_run carries learning/evidence/metric children (run produces
7215
+ // evidence) and self-nests for replications/multi-armed runs (V9: this is
7216
+ // where self-nesting genuinely lives, not on `experiment`).
7217
+ experiment_run: ["learning", "evidence", "metric", "experiment_run"],
7218
+ // ── OKR hierarchy ──────────────────────────────────────────────────────────
7219
+ // strategic_question (0.17.4): an open coordination question raised under an
7220
+ // objective (objective_raises_strategic_question, hierarchy).
7221
+ objective: ["key_result", "metric", "strategic_question"],
7222
+ key_result: ["metric"],
7223
+ // ── Strategic cascade: vision → mission → strategic_pillar → strategic_theme →
7224
+ // { initiative, objective } ───────────────────────────────────────────────
7225
+ //
7226
+ // strategic_pillar vs strategic_theme (UPG-692 T3.3 — distinct cascade levels,
7227
+ // NOT duplicates; sharpened rather than merged):
7228
+ // - strategic_pillar = a DURABLE structural division of the strategy. A
7229
+ // standing area the org organises around for years (e.g. "Structured
7230
+ // content", "Developer experience"). Few, long-lived; time_horizon is
7231
+ // multi-year / open-ended; carries its own `success_indicator`.
7232
+ // - strategic_theme = a TIME-BOUND thrust WITHIN a pillar. The current bet
7233
+ // for a period (e.g. "Go AI-native this year"). More numerous, rotates;
7234
+ // time_horizon is bounded (Q / FY); has NO `success_indicator` because it
7235
+ // is measured through its child objectives (strategic_theme_contains_objective).
7236
+ // That success-measurement asymmetry is the load-bearing distinction.
7237
+ vision: ["mission"],
7238
+ mission: ["strategic_pillar"],
7239
+ // 0.20.1: metric added as a strategic_pillar child, backing
7240
+ // strategic_pillar_measured_by_metric (hierarchy) — the pillar's own
7241
+ // north-star, mirrored from objective: ['key_result', 'metric', ...] one
7242
+ // level down the cascade.
7243
+ strategic_pillar: ["strategic_theme", "capability", "value_stream", "decision", "metric"],
7244
+ // v0.5.4 (UPG-511): objectives are the specific quarterly bets *within* a
7245
+ // strategic theme. The theme is the multi-quarter focus area; objectives are
7246
+ // subordinate. Pairs with `strategic_theme_contains_objective` in the edge
7247
+ // catalog (hierarchy, strategic_theme → objective, reverse_verb: rolls_up_to;
7248
+ // renamed from objective_rolls_up_to_strategic_theme in UPG-676).
7249
+ strategic_theme: ["initiative", "objective"],
7250
+ initiative: ["assumption", "strategic_question"],
7251
+ // v0.5.2 (UPG-528): capability decomposes into sub-capabilities (Wardley
7252
+ // value-chain spine) and is realised by user-facing features. Pairs with
7253
+ // `capability_depends_on_capability` and `capability_implemented_by_feature`
7254
+ // in the edge catalogue. The self-loop guard (UPG-520) keeps A → A refused;
7255
+ // A → B between distinct capabilities is the supported decomposition.
7256
+ capability: ["capability", "feature"],
7257
+ // ── Market hierarchy ────────────────────────────────────────────────────────
7258
+ // competitive_analysis hosts classification axes (its dimensions);
7259
+ // each axis hosts its values; values are reusable across the row and column
7260
+ // dossiers of a 2-axis matrix.
7261
+ competitive_analysis: ["competitor", "market_trend", "market_segment", "classification_axis"],
7262
+ competitor: ["competitor_feature", "competitor_signal"],
7263
+ classification_axis: ["classification_value"],
7264
+ // ── UX Research hierarchy ───────────────────────────────────────────────────
7265
+ research_study: [
7266
+ "participant",
7267
+ "observation",
7268
+ "affinity_cluster",
7269
+ "research_question",
7270
+ "interview_guide",
7271
+ "insight",
7272
+ "survey_response"
7273
+ ],
7274
+ observation: ["quote"],
7275
+ // F6 (UPG-672): affinity_cluster owns the observations it groups, in addition
7276
+ // to the insights it synthesises. Pairs with `affinity_cluster_groups_observation`
7277
+ // in the edge catalog (hierarchy). An observation can be both directly owned by
7278
+ // its research_study and grouped under an affinity_cluster (multi-parent grammar).
7279
+ affinity_cluster: ["insight", "observation"],
7280
+ // insight refines into insight; a raw insight can be distilled
7281
+ // into a more specific insight (refines_into chain).
7282
+ // v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
7283
+ // parallel to `observation: ['quote']`. Research-synthesis canon.
7284
+ // Pairs with `insight_evidenced_by_quote` in the edge catalog.
7285
+ // ── Design hierarchy ────────────────────────────────────────────────────────
7286
+ // (UPG-663, v0.9.2) A user_journey owns its steps (the stable 0.1.0 spine)
7287
+ // and carries phases as a non-owning band overlay. A journey_phase is NO
7288
+ // LONGER a containment parent of journey_step: it SPANS steps via the
7289
+ // `journey_phase_spans_journey_step` edge (mirroring the marketing
7290
+ // `customer_journey_stage_spans_journey_step` precedent), so each step has
7291
+ // exactly one containment parent and the journey has one canonical step list.
7292
+ user_journey: ["journey_step", "journey_phase"],
7293
+ journey_step: ["journey_action"],
7294
+ // v0.5.8 (UPG-528 Part 2d): insights own their evidencing quotes;
7295
+ // parallel to `observation: ['quote']`. Research-synthesis canon
7296
+ // (insight ← quote evidencing) extends quote's hierarchy parents to
7297
+ // both observation (raw-capture parent) and insight (synthesis parent).
7298
+ // Pairs with `insight_evidenced_by_quote` in the edge catalog.
7299
+ insight: ["design_question", "insight", "quote"],
7300
+ // v0.5.2 (UPG-528): need anchors a Wardley value chain; capabilities
7301
+ // fulfil the need at the chain top. Cross-domain hierarchy (need ∈ Design,
7302
+ // capability ∈ Strategy) is intentional: Wardley analysis starts from a
7303
+ // user need and decomposes into the capabilities required to fulfil it.
7304
+ need: ["design_question", "capability"],
7305
+ design_question: ["design_concept"],
7306
+ design_concept: ["prototype", "wireframe"],
7307
+ design_component: [
7308
+ "design_token",
7309
+ "design_pattern",
7310
+ "design_guideline",
7311
+ "interaction_spec",
7312
+ "design_component"
7313
+ ],
7314
+ // self-nesting for atomic hierarchy (organism → molecule → atom)
7315
+ prototype: ["annotation"],
7316
+ // ── Brand hierarchy ─────────────────────────────────────────────────────────
7317
+ brand_identity: [
7318
+ "brand_colour",
7319
+ "brand_typography",
7320
+ "brand_asset",
7321
+ "brand_voice",
7322
+ "brand_logo",
7323
+ "brand_imagery"
7324
+ ],
7325
+ // ── Product Specification hierarchy ─────────────────────────────────────────
7326
+ // story_task collapsed into canonical task. feature now owns
7327
+ // task directly; story-derived tasks implement user_story via
7328
+ // task_implements_user_story edge.
7329
+ feature: ["epic", "bug", "task"],
7330
+ feature_area: ["feature", "feature_area"],
7331
+ // epic owns user_story (the templated promise) and, mirroring feature, may
7332
+ // also directly contain bug/task — heterogeneous imported tickets that belong
7333
+ // to one epic rather than the feature as a whole (feedback df99026a).
7334
+ epic: ["user_story", "bug", "task"],
7335
+ user_story: ["acceptance_criterion"],
7336
+ task: ["task"],
7337
+ // planning_cycle (0.20.0): the cadence axis self-nests (a program-increment
7338
+ // contains iterations; a cycle contains its cooldown) via
7339
+ // planning_cycle_contains_planning_cycle. It does NOT contain user_story as a
7340
+ // child: scheduling work into a cycle is the semantic, deliberate-only
7341
+ // planning_cycle_schedules_work_item edge, so the item keeps its feature/epic
7342
+ // containment parent and is merely referenced by the cycle.
7343
+ planning_cycle: ["planning_cycle"],
7344
+ // ── Engineering hierarchy ───────────────────────────────────────────────────
7345
+ bounded_context: [
7346
+ "service",
7347
+ "domain_event",
7348
+ "decision",
7349
+ "data_model",
7350
+ "aggregate",
7351
+ "read_model",
7352
+ "code_repository",
7353
+ "integration_pattern",
7354
+ "external_api",
7355
+ "data_flow",
7356
+ // v0.5.7 (UPG-528 Part 2c): BCs publish api_contracts as their "published
7357
+ // language" (DDD canon). api_contract has two valid hierarchy parents now:
7358
+ // service (per-service exposure) AND bounded_context (BC-level surface).
7359
+ "api_contract"
7360
+ ],
7361
+ service: [
7362
+ "api_contract",
7363
+ "technical_debt_item",
7364
+ "feature_flag",
7365
+ "deployment",
7366
+ "api_endpoint",
7367
+ "database_schema",
7368
+ "queue_topic",
7369
+ "build_artifact",
7370
+ "library_dependency"
7371
+ ],
7372
+ // v0.5.1 (UPG-517 C2): contracts contain their endpoints. Service still
7373
+ // serves endpoints directly (legacy / non-contract endpoints); both
7374
+ // parent-child paths coexist.
7375
+ api_contract: ["api_endpoint"],
7376
+ decision: ["technical_debt_item"],
7377
+ aggregate: ["domain_entity", "value_object", "command"],
7378
+ // ── Growth hierarchy ────────────────────────────────────────────────────────
7379
+ funnel: ["funnel_step"],
7380
+ acquisition_channel: ["growth_campaign"],
7381
+ growth_campaign: ["experiment_plan", "variant"],
7382
+ // ── Business Model hierarchy ─────────────────────────────────────────────────
7383
+ business_model: [
7384
+ "value_proposition",
7385
+ "revenue_stream",
7386
+ "cost_structure",
7387
+ "unit_economics",
7388
+ "partnership",
7389
+ "key_resource",
7390
+ "key_activity",
7391
+ "customer_relationship",
7392
+ "distribution_channel"
7393
+ ],
7394
+ revenue_stream: ["pricing_tier"],
7395
+ // ── Go-To-Market hierarchy ───────────────────────────────────────────────────
7396
+ gtm_strategy: [
7397
+ "ideal_customer_profile",
7398
+ "positioning",
7399
+ "launch",
7400
+ "content_strategy",
7401
+ "sales_motion",
7402
+ "competitive_battle_card",
7403
+ "demand_gen_program",
7404
+ "territory"
7405
+ ],
7406
+ positioning: ["messaging", "objection", "proof_point"],
7407
+ value_proposition: ["objection", "proof_point"],
7408
+ competitive_battle_card: ["objection"],
7409
+ objection: ["rebuttal"],
7410
+ rebuttal: ["proof_point"],
7411
+ // ── Team & Organisation hierarchy ───────────────────────────────────────────
7412
+ department: ["team", "stakeholder"],
7413
+ team: [
7414
+ "role",
7415
+ "team_okr",
7416
+ "retrospective",
7417
+ "dependency",
7418
+ "skill",
7419
+ "ceremony",
7420
+ "capacity_plan",
7421
+ "decision",
7422
+ // Self-nesting (0.17.2): a parent team contains its sub-teams / squads, one
7423
+ // level below department_contains_team. Backs team_contains_team. Mirrors
7424
+ // feature_area_contains_feature_area (same-type child listed last).
7425
+ "team"
7426
+ ],
7427
+ // ── Data & Analytics hierarchy ───────────────────────────────────────────────
7428
+ data_source: ["metric", "data_pipeline", "data_lineage", "event_schema"],
7429
+ // UPG-685 T0.4 (0.13.0 Wave 1): `outcome` removed as a metric child. It closed a
7430
+ // containment cycle (outcome ⊃ metric ⊃ outcome) and was backed only by the CAUSAL
7431
+ // `metric_drives_outcome` edge, not a hierarchy edge — the "metric leads to outcome"
7432
+ // meaning lives correctly on that causal edge. The real containment (outcome ⊃ metric)
7433
+ // is kept. Sub-metric trees (metric ⊃ metric) stay.
7434
+ metric: ["metric", "data_quality_rule", "metric_quality_assessment"],
7435
+ data_domain: ["data_product", "data_source", "glossary_term", "data_model", "dashboard"],
7436
+ dashboard: ["report", "experiment_run"],
7437
+ // ── Content & Knowledge hierarchy ────────────────────────────────────────────
7438
+ // release contains features and bugs (GitHub milestone→issue import).
7439
+ release: ["changelog", "feature", "bug"],
7440
+ content_strategy: ["content_calendar", "content_theme"],
7441
+ content_calendar: [
7442
+ "content_theme",
7443
+ "content_piece",
7444
+ "knowledge_base_article",
7445
+ "brand_asset",
7446
+ "document",
7447
+ "documentation_template"
7448
+ ],
7449
+ // ── Operations & CS hierarchy ────────────────────────────────────────────────
7450
+ service_blueprint: [
7451
+ "user_flow",
7452
+ "playbook",
7453
+ "service_level_agreement",
7454
+ "customer_health_score",
7455
+ "support_ticket",
7456
+ "customer_feedback"
7457
+ ],
7458
+ customer_health_score: ["nps_campaign", "success_milestone"],
7459
+ customer_feedback: ["churn_reason"],
7460
+ user_flow: ["screen", "customer_journey_stage"],
7461
+ customer_journey_stage: ["touchpoint"],
7462
+ // ── Legal, Compliance & Risk hierarchy ──────────────────────────────────────
7463
+ legal_entity: ["ip_asset", "contract"],
7464
+ contract: ["contract_clause"],
7465
+ compliance_framework: [
7466
+ "security_audit",
7467
+ "compliance_requirement",
7468
+ "privacy_policy",
7469
+ "audit_log_policy",
7470
+ "risk",
7471
+ "data_contract",
7472
+ "legal_entity"
7473
+ ],
7474
+ // ── DevOps & Platform hierarchy ──────────────────────────────────────────────
7475
+ infrastructure_component: [
7476
+ "service_level_objective",
7477
+ "monitor",
7478
+ "ci_pipeline",
7479
+ "incident",
7480
+ "runbook",
7481
+ "release_strategy",
7482
+ "on_call_rotation"
7483
+ ],
7484
+ service_level_objective: ["service_level_indicator", "error_budget"],
7485
+ incident: ["postmortem"],
7486
+ monitor: ["alert_rule"],
7487
+ ci_pipeline: ["build_artifact"],
7488
+ // ── Security hierarchy ───────────────────────────────────────────────────────
7489
+ security_policy: [
7490
+ "security_control",
7491
+ "access_policy",
7492
+ "data_classification",
7493
+ "threat_model",
7494
+ "security_review",
7495
+ "incident"
7496
+ ],
7497
+ security_review: ["penetration_test"],
7498
+ threat_model: ["threat", "vulnerability"],
7499
+ // ── Sales & Revenue hierarchy ────────────────────────────────────────────────
7500
+ pipeline_sales: ["pipeline_stage", "lead", "account", "forecast", "subscription"],
7501
+ account: ["contact", "deal"],
7502
+ deal: ["quote_document"],
7503
+ subscription: ["invoice"],
7504
+ // ── Program Management hierarchy ─────────────────────────────────────────────
7505
+ program: ["project", "risk_register", "change_request", "resource_allocation", "status_report"],
7506
+ project: ["milestone", "deliverable"],
7507
+ risk_register: ["risk"],
7508
+ // ── Accessibility hierarchy ──────────────────────────────────────────────────
7509
+ a11y_standard: ["a11y_guideline", "a11y_audit", "a11y_annotation"],
7510
+ a11y_audit: ["a11y_issue"],
7511
+ // ── Product Specification expansion ─────────────────────────────────────────
7512
+ roadmap: ["roadmap_item", "roadmap_theme", "release"],
7513
+ roadmap_theme: ["feature"],
7514
+ // ── Unified Context Layer hierarchy ─────────────────────────────────────────
7515
+ design_system: [
7516
+ "design_component",
7517
+ "design_token",
7518
+ "design_guideline",
7519
+ "brand_identity",
7520
+ "user_journey",
7521
+ "user_flow",
7522
+ "insight"
7523
+ ],
7524
+ screen: ["screen_state", "screen", "surface", "design_component", "wireframe"],
7525
+ // self-nesting + surfaces + components + wireframes
7526
+ // surface (0.27.0): the place inside a screen. Self-nests, so a shell holds
7527
+ // panes, a pane holds regions, a region holds slots. A surface is reached
7528
+ // through the screen that renders it (`screen: [... 'surface']`), never as a
7529
+ // top-level product child: every UPG_VALID_CHILDREN pair must be backed by a
7530
+ // hierarchy-classified edge (guardrail G2b), and the ratified ten do not
7531
+ // include a product-to-surface verb. An app-shell surface therefore hangs off
7532
+ // the screen that mounts it.
7533
+ //
7534
+ // NOTE: `surface_kind` narrows this further than UPG_VALID_CHILDREN can
7535
+ // express. UPG_VALID_CHILDREN is keyed on entity TYPE, so it can only say
7536
+ // "a surface may contain a surface"; the kind ordering (shell > tool > pane >
7537
+ // region > slot / gutter / action_bar, with overlay and ambient parented by
7538
+ // whatever raises them) is a property-level rule documented on `SurfaceKind`
7539
+ // and enforced by validators, not by this table. Precedent: the same split
7540
+ // applies to `planning_cycle`, whose `cadence_kind` narrows its self-nesting.
7541
+ surface: ["surface", "design_component"],
7542
+ // ── Marketing & Communications hierarchy ────────────────────────────────────
7543
+ marketing_strategy: [
7544
+ "marketing_channel",
7545
+ "seo_keyword",
7546
+ "press_release",
7547
+ "event",
7548
+ "community_initiative"
7549
+ ],
7550
+ marketing_channel: ["marketing_campaign_plan"],
7551
+ marketing_campaign_plan: ["email_sequence", "social_post", "ad_creative"],
7552
+ // ── Localisation & i18n hierarchy ───────────────────────────────────────────
7553
+ locale: ["translation_bundle", "cultural_adaptation", "regional_pricing", "locale_config"],
7554
+ translation_bundle: ["translation_key"],
7555
+ // ── Customer Education & Training hierarchy ──────────────────────────────────
7556
+ education_program: [
7557
+ "tutorial",
7558
+ "walkthrough",
7559
+ "webinar",
7560
+ "certification",
7561
+ "help_video",
7562
+ "learning_path"
7563
+ ],
7564
+ // a learning_path can include its terminal certification.
7565
+ learning_path: ["tutorial", "certification"],
7566
+ // ── Quality Assurance & Testing hierarchy ────────────────────────────────────
7567
+ // (UPG-678) test_plan re-homed validation → QA. It is the QA planning layer:
7568
+ // the verification approach (scope, environments, pass criteria) that the
7569
+ // test suites execute. A test_plan groups the suites that carry it out.
7570
+ test_plan: ["test_suite", "test_environment"],
7571
+ test_suite: [
7572
+ "test_case",
7573
+ "regression_test",
7574
+ "qa_session",
7575
+ "test_coverage_report",
7576
+ "test_environment",
7577
+ "test_result"
7578
+ ],
7579
+ test_case: ["test_result"],
7580
+ qa_session: ["bug"],
7581
+ // ── Partner & Ecosystem Management hierarchy ─────────────────────────────────
7582
+ partner_program: [
7583
+ "partner_tier",
7584
+ "integration_partner",
7585
+ "partner_revenue_share",
7586
+ "api_ecosystem",
7587
+ "developer_portal"
7588
+ ],
7589
+ api_ecosystem: ["marketplace_listing"],
7590
+ // ── Feedback & Voice of Customer hierarchy ───────────────────────────────────
7591
+ feedback_program: [
7592
+ "feature_request",
7593
+ "nps_campaign",
7594
+ "feedback_theme",
7595
+ "user_advisory_board",
7596
+ "beta_program"
7597
+ ],
7598
+ feature_request: ["feedback_vote"],
7599
+ // v0.5.8 (UPG-528 Part 2d): CABs convene as quarterly ceremonies;
7600
+ // CAB owns its meeting cadence (Stettler, Moore CAB playbook canon).
7601
+ // Pairs with `user_advisory_board_convenes_as_ceremony` in the edge
7602
+ // catalog. ceremony now has two valid hierarchy parents (team for sprint
7603
+ // ceremonies, user_advisory_board for CAB meetings).
7604
+ user_advisory_board: ["ceremony"],
7605
+ // ── Pricing & Packaging hierarchy ────────────────────────────────────────────
7606
+ pricing_strategy: [
7607
+ "experiment_plan",
7608
+ "pricing_tier",
7609
+ "discount_strategy",
7610
+ "trial_config",
7611
+ "paywall"
7612
+ ],
7613
+ // ── AI/ML Operations hierarchy ───────────────────────────────────────────────
7614
+ // (UPG-665) The prompt abstraction is corrected: ai_model → prompt_template →
7615
+ // prompt_version (a model has templates; each template has versions). The
7616
+ // model no longer owns prompt_version directly.
7617
+ ai_model: [
7618
+ "prompt_template",
7619
+ "eval_benchmark",
7620
+ "ai_cost_tracker",
7621
+ "hallucination_report",
7622
+ "ai_guardrail",
7623
+ "model_comparison",
7624
+ "ai_experiment",
7625
+ "ai_dataset",
7626
+ "ai_trace"
7627
+ ],
7628
+ // prompt_template owns its prompt_versions the way a file owns its commits.
7629
+ prompt_template: ["prompt_version"],
7630
+ // v0.5.7 (UPG-528 Part 2c): benchmarks define the metric set they measure
7631
+ // (HELM, MLPerf, BIG-bench all spec a metric list). metric already has many
7632
+ // hierarchy parents (outcome, objective, key_result, solution, data_source).
7633
+ eval_benchmark: ["eval_run", "metric"],
7634
+ // ai_trace spawns child traces for sub-calls (tool calls, chained
7635
+ // prompts). Self-nesting enables call-tree representation.
7636
+ ai_trace: ["ai_trace"],
7637
+ // ── Agentic Workflows & Process hierarchy ────────────────────────────────────
7638
+ workflow_template: ["workflow_run", "review_gate", "agent_task"],
7639
+ workflow_run: ["workflow_artifact"],
7640
+ agent_definition: ["agent_session", "agent_skill", "agent_hook", "workflow_template", "agent_task"],
7641
+ // v0.5.8 (UPG-528 Part 2d): review gates also vet research insights
7642
+ // (ResearchOps insight-review pattern). Pairs with `review_gate_vets_insight`
7643
+ // in the edge catalog. The gate now owns two child types: approval records
7644
+ // for workflow gates, insights for research-democratisation gates.
7645
+ review_gate: ["approval_record", "insight"],
7646
+ // ── Investigation hierarchy ──────────────────────────────────────────────────
7647
+ investigation: ["symptom", "root_cause"],
7648
+ root_cause: ["fix"],
7649
+ // ── Portfolio layer hierarchy ────────────────────────────────────────────────
7650
+ // organization → product_area is the org-axis anchor (who owns what);
7651
+ // organization → portfolio is the strategic-axis anchor (where you invest).
7652
+ // organization → workspace (WS3, 2026-07-05): org-altitude workspace anchor
7653
+ // (organization_thinks_in_workspace) — a workspace scoped to org altitude
7654
+ // lives in the same portfolio-level graph as its organization node.
7655
+ organization: ["portfolio", "product_area", "workspace"],
7656
+ // portfolios can nest (multi-level investment structures).
7657
+ portfolio: ["product", "portfolio"],
7658
+ // product_area is the organisational container for products and can
7659
+ // nest (parent → sub-area). product_area also groups features; the
7660
+ // "Studio area owns 6 features" mental model. product_area → workspace
7661
+ // (WS3, 2026-07-05): plane-altitude workspace anchor
7662
+ // (product_area_thinks_in_workspace) — "plane" is Entopo's app-level name
7663
+ // for product_area, not a distinct spec entity.
7664
+ product_area: ["product", "product_area", "feature", "workspace"]
7665
+ };
7666
+ function getValidChildren(entityType) {
7667
+ return UPG_VALID_CHILDREN[entityType] ?? [];
7668
+ }
7669
+ function canBeChildOf(childType, parentType) {
7670
+ return getValidChildren(parentType).includes(childType);
7671
+ }
7672
+ var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
7682
7673
  "person",
7683
- "department",
7684
- "skill",
7685
- "ceremony",
7686
- // ── Data & Analytics (4 of 10): lineage and domain containers, plus
7687
- // atomic definitions ────────────────────────────────────────────────────
7688
- "data_lineage",
7689
- "data_domain",
7690
- "data_quality_rule",
7691
- // ── AI (3 of 10): traces and cost trackers are append-only; model
7692
- // comparison snapshots are immutable ───────────────────────────────────
7693
- "ai_cost_tracker",
7694
- "ai_trace",
7695
- // ── Automation (2 of 6): approvals and artifacts are ledger entries ──────
7696
- "approval_record",
7697
- "workflow_artifact",
7698
- // ── Growth (7 of 9): funnels and attribution are structural definitions;
7699
- // cohorts/segments are computed snapshots ────────────────────────────
7700
- "funnel",
7701
- "funnel_step",
7702
- "acquisition_channel",
7703
- "cohort",
7704
- "behavioral_segment",
7705
- "growth_loop",
7706
- "attribution_model",
7707
- // ── Sales (5 of 6): pipeline structure + accounts/contacts + forecast
7708
- // snapshots. Only `deal` has a lifecycle (Phase B SALES_DEAL template) ─
7709
- "account",
7710
- "contact",
7711
- "pipeline_sales",
7712
- "pipeline_stage",
7713
- "forecast",
7714
- // ── Customer Success (5 of 9): journey structure + derived values ────────
7715
- "customer_journey_stage",
7716
- "touchpoint",
7717
- "churn_reason",
7718
- "service_level_agreement",
7719
- // ── DevOps (5 of 10): indicators and budgets are measurement references;
7720
- // strategies and pipelines are configuration ──────────────────────────
7721
- "service_level_indicator",
7722
- "service_level_objective",
7723
- "error_budget",
7724
- "ci_pipeline",
7725
- "release_strategy",
7726
- // ── Security (1 of 5): data classification is a reference label ──────────
7727
- "data_classification",
7728
- // ── Accessibility (2 of 4): standards and annotations ─────────────────────
7729
- "a11y_standard",
7730
- "a11y_annotation",
7731
- // ── Testing (3 of 5): regression tests are reference definitions; test
7732
- // results and coverage reports are immutable records ───────────────────
7733
- "regression_test",
7734
- "test_coverage_report",
7735
- "test_result",
7736
- // ── Customer Feedback (2 of 3): themes and votes are atomic data points ──
7737
- "feedback_theme",
7738
- "feedback_vote",
7739
- // ── Pricing (2 of 4): trial and paywall are configuration ─────────────────
7740
- "trial_config",
7741
- "paywall",
7742
- // ── Content (2 of 5): calendars and themes are planning configuration ────
7743
- "content_calendar",
7744
- "content_theme",
7745
- // ── Legal (2 of 3): legal_entity is an org; contract_clause is an atom
7746
- // of a contract (which has its own lifecycle) ───────────────────────────
7747
- "legal_entity",
7748
- "contract_clause",
7749
- // ── Portfolio (3 of 3): structural containers ─────────────────────────────
7750
- "organization",
7751
- "portfolio",
7752
- "product_area",
7753
- // ── Program Management (2 of 3): allocations and registers are tracking
7754
- // containers ────────────────────────────────────────────────────────────
7755
- "resource_allocation",
7756
- "risk_register",
7757
- // ── Localisation (4 of 5): translation keys, configs, and regional
7758
- // adaptations are reference data ────────────────────────────────────────
7759
- "translation_key",
7760
- "locale_config",
7761
- "cultural_adaptation",
7762
- "regional_pricing",
7763
- // ── Marketing (1 of 4): SEO keywords are tracked reference terms ─────────
7764
- "seo_keyword",
7765
- // ── Ecosystem (2 of 2): partner tiers and rev-share terms are config ─────
7766
- "partner_tier",
7767
- "partner_revenue_share",
7768
- // ── Validation (2 of 4): evidence + learning are data points captured
7769
- // from experiment_runs (which have lifecycles); hypothesis_evidence was
7770
- // lifecycle-free here but is deprecated at v0.4.0 ──────────────────────────────────────
7771
- "evidence",
7772
- "learning",
7773
- // ── Workspace (1 of 4): a capture is a fact about a moment, not a workflow.
7774
- // It is taken, and from then on it either still matches its subject or it
7775
- // does not — which is what `content_hash` answers, not a phase. ─────────
7674
+ // A framework_exercise has no structural parent: it anchors to the entities
7675
+ // it scores via the `framework_exercise_includes_node` edge, not by
7676
+ // containment. Modelling it as a product child would force a containment edge
7677
+ // and read against the whole point of the exercise model (relational, not
7678
+ // hierarchical). Same posture as `person`. See ADR 2026-06-02-framework-exercises.
7679
+ "framework_exercise",
7680
+ // A composition has no structural parent for the same reason a
7681
+ // framework_exercise does not: it anchors to what it SHOWS, relationally, via
7682
+ // `composition_focuses_node`. Modelling it as a product child would force a
7683
+ // containment edge that reads against the point — a published view is not a
7684
+ // part of the product, it is a lens onto one. This is also what keeps the
7685
+ // type to two edges rather than three: no provenance edge is minted (that is
7686
+ // `workspace_produced_node`, reused) and no parent edge is needed.
7687
+ "composition",
7688
+ // A capture has no structural parent for the same reason: it anchors to what
7689
+ // it RENDERS, via `capture_renders_node`. Giving it a product parent would
7690
+ // claim a capture is part of the product rather than a picture of one, and
7691
+ // would mint a containment edge whose only job is to satisfy the hierarchy.
7776
7692
  "capture"
7777
7693
  ]);
7778
- function isLifecycleFreeType(entityType) {
7779
- return UPG_LIFECYCLE_FREE_TYPES.has(entityType);
7694
+ function isContainmentFreeType(entityType) {
7695
+ return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
7780
7696
  }
7781
- var UPG_LIFECYCLE_PLANNED_TYPES = /* @__PURE__ */ new Set([
7782
- // Phase B cleared this set entirely. Every type that was triaged
7783
- // into Phase B/C/D under now either has a lifecycle in
7784
- // `UPG_LIFECYCLES` or is documented as lifecycle-free in
7785
- // `UPG_LIFECYCLE_FREE_TYPES`.
7786
- //
7787
- // Reintroduce entries here when a new entity type is added to the spec
7788
- // and decided to be lifecycle-bearing but its phases have not yet been
7789
- // designed. The `10-lifecycle-coverage` audit will surface uncovered
7790
- // active types until they're either implemented or moved to the
7791
- // free-types set.
7792
- ]);
7793
- function isLifecyclePlannedType(entityType) {
7794
- return UPG_LIFECYCLE_PLANNED_TYPES.has(entityType);
7697
+
7698
+ // src/grammar/projection.ts
7699
+ var VARIES_BY_EDGE = "surface_varies_by_configuration_axis";
7700
+ var PRESENT_UNDER_PROPERTY = "present_under";
7701
+ var ACTIVE_WHEN_PROPERTY = "active_when";
7702
+ var QUALIFIABLE_EDGE_TYPES = [
7703
+ "surface_contains_surface",
7704
+ "feature_occupies_surface"
7705
+ ];
7706
+ function readStringArray(properties, key) {
7707
+ const raw = properties?.[key];
7708
+ if (!Array.isArray(raw)) return void 0;
7709
+ return raw.filter((v) => typeof v === "string");
7795
7710
  }
7796
- function getLifecycleRenderShape(entityType) {
7797
- const lifecycle = getLifecycleForType(entityType);
7798
- if (!lifecycle) return null;
7799
- const terminalSet = new Set(lifecycle.terminal_phases);
7800
- const phaseIndex = new Map(lifecycle.phases.map((p, i) => [p.id, i]));
7801
- const states = lifecycle.phases.map((phase) => ({
7802
- id: phase.id,
7803
- label: phase.label,
7804
- description: phase.description,
7805
- terminal: terminalSet.has(phase.id)
7806
- }));
7807
- const transitions = [];
7808
- for (const phase of lifecycle.phases) {
7809
- const fromTerminal = terminalSet.has(phase.id);
7810
- for (const to of phase.transitions_to) {
7811
- const toTerminal = terminalSet.has(to);
7812
- let kind;
7813
- if (fromTerminal && !toTerminal) {
7814
- kind = "reopen";
7815
- } else if (toTerminal && !fromTerminal) {
7816
- kind = "terminal";
7817
- } else {
7818
- const fromIdx = phaseIndex.get(phase.id) ?? 0;
7819
- const toIdx = phaseIndex.get(to) ?? 0;
7820
- kind = toIdx < fromIdx ? "backward" : "forward";
7711
+ function readActiveWhen(edge) {
7712
+ const raw = edge.properties?.[ACTIVE_WHEN_PROPERTY];
7713
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
7714
+ const axis = raw.axis;
7715
+ const values = readStringArray(raw, "values");
7716
+ if (typeof axis !== "string" || axis.length === 0 || !values || values.length === 0) {
7717
+ return void 0;
7718
+ }
7719
+ return { axis, values };
7720
+ }
7721
+ function projectGraph(nodes, edges, configuration) {
7722
+ const axes = Object.keys(configuration);
7723
+ if (axes.length === 0) {
7724
+ return {
7725
+ nodes: [...nodes],
7726
+ edges: [...edges],
7727
+ excluded_node_ids: [],
7728
+ deactivated_edge_count: 0,
7729
+ dangling_edge_count: 0
7730
+ };
7731
+ }
7732
+ const excluded = /* @__PURE__ */ new Set();
7733
+ for (const edge of edges) {
7734
+ if (edge.type !== VARIES_BY_EDGE) continue;
7735
+ const chosen = configuration[edge.target];
7736
+ if (chosen === void 0) continue;
7737
+ const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
7738
+ if (!presentUnder) continue;
7739
+ if (!presentUnder.includes(chosen)) excluded.add(edge.source);
7740
+ }
7741
+ const survivingNodes = excluded.size === 0 ? [...nodes] : nodes.filter((n) => !excluded.has(n.id));
7742
+ let deactivated = 0;
7743
+ let dangling = 0;
7744
+ const survivingEdges = [];
7745
+ for (const edge of edges) {
7746
+ const qualifier = readActiveWhen(edge);
7747
+ if (qualifier) {
7748
+ const chosen = configuration[qualifier.axis];
7749
+ if (chosen !== void 0 && !qualifier.values.includes(chosen)) {
7750
+ deactivated++;
7751
+ continue;
7821
7752
  }
7822
- transitions.push({ from: phase.id, to, kind });
7823
7753
  }
7754
+ if (excluded.has(edge.source) || excluded.has(edge.target)) {
7755
+ dangling++;
7756
+ continue;
7757
+ }
7758
+ survivingEdges.push(edge);
7824
7759
  }
7825
7760
  return {
7826
- entity_type: lifecycle.entity_type,
7827
- ...lifecycle.template_id !== void 0 && { template_id: lifecycle.template_id },
7828
- states,
7829
- transitions,
7830
- initial_state: lifecycle.initial_phase
7761
+ nodes: survivingNodes,
7762
+ edges: survivingEdges,
7763
+ excluded_node_ids: [...excluded].sort(),
7764
+ deactivated_edge_count: deactivated,
7765
+ dangling_edge_count: dangling
7831
7766
  };
7832
7767
  }
7768
+ function enumerateProjections(nodes) {
7769
+ const out = [
7770
+ { configuration: {} }
7771
+ ];
7772
+ for (const node of nodes) {
7773
+ if (node.type !== "configuration_axis") continue;
7774
+ const values = readStringArray(node.properties, "values");
7775
+ if (!values) continue;
7776
+ for (const value of values) {
7777
+ out.push({ axis: node.id, value, configuration: { [node.id]: value } });
7778
+ }
7779
+ }
7780
+ return out;
7781
+ }
7782
+
7783
+ // src/grammar/configuration-drift.ts
7784
+ var ALTERNATES_EDGE = "surface_alternates_with_surface";
7785
+ var PARENT_EDGE_TYPES = /* @__PURE__ */ new Set(["surface_contains_surface", "screen_renders_surface"]);
7786
+ function edgeLabel(edge) {
7787
+ return edge.id ?? `${edge.source} -> ${edge.target}`;
7788
+ }
7789
+ function checkConfigurationDrift(nodes, edges) {
7790
+ const findings = [];
7791
+ const axisValues = /* @__PURE__ */ new Map();
7792
+ for (const node of nodes) {
7793
+ if (node.type !== "configuration_axis") continue;
7794
+ const values = readStringArray(node.properties, "values");
7795
+ if (!values || values.length === 0) {
7796
+ findings.push({
7797
+ kind: "axis_values_empty",
7798
+ severity: "error",
7799
+ node_id: node.id,
7800
+ axis_id: node.id,
7801
+ message: "Configuration axis declares no values. An axis with no values selects nothing, so no projection can be taken along it."
7802
+ });
7803
+ axisValues.set(node.id, []);
7804
+ continue;
7805
+ }
7806
+ axisValues.set(node.id, values);
7807
+ const rawDefault = node.properties?.default_value;
7808
+ if (typeof rawDefault === "string" && rawDefault.length > 0 && !values.includes(rawDefault)) {
7809
+ findings.push({
7810
+ kind: "axis_default_not_a_value",
7811
+ severity: "error",
7812
+ node_id: node.id,
7813
+ axis_id: node.id,
7814
+ message: `default_value "${rawDefault}" is not one of the axis values [${values.join(", ")}]. It names a value of this axis, so it has to be one the axis declares.`
7815
+ });
7816
+ }
7817
+ }
7818
+ for (const edge of edges) {
7819
+ if (edge.type !== VARIES_BY_EDGE) continue;
7820
+ const values = axisValues.get(edge.target);
7821
+ const presentUnder = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY);
7822
+ if (!presentUnder || presentUnder.length === 0) {
7823
+ findings.push({
7824
+ kind: "present_under_empty",
7825
+ severity: "error",
7826
+ edge_id: edge.id,
7827
+ node_id: edge.source,
7828
+ axis_id: edge.target,
7829
+ message: `${edgeLabel(edge)} declares variance without a non-empty present_under. A surface that exists under no configuration should be deleted rather than declared; a surface that exists under all of them should carry no varies_by edge at all.`
7830
+ });
7831
+ continue;
7832
+ }
7833
+ if (!values) continue;
7834
+ for (const value of presentUnder) {
7835
+ if (!values.includes(value)) {
7836
+ findings.push({
7837
+ kind: "present_under_unknown_value",
7838
+ severity: "error",
7839
+ edge_id: edge.id,
7840
+ node_id: edge.source,
7841
+ axis_id: edge.target,
7842
+ message: `present_under names "${value}", which the axis does not declare. Its values are [${values.join(", ")}].`
7843
+ });
7844
+ }
7845
+ }
7846
+ }
7847
+ for (const edge of edges) {
7848
+ const hasQualifierKey = edge.properties !== void 0 && Object.prototype.hasOwnProperty.call(edge.properties, ACTIVE_WHEN_PROPERTY);
7849
+ if (!hasQualifierKey) continue;
7850
+ if (edge.type !== void 0 && !QUALIFIABLE_EDGE_TYPES.includes(edge.type)) {
7851
+ findings.push({
7852
+ kind: "qualifier_on_illegal_edge",
7853
+ severity: "error",
7854
+ edge_id: edge.id,
7855
+ message: `active_when is not legal on ${edge.type}. The qualifier is scoped to [${QUALIFIABLE_EDGE_TYPES.join(", ")}] deliberately: conditional composition is the question this release answers, and a general modality system is not.`
7856
+ });
7857
+ continue;
7858
+ }
7859
+ const qualifier = readActiveWhen(edge);
7860
+ if (!qualifier) {
7861
+ const rawQualifier = edge.properties?.[ACTIVE_WHEN_PROPERTY];
7862
+ const rawValues = rawQualifier && typeof rawQualifier === "object" && !Array.isArray(rawQualifier) ? rawQualifier.values : void 0;
7863
+ if (Array.isArray(rawValues) && rawValues.length === 0) {
7864
+ findings.push({
7865
+ kind: "qualifier_values_empty",
7866
+ severity: "error",
7867
+ edge_id: edge.id,
7868
+ message: `${edgeLabel(edge)} carries an active_when with an empty values list. Read literally that would remove the relationship from every configuration, which is a way to delete an edge by writing a property; it is treated as absent instead. To say a relationship never holds, delete the edge.`
7869
+ });
7870
+ continue;
7871
+ }
7872
+ findings.push({
7873
+ kind: "qualifier_axis_unresolved",
7874
+ severity: "error",
7875
+ edge_id: edge.id,
7876
+ message: `${edgeLabel(edge)} carries a malformed active_when. It must be an object with an axis id and a non-empty values array; anything else reads as absent, which silently makes the relationship invariant.`
7877
+ });
7878
+ continue;
7879
+ }
7880
+ const values = axisValues.get(qualifier.axis);
7881
+ if (!values) {
7882
+ findings.push({
7883
+ kind: "qualifier_axis_unresolved",
7884
+ severity: "error",
7885
+ edge_id: edge.id,
7886
+ axis_id: qualifier.axis,
7887
+ message: `active_when names axis "${qualifier.axis}", which is not a configuration_axis node in this graph.`
7888
+ });
7889
+ continue;
7890
+ }
7891
+ for (const value of qualifier.values) {
7892
+ if (!values.includes(value)) {
7893
+ findings.push({
7894
+ kind: "qualifier_unknown_value",
7895
+ severity: "error",
7896
+ edge_id: edge.id,
7897
+ axis_id: qualifier.axis,
7898
+ message: `active_when names value "${value}", which axis "${qualifier.axis}" does not declare. Its values are [${values.join(", ")}].`
7899
+ });
7900
+ }
7901
+ }
7902
+ }
7903
+ const varianceBySource = /* @__PURE__ */ new Map();
7904
+ for (const edge of edges) {
7905
+ if (edge.type !== VARIES_BY_EDGE) continue;
7906
+ const values = readStringArray(edge.properties, PRESENT_UNDER_PROPERTY) ?? [];
7907
+ const list = varianceBySource.get(edge.source) ?? [];
7908
+ list.push({ axis: edge.target, values });
7909
+ varianceBySource.set(edge.source, list);
7910
+ }
7911
+ for (const edge of edges) {
7912
+ if (edge.type !== ALTERNATES_EDGE) continue;
7913
+ const left = varianceBySource.get(edge.source) ?? [];
7914
+ const right = varianceBySource.get(edge.target) ?? [];
7915
+ if (left.length === 0 || right.length === 0) {
7916
+ findings.push({
7917
+ kind: "alternation_axis_mismatch",
7918
+ severity: "error",
7919
+ edge_id: edge.id,
7920
+ message: `${edgeLabel(edge)} declares an alternation, but at least one endpoint declares no variance at all. Two surfaces that are both always present are not alternatives; they co-exist.`
7921
+ });
7922
+ continue;
7923
+ }
7924
+ const sharedAxes = left.filter((l) => right.some((r) => r.axis === l.axis)).map((l) => l.axis);
7925
+ if (sharedAxes.length === 0) {
7926
+ findings.push({
7927
+ kind: "alternation_axis_mismatch",
7928
+ severity: "error",
7929
+ edge_id: edge.id,
7930
+ message: `${edgeLabel(edge)} declares an alternation between surfaces that vary on different axes. Alternatives are selected by ONE lever; surfaces on different levers can both be present at once.`
7931
+ });
7932
+ continue;
7933
+ }
7934
+ for (const axis of sharedAxes) {
7935
+ const leftValues = left.find((l) => l.axis === axis)?.values ?? [];
7936
+ const rightValues = right.find((r) => r.axis === axis)?.values ?? [];
7937
+ const overlap = leftValues.filter((v) => rightValues.includes(v));
7938
+ if (overlap.length > 0) {
7939
+ findings.push({
7940
+ kind: "alternation_overlap",
7941
+ severity: "error",
7942
+ edge_id: edge.id,
7943
+ axis_id: axis,
7944
+ message: `${edgeLabel(edge)} declares an alternation, but both surfaces are present under [${overlap.join(", ")}] on axis "${axis}". They co-exist there, so the graph is asserting "one of these" and "both, together" about the same pair.`
7945
+ });
7946
+ }
7947
+ }
7948
+ }
7949
+ const parentsByChild = /* @__PURE__ */ new Map();
7950
+ for (const edge of edges) {
7951
+ if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
7952
+ parentsByChild.set(edge.target, (parentsByChild.get(edge.target) ?? 0) + 1);
7953
+ }
7954
+ }
7955
+ for (const [axisId, values] of axisValues) {
7956
+ for (const value of values) {
7957
+ const projected = projectGraph(nodes, edges, { [axisId]: value });
7958
+ const survivingParents = /* @__PURE__ */ new Map();
7959
+ for (const edge of projected.edges) {
7960
+ if (edge.type && PARENT_EDGE_TYPES.has(edge.type)) {
7961
+ survivingParents.set(edge.target, (survivingParents.get(edge.target) ?? 0) + 1);
7962
+ }
7963
+ }
7964
+ for (const node of projected.nodes) {
7965
+ if (node.type !== "surface") continue;
7966
+ if ((parentsByChild.get(node.id) ?? 0) === 0) continue;
7967
+ if ((survivingParents.get(node.id) ?? 0) > 0) continue;
7968
+ findings.push({
7969
+ kind: "orphaned_under_projection",
7970
+ severity: "warning",
7971
+ node_id: node.id,
7972
+ axis_id: axisId,
7973
+ message: `Surface is present under ${axisId} = "${value}" but every containment parent it has is absent there. Either it needs a parent in that configuration, or it should declare that it is absent from it too.`
7974
+ });
7975
+ }
7976
+ }
7977
+ }
7978
+ return findings;
7979
+ }
7833
7980
 
7834
7981
  // src/grammar/scales.ts
7835
7982
  var UPG_SCALES = {
@@ -12254,8 +12401,8 @@ var UPG_PROPERTY_SCHEMA = {
12254
12401
  },
12255
12402
  // CompositionProperties
12256
12403
  composition: {
12257
- member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
12258
- presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
12404
+ member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement. The clause list is authoritative and the named fields are a positive-only projection of it; since 0.34.0 a clause is a discriminated union on `dimension`, so the `type` axis carries entity types rather than free strings.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
12405
+ presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole: `group_by`, `sort`, `layout`, `nest_by`, and `orphan_disposition` (0.34.0, absent means `'root'`). A consumer may ignore it entirely and still be conformant, because every default it then applies is the safe one.", notes: "THE DESCRIPTION LISTS THE FIELDS ON PURPOSE. This property is `object` in the runtime property registry, so an agent reading `get_entity_schema` gets an opaque blob and this sentence. For an object-typed property the description IS the declared shape, which is why `check:editorial` hashes it (E.4, 0.34.0) and why a field added to `UPGViewPresentation` without a word here would be invisible to every gate and every agent at once." },
12259
12406
  members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
12260
12407
  rev: { type: "number", description: "Monotonic revision, bumped on each republish of the same slug.", notes: "A published revision is a fact about the composition, user-visible as a new print from the same plate, so it is serialised. Worth distinguishing from a store's concurrency token, which shares the name in some backends but is a fact about the table rather than about the thing, and is not spec data." },
12261
12408
  published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
@@ -14506,7 +14653,7 @@ var UPG_PROPERTY_SCHEMA = {
14506
14653
  team_type: { type: "string", enum: ["product", "engineering", "design", "growth", "customer_success"], description: "Functional area of the team" },
14507
14654
  size: { type: "number", description: "Number of people on the team" },
14508
14655
  mission: { type: "string", description: "Team's mission statement" },
14509
- key_prefix: { type: "string", description: 'Prefix this team mints node keys with (e.g. `"ENTP"`, giving `ENTP-1`, `ENTP-2`, ...). @example "ENTP"', notes: "SUPERSEDES `product.key_prefix` FOR THIS PRODUCT. The moment any team declares a prefix, the product-level prefix stops being consulted and the declared team prefixes become the candidate set a create surface offers. A product-level prefix that still resolved would win on order alone, and a multi-team product would then silently mint everything under it, which is the defect this field exists to end. A DECLARED PREFIX IS A CANDIDATE BEFORE IT IS OBSERVED. Candidates derived only from keys that already exist cannot see a team's first create, which is the one that most needs asking about. UNIQUENESS IS PRODUCT-SCOPED, AND A TEAM PREFIX DOES NOT WIDEN IT. The key sequence runs per product across entity types, so two teams in one product share one number line and never collide with each other. A team prefix names a team WITHIN that scope; it does not create a sequence that spans products. MINTING IS PRODUCT-SCOPED (normative, 0.33.0). `team` is `portfolio_shared`, so one team node can be referenced from two products. Minting is not portfolio-scoped with it. The rule, and it is a requirement rather than a recommendation: 1. A portfolio-shared team's prefix is NOT a minting candidate in a second product. The first product a team mints under is the only product that prefix mints in. 2. Refusal is NO-KEY. The node is created without a key. Refusal MUST NOT be an exception: a keyless create is a legal outcome everywhere else in the ladder, and throwing here would break creates that succeed today on every surface. Throwing stays reserved for a broken invariant, not for policy. 3. The rule applies on the MINT path, not the picker path, and it covers the INFERRED case. A prefix that was never requested by anyone, and was derived from keys that already exist in the second product, is refused on the same terms as one a caller named. A guard that only inspects an explicitly requested prefix misses the quiet path, and the quiet path is the one a fixture reproduces. WHY THE SCOPE STOPS AT THE PRODUCT. Key uniqueness is enforced by a `(product_id, key)` index, which permits the same key under two product ids by construction. A prefix that minted in two products would therefore run two independent sequences under one name and hand two different nodes the same citation, with nothing objecting. Portfolio-shared team minting is DEFERRED until a supra-product uniqueness design exists, rather than approximated. This rule states the CONTRACT. The behaviour belongs to whatever mints keys, which is not this package: see `UPGBaseNode.key`." }
14656
+ key_prefix: { type: "string", description: 'Prefix this team mints node keys with (e.g. `"ENTP"`, giving `ENTP-1`, `ENTP-2`, ...). @example "ENTP"', notes: "SUPERSEDES `product.key_prefix` FOR THIS PRODUCT, AND NOTHING ELSE (normative, narrowed in 0.34.0). The moment any team declares a prefix, the PRODUCT-LEVEL prefix stops being consulted. A product-level prefix that still resolved would win on order alone, and a multi-team product would then silently mint everything under it, which is the defect this field exists to end. THE CANDIDATE SET IS THE UNION OF DECLARED AND OBSERVED PREFIXES. A declaration adds a candidate; it does not remove one. A prefix stops being offered only when something claims it or when nothing has ever minted under it, and while more than one candidate stands the create surface keeps asking. WHY THIS WAS NARROWED, stated because 0.33.0 shipped the wider reading and an implementer built against it. The paragraph above made ANY declaration replace the candidate set outright, and its own stated rationale is entirely about `product.key_prefix`: a SINGLE STRING, declared once, that cannot represent two teams. An OBSERVED prefix is not that. It is evidence of a namespace already in active use. Suppressing it reproduces the precise defect this field exists to end, inside one product, and does so silently. Measured on the only keyed graph in the estate: it carries two observed prefixes, 370 keys under one and 662 under the other, and declares neither. Under the wider reading, one team declaring the smaller prefix collapses the candidate set to it, the picker disappears, and every later create mints under it, including the 662 keys' worth of work belonging to the other namespace. The sentence over-reached beyond its own reason, and this narrows it back to that reason. Shipped as a CHANGE in 0.34.0 rather than as a patch correction. Read as a correction it is defensible, but implementers had already built to the wider text, and moving a contract under them in a patch is how a patch becomes a surprise. A DECLARED PREFIX IS A CANDIDATE BEFORE IT IS OBSERVED. Candidates derived only from keys that already exist cannot see a team's first create, which is the one that most needs asking about. That is why declaration ADDS to the candidate set; it is not a reason for it to subtract. UNIQUENESS IS PRODUCT-SCOPED, AND A TEAM PREFIX DOES NOT WIDEN IT. The key sequence runs per product across entity types, so two teams in one product share one number line and never collide with each other. A team prefix names a team WITHIN that scope; it does not create a sequence that spans products. MINTING IS PRODUCT-SCOPED (normative, 0.33.0). `team` is `portfolio_shared`, so one team node can be referenced from two products. Minting is not portfolio-scoped with it. The rule, and it is a requirement rather than a recommendation: 1. A portfolio-shared team's prefix is NOT a minting candidate in a second product. The first product a team mints under is the only product that prefix mints in. 2. Refusal is NO-KEY. The node is created without a key. Refusal MUST NOT be an exception: a keyless create is a legal outcome everywhere else in the ladder, and throwing here would break creates that succeed today on every surface. Throwing stays reserved for a broken invariant, not for policy. 3. The rule applies on the MINT path, not the picker path, and it covers the INFERRED case. A prefix that was never requested by anyone, and was derived from keys that already exist in the second product, is refused on the same terms as one a caller named. A guard that only inspects an explicitly requested prefix misses the quiet path, and the quiet path is the one a fixture reproduces. WHY THE SCOPE STOPS AT THE PRODUCT. Key uniqueness is enforced by a `(product_id, key)` index, which permits the same key under two product ids by construction. A prefix that minted in two products would therefore run two independent sequences under one name and hand two different nodes the same citation, with nothing objecting. Portfolio-shared team minting is DEFERRED until a supra-product uniqueness design exists, rather than approximated. HOW RULE 1 IS DECIDED: THE EVIDENCE RULE (normative, 0.34.0). Rule 1 names a first product without saying how a minter knows which one it is. It is decided by EVIDENCE, not by a stored marker: at mint, if any other IN-SCOPE product already holds a key under the prefix, refuse. Evidence is derivable from the graphs themselves, needs no migration, cannot go stale, and cannot disagree with the keys. A durable home marker could do all three, and would mint state for a fact the graph already carries. The cost is stated rather than hidden: evidence is SCOPE-DEPENDENT, which is why the scope is ruled below in the same breath rather than left open. THE UNDECIDABLE CASE: NEITHER MINTS (normative, 0.34.0). When two products already hold keys under one prefix and nothing establishes which was first, NEITHER mints. No tiebreak is invented. Creation order is not recorded, and `max(existing)` measures import volume rather than precedence, so any tiebreak would be a guess wearing a rule, and a silent one, since it would attribute a namespace to a product with nothing to say it was wrong. Refusing both is the NO-KEY outcome rule 2 already sanctions, and it is recoverable: once either product declares the prefix, the other is unambiguous and a backfill can run. WHAT \"IN SCOPE\" MEANS: ENGINE-DEFINED, WITH A FLOOR AND A CEILING (normative, 0.34.0). The scope over which the evidence rule looks is defined by the engine, bounded on both sides. FLOOR. It MUST include every product the engine can enumerate for this caller. An engine that looks at fewer products than it can see is choosing not to notice a collision it could have seen. CEILING. It MUST NEVER include a product the caller could not otherwise read. A wider read is a cross-tenant information channel: refusing a mint because of a key in a graph the caller cannot see leaks that the graph exists and what is in it. This half is a security constraint and is not negotiable. PORTFOLIO ALTITUDE IS THE WRONG NORMATIVE ALTITUDE, and it was measured rather than argued: deleting the portfolio document changes nothing about minting, because no minter consults the portfolio seam. Stating the invariant there states it where nobody looks. In practice the local engine's scope is every graph in the workspace folder and the cloud engine's is the caller's own product list. THE HONEST CONSEQUENCE, which belongs in the text rather than in a later surprise: the invariant is SCOPE-RELATIVE. Two engines can legitimately disagree about whether one mint is safe, because they can legitimately see different sets of products. That is a real limitation of the evidence rule and the price of the ceiling. IMMUTABLE ONCE ANYTHING HAS MINTED UNDER IT, PER PRODUCT (normative, 0.34.0). Once any key exists under this prefix IN A GIVEN PRODUCT, the declaration cannot be edited for that product. Refusal-shaped and NO-KEY, matching rule 2: never an exception, because a keyless create is legal everywhere else in the ladder. SCOPED PER PRODUCT, NOT PER TEAM, and the reason is that `team` is `portfolio_shared`: a team that has never minted in product B must still be free to declare there. A per-team global lock would strand it. WHAT IT PREVENTS. Without it, a team edits its prefix after four hundred mints and one product silently carries two number lines under two names, with nothing recording that they were ever one sequence. RENAMING STAYS POSSIBLE BY THE HONEST ROUTE: a migration that rewrites the existing keys. That is a deliberate act with a visible cost, which is the difference between renaming a namespace and forking it by accident. THE PICKER MAY OFFER WHAT THE MINT REFUSES, AND THE SURFACE OWNS THE DIVERGENCE (0.34.0). Rule 3 applies on the MINT path and not the picker path, which is deliberate: the picker cannot cheaply know the answer, since knowing it requires reading other products. The consequence is that a surface can present a choice that then fails. The owner is named here rather than left implicit: A CREATE SURFACE THAT OFFERS A PREFIX THE MINT MAY REFUSE MUST BE ABLE TO REPORT THE REFUSAL, AND MUST BE ABLE TO PRESENT NO-KEY AS AN OUTCOME RATHER THAN AS AN ERROR. This is a design obligation on the surface, not a spec mechanic: the spec cannot fix a UX gap and should not pretend to. The union ruling above shrinks the divergence considerably, because the picker now keeps asking in exactly the case that would otherwise resolve wrongly. This rule states the CONTRACT. The behaviour belongs to whatever mints keys, which is not this package: see `UPGBaseNode.key`." }
14510
14657
  },
14511
14658
  // TeamOkrProperties: TeamOkr entity.
14512
14659
  team_okr: {
@@ -14832,8 +14979,8 @@ var UPG_PROPERTY_SCHEMA = {
14832
14979
  },
14833
14980
  // WorkspaceProperties: Workspace: a spatial thinking space for arranging entities.
14834
14981
  workspace: {
14835
- member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
14836
- presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
14982
+ member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement. The clause list is authoritative and the named fields are a positive-only projection of it; since 0.34.0 a clause is a discriminated union on `dimension`, so the `type` axis carries entity types rather than free strings.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
14983
+ presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole: `group_by`, `sort`, `layout`, `nest_by`, and `orphan_disposition` (0.34.0, absent means `'root'`). A consumer may ignore it entirely and still be conformant, because every default it then applies is the safe one.", notes: "THE DESCRIPTION LISTS THE FIELDS ON PURPOSE. This property is `object` in the runtime property registry, so an agent reading `get_entity_schema` gets an opaque blob and this sentence. For an object-typed property the description IS the declared shape, which is why `check:editorial` hashes it (E.4, 0.34.0) and why a field added to `UPGViewPresentation` without a word here would be invisible to every gate and every agent at once." },
14837
14984
  visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
14838
14985
  purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
14839
14986
  workspace_purpose: { type: "string", enum: ["discovery", "planning", "retrospective", "design", "research", "strategy", "general"], description: "What the workspace is for. Drives template suggestions and surfaces in workspace browsers.", notes: "`discovery` is persona, job and opportunity exploration. `planning` covers roadmap and decision sessions. `retrospective` is reflection on shipped work. `design` is experience or UI exploration. `research` organises study data and synthesis. `strategy` is high-level direction setting. `general` is the catch-all." },
@@ -29301,6 +29448,48 @@ var UPG_ANTI_PATTERNS = [
29301
29448
  severity: "medium",
29302
29449
  source: { kind: "fundamental" }
29303
29450
  },
29451
+ // ── Citable-key collision (0.34.0): portfolio-scoped, and the FIRST detector
29452
+ // to ship with a labeled corpus behind it (packages/upg-evals/corpora/
29453
+ // intelligence/duplicate-key-across-products/).
29454
+ //
29455
+ // WHY IT CANNOT BE A SINGLE-GRAPH CHECK. Key uniqueness is enforced by a
29456
+ // `(product_id, key)` index, so each product reports its own key as valid. The
29457
+ // collision is only visible from above, which is why this is `scope: 'portfolio'`
29458
+ // and carries no structured_condition: the detector reads across products, and
29459
+ // IntelligenceCondition composes aggregate counts within one graph.
29460
+ //
29461
+ // MECHANICALLY DECIDABLE, which is unusual in this file and is why it can ship
29462
+ // enforced: one `(prefix, number)` pair appearing in two products of one
29463
+ // portfolio, with no judgement about intent. Every other portfolio entry here
29464
+ // approximates a per-node rule; this one is exact.
29465
+ //
29466
+ // ENFORCED FROM DAY ONE. What was staged is the CORPUS, not the enforcement.
29467
+ // The tempting shape was a check that ships "defined" and becomes "enforced"
29468
+ // later, and there is no such mechanic: a registered detector that declines to
29469
+ // fire is worse than either real option, and nothing in the spec could hang it.
29470
+ // The detector has little to fire on until a second keyed product exists, and
29471
+ // that is a property of the estate rather than of the check.
29472
+ //
29473
+ // THE NEAR-MISSES ARE CONSTRUCTED, and the corpus says so in its own README
29474
+ // rather than in a plan nobody opens. Three classes: two products sharing a
29475
+ // prefix over DISJOINT number ranges (latent, not yet colliding); a
29476
+ // DELIBERATELY shared prefix across a product family wanting one citation
29477
+ // namespace; and a product holding IMPORTED keys minted elsewhere. Until a real
29478
+ // collision is sampled, the recall figure is a claim about the author's
29479
+ // imagination. The corpus carries a dated obligation to re-grade precision and
29480
+ // recall on the first sampled collision.
29481
+ {
29482
+ id: "duplicate-key-across-products",
29483
+ since: "0.34.0",
29484
+ scope: "portfolio",
29485
+ name: "One citable key, two products",
29486
+ description: "The same (prefix, number) pair identifies a node in two or more products of one portfolio. Keys are minted per product and uniqueness is enforced by a (product_id, key) index, so two products minting under one prefix run two independent sequences under one name. Each product reports its own key as valid and only the portfolio can see that the citation is ambiguous.",
29487
+ why_it_matters: "A key exists to be cited. When one citation resolves to two different things, every reference made with it becomes ambiguous after the fact, including references already written down outside the graph where nothing can be corrected. The damage is retroactive and grows with adoption: the longer both sequences run, the more citations a reconciliation has to rewrite.",
29488
+ remediation: "Decide which product owns the prefix and declare it there with team.key_prefix, so ownership is explicit rather than inferred. Migrate the other product by rewriting its existing keys, which is deliberate and visibly costly, never by editing a declaration under a live sequence. If both products want one namespace, record that decision so it does not read as drift.",
29489
+ stages: ["build", "beta", "launch", "growth", "mature"],
29490
+ severity: "high",
29491
+ source: { kind: "fundamental" }
29492
+ },
29304
29493
  // ── surface (0.27.0): the place, its guest list, and its arbitration rule ──
29305
29494
  // Three patterns, calibrated so the contention one leads and the two coverage
29306
29495
  // companions sit a tier below it. Like every entry in this file, each detector
@@ -33045,8 +33234,8 @@ var NODE_KEY_ORDER = [
33045
33234
  "sort_order",
33046
33235
  "properties"
33047
33236
  ];
33048
- var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence", "properties"];
33049
- var CROSS_EDGE_KEY_ORDER = ["id", "source", "target", "type", "source_product_id", "target_product_id", "mapping_confidence", "alias", "relevance", "audience_role"];
33237
+ var EDGE_KEY_ORDER = ["id", "source", "target", "type", "mapping_confidence", "provenance", "properties"];
33238
+ var CROSS_EDGE_KEY_ORDER = ["id", "source", "target", "type", "source_product_id", "target_product_id", "mapping_confidence", "properties", "alias", "relevance", "audience_role"];
33050
33239
  var PRODUCT_KEY_ORDER = ["id", "title", "description", "stage", "properties"];
33051
33240
  function canonicalNode(node) {
33052
33241
  const repaired = repairNodeDrift(node);
@@ -33063,7 +33252,14 @@ function canonicalEdge(edge) {
33063
33252
  }
33064
33253
  function canonicalCrossEdge(edge) {
33065
33254
  return orderedObject(edge, CROSS_EDGE_KEY_ORDER, {
33066
- forceKeys: ["id", "source", "target", "type"]
33255
+ forceKeys: ["id", "source", "target", "type"],
33256
+ // `properties` is an OPEN bag and must be canonicalised like every other one
33257
+ // (node, edge, product all pass it). Without this, two writers emitting the
33258
+ // same parity assessment with their keys in a different order produce
33259
+ // different bytes, which breaks contract 3 of this format: input key order
33260
+ // does not change the output. Free to fix today and measured as such: zero
33261
+ // of the 61 cross-edges in the estate carry properties.
33262
+ openKeys: ["properties"]
33067
33263
  });
33068
33264
  }
33069
33265
  function canonicalProduct(product) {
@@ -33324,7 +33520,7 @@ function checkHeaderSealText(text) {
33324
33520
  }
33325
33521
 
33326
33522
  // src/index.ts
33327
- var UPG_VERSION = "0.33.0";
33523
+ var UPG_VERSION = "0.34.0";
33328
33524
  var UPG_FORMAT_VERSION = "0.4.0";
33329
33525
  var MARKDOWN_FORMAT_VERSION = "0.1";
33330
33526
  var UPG_TYPES = getTypes();
@@ -33387,6 +33583,14 @@ var UPG_ENTITY_COUNT = UPG_TYPES.length;
33387
33583
  var UPG_DOMAIN_COUNT = UPG_DOMAINS.length;
33388
33584
  var UPG_EDGE_COUNT = UPG_EDGE_TYPES.length;
33389
33585
  var UPG_META_COUNT = UPG_ENTITY_META.length;
33586
+ var UPG_PHASE_COUNT = (() => {
33587
+ const pairs = /* @__PURE__ */ new Set();
33588
+ for (const lifecycle of UPG_LIFECYCLES) {
33589
+ const owner = lifecycle.template_id ? `T:${lifecycle.template_id}` : `E:${lifecycle.entity_type}`;
33590
+ for (const phase of lifecycle.phases) pairs.add(`${owner}::${phase.id}`);
33591
+ }
33592
+ return pairs.size;
33593
+ })();
33390
33594
  // Annotate the CommonJS export names for ESM import in node:
33391
33595
  0 && (module.exports = {
33392
33596
  ACTIVE_WHEN_PROPERTY,
@@ -33399,10 +33603,12 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
33399
33603
  CONFIGURATION_VARIANCE_EDGE_PROPERTY_SCHEMA,
33400
33604
  CONTENT_DEPTH_WARNING_RULES,
33401
33605
  COVERAGE_ANTI_PATTERNS,
33606
+ CROSS_EDGE_KEY_ORDER,
33402
33607
  DEFAULT_ENTITY_EMOJI,
33403
33608
  DEFERRED_PROPOSED_BY_CONTRACT,
33404
33609
  DEFER_EDGE_PROPERTY_SCHEMA,
33405
33610
  DISCOVERY_RESEARCH_VALIDATION_PLAYBOOK,
33611
+ EDGE_KEY_ORDER,
33406
33612
  ENGINEERING_PLATFORM_PLAYBOOK,
33407
33613
  ENTITY_EMOJI,
33408
33614
  EXPERIENCE_DESIGN_BRAND_PLAYBOOK,
@@ -33470,7 +33676,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
33470
33676
  UPG_LIFECYCLE_PLANNED_TYPES,
33471
33677
  UPG_META_COUNT,
33472
33678
  UPG_MIGRATIONS,
33679
+ UPG_PHASE_COUNT,
33473
33680
  UPG_PLAYBOOKS,
33681
+ UPG_POLYMORPHIC_EDGE_FAMILIES,
33474
33682
  UPG_POLYMORPHIC_EDGE_KEYS,
33475
33683
  UPG_PORTFOLIO_KINDS,
33476
33684
  UPG_PORTFOLIO_SHARED_TYPES,