@unified-product-graph/core 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -74,8 +74,8 @@ var UPG_DOMAINS = [
74
74
  {
75
75
  id: "ux_design",
76
76
  label: "Experience Design",
77
- description: "How users experience and interact with your product. User journeys map end-to-end experiences, broken into journey steps. User flows chart navigation paths. Screens and screen states define what users see. Design questions frame open problems. Design concepts explore possible solutions. Prototypes and wireframes make ideas tangible. Connects User (who) to Product Specification (what) through Design System (how).",
78
- types: ["user_journey", "journey_step", "journey_phase", "journey_action", "user_flow", "screen", "screen_state", "design_question", "design_concept", "prototype", "wireframe"]
77
+ description: "How users experience and interact with your product. User journeys map end-to-end experiences, broken into journey steps. User flows chart navigation paths. Screens and screen states define what users see. Surfaces name the places inside a screen, who may occupy them, and the rule that settles contention. Design questions frame open problems. Design concepts explore possible solutions. Prototypes and wireframes make ideas tangible. Connects User (who) to Product Specification (what) through Design System (how).",
78
+ types: ["user_journey", "journey_step", "journey_phase", "journey_action", "user_flow", "screen", "screen_state", "surface", "design_question", "design_concept", "prototype", "wireframe"]
79
79
  },
80
80
  {
81
81
  id: "design_system",
@@ -641,6 +641,11 @@ var UPG_ENTITY_META = [
641
641
  { name: "user_flow", type_id: "ent_068", maturity: "stable", since: "0.1.0" },
642
642
  { name: "screen", type_id: "ent_069", maturity: "stable", since: "0.1.0" },
643
643
  { name: "screen_state", type_id: "ent_070", maturity: "stable", since: "0.1.0" },
644
+ // surface (0.27.0): the place inside a screen. `screen` is route-level
645
+ // (route / viewport / access_level); `surface` is the contested slot within
646
+ // it, carrying its occupants and the rule that arbitrates between them.
647
+ // Enters `proposed` per the maturity rubric (precedent: planning_cycle 0.20.0).
648
+ { name: "surface", type_id: "ent_359", maturity: "proposed", since: "0.27.0" },
644
649
  { name: "journey_phase", type_id: "ent_330", maturity: "proposed", since: "0.2.0" },
645
650
  { name: "journey_action", type_id: "ent_331", maturity: "proposed", since: "0.2.0" },
646
651
  { name: "design_decision", type_id: "ent_319", maturity: "deprecated", since: "0.2.0", deprecated_in: "0.2.0", replacement: "decision" },
@@ -1533,6 +1538,57 @@ var UPG_EDGE_CATALOG = {
1533
1538
  // edge mirrors `journey_step_realised_by_feature` one level deeper.
1534
1539
  journey_action_surfaces_need: { forward_verb: "surfaces", reverse_verb: "surfaced_in", classification: "cross-domain", source_type: "journey_action", target_type: "need" },
1535
1540
  journey_action_realised_by_feature: { forward_verb: "realised_by", reverse_verb: "realises", classification: "cross-domain", source_type: "journey_action", target_type: "feature" },
1541
+ // ── surface (0.27.0): the place inside a screen ─────────────────────────
1542
+ // `screen` is route-level; `surface` is the contested place within it. The
1543
+ // ten edges below give a surface its spine (nesting), its guest list, its
1544
+ // purpose, its governing rule, its rendering vocabulary, its measurement,
1545
+ // and its replacement path.
1546
+ //
1547
+ // Classification adjudications, each anchored on the nearest precedent:
1548
+ // contains → hierarchy (feature_area_contains_feature_area, planning_cycle_contains_planning_cycle)
1549
+ // serves job → cross-domain (ux_design → users_needs; cf. user_journey_addresses_job).
1550
+ // Verb is `serves`, not `addresses`: a feature ADDRESSES a job
1551
+ // (it satisfies the struggle); a surface SERVES it (it is the
1552
+ // place where the job gets done). `serves`/`served_by` is an
1553
+ // established house pair (service_serves_api_endpoint,
1554
+ // api_endpoint_serves_feature, product_serves_account).
1555
+ // governed_by → cross-domain (ux_design → design_system). The verb precedent
1556
+ // design_component_governed_by_design_guideline is `hierarchy`,
1557
+ // but both of its endpoints sit inside design_system; once the
1558
+ // seam is crossed the catalog uses cross-domain
1559
+ // (insight_informs_design_guideline). cross_product_eligible:
1560
+ // the guideline usually lives in the shared design-system graph.
1561
+ // renders comp. → hierarchy + cross_product_eligible, matching its direct
1562
+ // sibling screen_renders_design_component verbatim.
1563
+ // measured_by → semantic + cross_product_eligible. The enumerated
1564
+ // *_measured_by_metric convention splits on OWNERSHIP, not on the
1565
+ // verb: outcome / objective / strategic_pillar are `hierarchy`
1566
+ // because those entities own their metrics as children (each has a
1567
+ // UPG_VALID_CHILDREN entry to match, which the hierarchy-integrity
1568
+ // guardrail enforces). A surface does not own a metric any more
1569
+ // than a revenue_stream does, so it takes the `semantic` half of
1570
+ // the convention (revenue_stream / cost_structure). It stays
1571
+ // cross_product_eligible with the rest: a metric DEFINITION is
1572
+ // portfolio_shared, so the reading it names commonly lives in the
1573
+ // registry rather than in the product graph.
1574
+ // supersedes → semantic (prompt_version_supersedes_prompt_version).
1575
+ surface_contains_surface: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "surface", target_type: "surface" },
1576
+ surface_serves_job: { forward_verb: "serves", reverse_verb: "served_by", classification: "cross-domain", source_type: "surface", target_type: "job" },
1577
+ surface_governed_by_design_guideline: { forward_verb: "governed_by", reverse_verb: "governs", classification: "cross-domain", source_type: "surface", target_type: "design_guideline", cross_product_eligible: true },
1578
+ surface_renders_design_component: { forward_verb: "renders", reverse_verb: "rendered_on", classification: "hierarchy", source_type: "surface", target_type: "design_component", cross_product_eligible: true },
1579
+ surface_measured_by_metric: { forward_verb: "measured_by", reverse_verb: "measures", classification: "semantic", source_type: "surface", target_type: "metric", cross_product_eligible: true },
1580
+ surface_supersedes_surface: { forward_verb: "supersedes", reverse_verb: "superseded_by", classification: "semantic", source_type: "surface", target_type: "surface" },
1581
+ // Inbound. `feature_occupies_surface` is the guest list: the edge that makes
1582
+ // "what else lives here?" answerable, and the one the contention anti-pattern
1583
+ // counts. cross-domain (product_spec → ux_design), like screen_surfaces_feature
1584
+ // running the other way.
1585
+ feature_occupies_surface: { forward_verb: "occupies", reverse_verb: "occupied_by", classification: "cross-domain", source_type: "feature", target_type: "surface" },
1586
+ screen_renders_surface: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "hierarchy", source_type: "screen", target_type: "surface" },
1587
+ decision_affects_surface: { forward_verb: "affects", reverse_verb: "affected_by", classification: "cross-domain", source_type: "decision", target_type: "surface" },
1588
+ // The finer-grained sibling of journey_step_shown_on_screen: a step happens on
1589
+ // a PLACE, not just a route. Same semantic classification as that edge (a step
1590
+ // is not contained by the surface; it references where it happens).
1591
+ journey_step_occurs_on_surface: { forward_verb: "occurs_on", reverse_verb: "hosts", classification: "semantic", source_type: "journey_step", target_type: "surface" },
1536
1592
  // 2.5 UI System Domain
1537
1593
  product_systematised_in_design_system: { forward_verb: "systematised_in", reverse_verb: "systematises", classification: "hierarchy", source_type: "product", target_type: "design_system" },
1538
1594
  product_built_with_design_component: { forward_verb: "built_with", reverse_verb: "built_for", classification: "hierarchy", source_type: "product", target_type: "design_component" },
@@ -4054,8 +4110,24 @@ var UPG_VALID_CHILDREN = {
4054
4110
  "user_flow",
4055
4111
  "insight"
4056
4112
  ],
4057
- screen: ["screen_state", "screen", "design_component", "wireframe"],
4058
- // self-nesting + components + wireframes
4113
+ screen: ["screen_state", "screen", "surface", "design_component", "wireframe"],
4114
+ // self-nesting + surfaces + components + wireframes
4115
+ // surface (0.27.0): the place inside a screen. Self-nests, so a shell holds
4116
+ // panes, a pane holds regions, a region holds slots. A surface is reached
4117
+ // through the screen that renders it (`screen: [... 'surface']`), never as a
4118
+ // top-level product child: every UPG_VALID_CHILDREN pair must be backed by a
4119
+ // hierarchy-classified edge (guardrail G2b), and the ratified ten do not
4120
+ // include a product-to-surface verb. An app-shell surface therefore hangs off
4121
+ // the screen that mounts it.
4122
+ //
4123
+ // NOTE: `surface_kind` narrows this further than UPG_VALID_CHILDREN can
4124
+ // express. UPG_VALID_CHILDREN is keyed on entity TYPE, so it can only say
4125
+ // "a surface may contain a surface"; the kind ordering (shell > tool > pane >
4126
+ // region > slot / gutter / action_bar, with overlay and ambient parented by
4127
+ // whatever raises them) is a property-level rule documented on `SurfaceKind`
4128
+ // and enforced by validators, not by this table. Precedent: the same split
4129
+ // applies to `planning_cycle`, whose `cadence_kind` narrows its self-nesting.
4130
+ surface: ["surface", "design_component"],
4059
4131
  // ── Marketing & Communications hierarchy ────────────────────────────────────
4060
4132
  marketing_strategy: [
4061
4133
  "marketing_channel",
@@ -5127,6 +5199,48 @@ var SCREEN_LIFECYCLE = {
5127
5199
  }
5128
5200
  ]
5129
5201
  };
5202
+ var SURFACE_LIFECYCLE = {
5203
+ entity_type: "surface",
5204
+ initial_phase: "draft",
5205
+ terminal_phases: ["deprecated"],
5206
+ phases: [
5207
+ {
5208
+ id: "draft",
5209
+ status_category: "started",
5210
+ label: "Draft",
5211
+ description: "The surface is a rough idea: named and scoped, but not yet worked in a design tool. Its occupants and arbitration rule are usually still undecided.",
5212
+ transitions_to: ["in_design"]
5213
+ },
5214
+ {
5215
+ id: "in_design",
5216
+ status_category: "started",
5217
+ label: "In Design",
5218
+ description: "The surface is being designed: its dimensions, occupants, and arbitration rule are being settled. May fall back to `draft` if the approach is rethought from scratch.",
5219
+ transitions_to: ["built", "draft"]
5220
+ },
5221
+ {
5222
+ id: "built",
5223
+ status_category: "started",
5224
+ label: "Built",
5225
+ description: "The surface has been implemented in code. It exists in a build but is not yet live for users. May return to `in_design` if implementation surfaces a layout or contention gap.",
5226
+ transitions_to: ["shipped", "in_design"]
5227
+ },
5228
+ {
5229
+ id: "shipped",
5230
+ status_category: "completed",
5231
+ label: "Shipped",
5232
+ description: "The surface is live in production and reachable by users. Its occupants are the real guest list.",
5233
+ transitions_to: ["deprecated"]
5234
+ },
5235
+ {
5236
+ id: "deprecated",
5237
+ status_category: "completed",
5238
+ label: "Deprecated",
5239
+ description: "The surface has been retired. Its occupants have moved elsewhere, or a replacement has taken the place; record the replacement with `surface_supersedes_surface`.",
5240
+ transitions_to: []
5241
+ }
5242
+ ]
5243
+ };
5130
5244
  var SERVICE_LIFECYCLE = {
5131
5245
  entity_type: "service",
5132
5246
  initial_phase: "development",
@@ -6328,6 +6442,7 @@ var UPG_LIFECYCLES = [
6328
6442
  DESIGN_CONCEPT_LIFECYCLE,
6329
6443
  BRAND_IDENTITY_LIFECYCLE,
6330
6444
  SCREEN_LIFECYCLE,
6445
+ SURFACE_LIFECYCLE,
6331
6446
  // Engineering & Operations
6332
6447
  SERVICE_LIFECYCLE,
6333
6448
  DEPLOYMENT_LIFECYCLE,
@@ -13057,6 +13172,17 @@ var UPG_PROPERTY_SCHEMA = {
13057
13172
  signal_channel: { type: "string", description: "Channel through which the signal was received" },
13058
13173
  signal_urgency: { type: "string", enum: ["low", "medium", "high", "critical"], description: "Perceived urgency of the customer's request" }
13059
13174
  },
13175
+ // SurfaceProperties: A place in the UI, its occupants, and the rule that arbitrates between them.
13176
+ surface: {
13177
+ surface_kind: { type: "string", enum: ["shell", "tool", "pane", "region", "slot", "gutter", "action_bar", "overlay", "ambient"], description: "Structural kind. Determines what may legally nest inside this surface." },
13178
+ persistence: { type: "string", enum: ["always", "conditional", "on_demand", "transient"], description: "How reliably the surface is present." },
13179
+ visibility_condition: { type: "string", description: 'When the surface appears, in plain language. Pairs with `persistence: \'conditional\'`, which states *that* it is conditional; this states *what* the condition is. @example "A node is selected", "Only for workspace admins"' },
13180
+ capacity: { type: "number", description: 'How many occupants the surface holds at once, as a non-negative count. ABSENT MEANS UNBOUNDED. UPG has no union-typed property primitive (`PropertyDefinition.type` is a single scalar kind), so the proposed `integer | unbounded` shape is expressed as an optional number whose absence carries the "no cap" reading, rather than a magic sentinel value that every consumer would have to special-case. `0` is a real cap meaning "nothing may occupy this surface" (a reserved place), not "unbounded".' },
13181
+ arbitration_rule: { type: "string", description: 'Who wins when more occupants want the surface than `capacity` allows, and why. ABSENCE IS MEANINGFUL: a null or empty `arbitration_rule` on a contested surface means nobody decided, which is exactly what the `contended-surface-without-arbitration` anti-pattern detects. Do not fill this in with a placeholder to silence the check. @example "Highest priority wins; ties break to the most recently updated."' },
13182
+ extensibility: { type: "string", enum: ["closed", "plugin_registerable", "user_configurable"], description: "Who may add occupants to the surface." },
13183
+ mutates_content: { type: "boolean", description: "Whether occupying this surface can change the underlying content, as opposed to only selecting or revealing it. The selector-versus-mutator distinction: a gutter that toggles a value is a mutator, a gutter that jumps the cursor is not." },
13184
+ dimensional_constraint: { type: "string", description: 'The hard spatial budget the surface imposes on its occupants, in whatever unit the design system speaks. @example "292px wide", "25px per field", "two grid columns"' }
13185
+ },
13060
13186
  // SurveyResponseProperties: Aggregated survey response data.
13061
13187
  survey_response: {
13062
13188
  response_count: { type: "number", description: "Total responses", modifier: "snapshot" },
@@ -20033,6 +20159,7 @@ var ENTITY_EMOJI = {
20033
20159
  sales_motion: "\u{1F501}",
20034
20160
  screen: "\u{1F5A5}\uFE0F",
20035
20161
  screen_state: "\u{1F533}",
20162
+ surface: "\u{1FA9F}",
20036
20163
  security_audit: "\u{1F6C3}",
20037
20164
  security_control: "\u{1F39A}\uFE0F",
20038
20165
  security_policy: "\u{1F510}",
@@ -20656,6 +20783,7 @@ var STANDARD_LABELS = {
20656
20783
  design_system: { alt_labels: ["component library", "style system", "ui kit"] },
20657
20784
  screen: { alt_labels: ["page", "view", "route", "ui state"] },
20658
20785
  screen_state: { alt_labels: ["view state", "empty state", "loading state", "error state"] },
20786
+ surface: { alt_labels: ["slot", "pane", "region", "zone", "shell", "panel", "placement", "ui surface"] },
20659
20787
  // Brand layer
20660
20788
  brand_identity: { alt_labels: ["brand", "brand guidelines", "brand book"] },
20661
20789
  brand_imagery: { alt_labels: ["brand image", "visual asset", "brand photo"] },
@@ -23237,22 +23365,30 @@ var UX_DESIGN_GUIDE = {
23237
23365
  // of a journey_step, so the step must precede the action. The journey is the
23238
23366
  // anchor; phases are the band overlay; steps are the timeline; actions
23239
23367
  // decompose a step into service-blueprint rows.
23240
- creation_sequence: ["user_journey", "journey_phase", "journey_step", "journey_action", "screen", "screen_state", "user_flow", "wireframe", "prototype", "design_question", "design_concept"],
23368
+ creation_sequence: ["user_journey", "journey_phase", "journey_step", "journey_action", "screen", "screen_state", "surface", "user_flow", "wireframe", "prototype", "design_question", "design_concept"],
23241
23369
  patterns: [
23242
23370
  {
23243
23371
  name: "Journey to Screen Flow",
23244
23372
  description: "Journeys map the experience, journey steps are shown on screens, prototypes simulate screens so the design can be tested",
23245
23373
  entity_types: ["user_journey", "journey_step", "screen", "prototype"],
23246
23374
  edge_chain: ["persona_experiences_user_journey", "user_journey_contains_journey_step", "journey_step_shown_on_screen", "prototype_simulates_screen"]
23375
+ },
23376
+ {
23377
+ name: "Surface Contention",
23378
+ description: "A screen renders surfaces, features occupy them, and the surface records the rule that settles who wins. Nest the place (shell, pane, region, slot), list the occupants, then write the arbitration rule down once so it is not re-argued at the next feature.",
23379
+ entity_types: ["screen", "surface", "feature"],
23380
+ edge_chain: ["screen_renders_surface", "surface_contains_surface", "feature_occupies_surface", "surface_serves_job"]
23247
23381
  }
23248
23382
  ],
23249
23383
  required_bridges: [
23250
23384
  { edge_type: "persona_experiences_user_journey", target_domain: "user", when: "Every journey must be anchored to a persona" },
23385
+ { edge_type: "feature_occupies_surface", target_domain: "product_spec", when: "A surface should list the features that occupy it; the guest list is what makes contention visible" },
23251
23386
  { edge_type: "prototype_tests_hypothesis", target_domain: "validation", when: "Prototypes should test design hypotheses before building" },
23252
23387
  { edge_type: "screen_surfaces_feature", target_domain: "product_spec", when: "Screens should connect to the features they surface" }
23253
23388
  ],
23254
23389
  anti_patterns: [
23255
23390
  { description: "Screens without journeys: isolated screens miss the experience context" },
23391
+ { description: "Surfaces without an arbitration rule: a place two features both want, with no recorded answer for who wins, is a decision that gets made again every time it comes up. Fill `arbitration_rule`, or link the design guideline that already answers it via surface_governed_by_design_guideline." },
23256
23392
  { description: "Prototypes without testing: if nobody tests the prototype, it is just art" },
23257
23393
  { description: "Design questions left open: exploration status should progress to resolved or parked" },
23258
23394
  { description: "Touchpoints stuffed in the deprecated journey_step.touchpoint string (UPG-675). Touchpoints belong in one of two layers: journey_action is the in-product blueprint layer (the finest band of a journey_step), and the touchpoint entity is the cross-channel customer-success layer (touchpoint_occurs_in_journey_step). Pick the layer; do not duplicate the touch as a free-text string." }
@@ -26867,6 +27003,99 @@ var UPG_ANTI_PATTERNS = [
26867
27003
  stages: ["concept", "validation", "build", "beta", "launch", "growth", "mature", "maintenance", "sunset"],
26868
27004
  severity: "medium",
26869
27005
  source: { kind: "fundamental" }
27006
+ },
27007
+ // ── surface (0.27.0): the place, its guest list, and its arbitration rule ──
27008
+ // Three patterns, calibrated so the contention one leads and the two coverage
27009
+ // companions sit a tier below it. Like every entry in this file, each detector
27010
+ // is a WHOLE-GRAPH approximation of a per-node rule (cf. personas-without-jobs,
27011
+ // which fires on "no persona anywhere links to a job", not per persona): the
27012
+ // `IntelligenceCondition` language composes aggregate counts, not per-node
27013
+ // predicates.
27014
+ {
27015
+ id: "contended-surface-without-arbitration",
27016
+ since: "0.27.0",
27017
+ name: "Contended surface without arbitration",
27018
+ description: 'Features occupy surfaces in this graph, but at least one surface carries no arbitration_rule. A surface is a place features compete for; the arbitration rule is the recorded answer to "who wins here, and why". Left blank, that answer lives in whoever remembers the last argument.',
27019
+ structured_condition: {
27020
+ operator: "and",
27021
+ checks: [
27022
+ { check: { type: "entity_count", entity_type: "surface", comparison: "nonzero" } },
27023
+ {
27024
+ check: {
27025
+ type: "relationship",
27026
+ source_type: "feature",
27027
+ edge_type: "feature_occupies_surface",
27028
+ target_type: "surface",
27029
+ comparison: "exists"
27030
+ }
27031
+ },
27032
+ {
27033
+ check: {
27034
+ type: "entity_count",
27035
+ entity_type: "surface",
27036
+ filter: { property: "arbitration_rule", present: false },
27037
+ comparison: "nonzero"
27038
+ }
27039
+ }
27040
+ ]
27041
+ },
27042
+ why_it_matters: "Contention over a UI place is settled every time it comes up, and an unrecorded settlement is re-argued at the next feature. The cost is paid in repeated decisions, not in a visible defect.",
27043
+ remediation: "For each surface with more than one `feature_occupies_surface` edge, write the rule that settles contention into `arbitration_rule`, and record the constraint it works within (`capacity`, `dimensional_constraint`). Where a design system already answers it, link the source with `surface_governed_by_design_guideline`.",
27044
+ stages: ["build", "beta", "launch", "growth", "mature", "maintenance"],
27045
+ severity: "medium",
27046
+ source: { kind: "fundamental" }
27047
+ },
27048
+ {
27049
+ id: "surface-without-job",
27050
+ since: "0.27.0",
27051
+ name: "Surface without a job",
27052
+ description: "The graph has surfaces, but none links to the job it exists to serve. A surface with no job is a place that survives on precedent: it is there because it has always been there.",
27053
+ structured_condition: {
27054
+ operator: "and",
27055
+ checks: [
27056
+ { check: { type: "entity_count", entity_type: "surface", comparison: "nonzero" } },
27057
+ {
27058
+ check: {
27059
+ type: "relationship",
27060
+ source_type: "surface",
27061
+ edge_type: "surface_serves_job",
27062
+ target_type: "job",
27063
+ comparison: "not_exists"
27064
+ }
27065
+ }
27066
+ ]
27067
+ },
27068
+ why_it_matters: "Without a job, there is no test for whether a surface still earns its space. Places accumulate, and the layout argument becomes a matter of taste rather than of purpose.",
27069
+ remediation: "Link each surface to the job it serves with `surface_serves_job`. A surface that cannot name one is a candidate for removal or for merging into its parent.",
27070
+ stages: ["build", "beta", "launch", "growth", "mature"],
27071
+ severity: "low",
27072
+ source: { kind: "practitioner", attribution: "Clayton Christensen, Jobs to Be Done" }
27073
+ },
27074
+ {
27075
+ id: "surface-without-measurement",
27076
+ since: "0.27.0",
27077
+ name: "Surface without measurement",
27078
+ description: "The graph has surfaces, but none links to a metric. Nothing reports whether the place is used, ignored, or in the way.",
27079
+ structured_condition: {
27080
+ operator: "and",
27081
+ checks: [
27082
+ { check: { type: "entity_count", entity_type: "surface", comparison: "nonzero" } },
27083
+ {
27084
+ check: {
27085
+ type: "relationship",
27086
+ source_type: "surface",
27087
+ edge_type: "surface_measured_by_metric",
27088
+ target_type: "metric",
27089
+ comparison: "not_exists"
27090
+ }
27091
+ }
27092
+ ]
27093
+ },
27094
+ why_it_matters: "An unmeasured surface cannot be retired on evidence. It is defended by whoever built it and challenged by whoever wants its space, with no reading to settle the question.",
27095
+ remediation: "Attach the reading that would justify keeping the place with `surface_measured_by_metric`. Engagement, or the rate at which its occupants are actually invoked, is usually the honest one.",
27096
+ stages: ["build", "beta", "launch", "growth", "mature"],
27097
+ severity: "low",
27098
+ source: { kind: "fundamental" }
26870
27099
  }
26871
27100
  ];
26872
27101
  function getAntiPatternById(id) {
@@ -26911,7 +27140,14 @@ function concernGatesFor(kind, concern) {
26911
27140
  var COVERAGE_ANTI_PATTERNS = /* @__PURE__ */ new Set([
26912
27141
  "single-domain-graph",
26913
27142
  "persona-count-below-stage-benchmark",
26914
- "competitors-missing-past-validation"
27143
+ "competitors-missing-past-validation",
27144
+ // surface companions (0.27.0): both presuppose the graph has grown past the
27145
+ // point where a place is worth justifying and measuring. A stub that has
27146
+ // sketched one surface has not yet drifted. `contended-surface-without-
27147
+ // arbitration` is deliberately NOT here: it only fires once features actually
27148
+ // occupy surfaces, which is itself the evidence that the graph has grown.
27149
+ "surface-without-job",
27150
+ "surface-without-measurement"
26915
27151
  ]);
26916
27152
  var THIN_GRAPH_THRESHOLD = 8;
26917
27153
  function isThinCoverageAdvisory(antiPatternId, totalEntityCount) {
@@ -27009,7 +27245,10 @@ function compareNumber(value, comparison, threshold) {
27009
27245
  function evaluateEntityCount(check, inputs) {
27010
27246
  const filter = check.filter;
27011
27247
  let count = 0;
27012
- if (filter && typeof filter.property === "string" && typeof filter.value === "string") {
27248
+ if (filter && typeof filter.property === "string" && typeof filter.present === "boolean") {
27249
+ const withValue = inputs.countsByTypeAndPropertyPresence?.[check.entity_type]?.[filter.property] ?? 0;
27250
+ count = filter.present ? withValue : Math.max(0, (inputs.countsByType[check.entity_type] ?? 0) - withValue);
27251
+ } else if (filter && typeof filter.property === "string" && typeof filter.value === "string") {
27013
27252
  count = inputs.countsByTypeAndProperty?.[check.entity_type]?.[filter.property]?.[filter.value] ?? 0;
27014
27253
  } else if (filter && typeof filter.status === "string") {
27015
27254
  count = inputs.countsByTypeAndStatus?.[check.entity_type]?.[filter.status] ?? 0;
@@ -27914,6 +28153,10 @@ var UPG_REGIONS = [
27914
28153
  type: "screen_state",
27915
28154
  role: "leaf"
27916
28155
  },
28156
+ {
28157
+ type: "surface",
28158
+ role: "container"
28159
+ },
27917
28160
  {
27918
28161
  type: "wireframe",
27919
28162
  role: "leaf"
@@ -28022,7 +28265,9 @@ var UPG_REGIONS = [
28022
28265
  "user_journey_contains_journey_step",
28023
28266
  "design_system_contains_design_component",
28024
28267
  "brand_identity_coloured_with_brand_colour",
28025
- "design_component_composes_design_component"
28268
+ "design_component_composes_design_component",
28269
+ "screen_renders_surface",
28270
+ "surface_contains_surface"
28026
28271
  ],
28027
28272
  boundary_edges: [
28028
28273
  {
@@ -28030,6 +28275,16 @@ var UPG_REGIONS = [
28030
28275
  edge_id: "need_reframed_as_design_question",
28031
28276
  crosses_into: "users_needs"
28032
28277
  },
28278
+ {
28279
+ direction: "import",
28280
+ edge_id: "feature_occupies_surface",
28281
+ crosses_into: "product_delivery"
28282
+ },
28283
+ {
28284
+ direction: "export",
28285
+ edge_id: "surface_serves_job",
28286
+ crosses_into: "users_needs"
28287
+ },
28033
28288
  {
28034
28289
  direction: "import",
28035
28290
  edge_id: "opportunity_explores_via_design_concept",
@@ -29905,6 +30160,28 @@ function portfolioBody(doc) {
29905
30160
  ...doc.signals && doc.signals.length > 0 ? { signals: sortNodes(doc.signals).map(canonicalNode) } : {}
29906
30161
  };
29907
30162
  }
30163
+ function deriveSingleCounts(doc) {
30164
+ return { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
30165
+ }
30166
+ function derivePortfolioCounts(doc) {
30167
+ const members = doc.products ?? [];
30168
+ const memberKindOf = (p) => p.member_kind ?? "product";
30169
+ const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
30170
+ const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
30171
+ const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
30172
+ return {
30173
+ products: members.filter((p) => memberKindOf(p) === "product").length,
30174
+ ...watchedCount > 0 ? { watched_products: watchedCount } : {},
30175
+ ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
30176
+ ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
30177
+ product_areas: doc.product_areas?.length ?? 0,
30178
+ portfolios: doc.portfolios?.length ?? 0,
30179
+ cross_edges: doc.cross_edges?.length ?? 0
30180
+ };
30181
+ }
30182
+ function deriveCounts(doc) {
30183
+ return isPortfolio(doc) ? derivePortfolioCounts(doc) : deriveSingleCounts(doc);
30184
+ }
29908
30185
  function computeBodyChecksum(doc) {
29909
30186
  const body = isPortfolio(doc) ? portfolioBody(doc) : singleBody(doc);
29910
30187
  const content = JSON.stringify(body);
@@ -29944,7 +30221,7 @@ function serializeSingleWithHeader(doc, opts) {
29944
30221
  };
29945
30222
  const summary = deriveSummary(product.description);
29946
30223
  if (summary) header.summary = summary;
29947
- header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
30224
+ header.counts = deriveSingleCounts(doc);
29948
30225
  if (doc.member_kind === "org_rollup" || doc.member_kind === "watched" || doc.member_kind === "operating_function") {
29949
30226
  header.member_kind = doc.member_kind;
29950
30227
  }
@@ -29967,20 +30244,7 @@ function serializePortfolioWithHeader(doc, opts) {
29967
30244
  };
29968
30245
  const summary = deriveSummary(org.description);
29969
30246
  if (summary) header.summary = summary;
29970
- const members = doc.products ?? [];
29971
- const memberKindOf = (p) => p.member_kind ?? "product";
29972
- const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
29973
- const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
29974
- const operatingFunctionCount = members.filter((p) => memberKindOf(p) === "operating_function").length;
29975
- header.counts = {
29976
- products: members.filter((p) => memberKindOf(p) === "product").length,
29977
- ...watchedCount > 0 ? { watched_products: watchedCount } : {},
29978
- ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
29979
- ...operatingFunctionCount > 0 ? { operating_functions: operatingFunctionCount } : {},
29980
- product_areas: doc.product_areas?.length ?? 0,
29981
- portfolios: doc.portfolios?.length ?? 0,
29982
- cross_edges: doc.cross_edges?.length ?? 0
29983
- };
30247
+ header.counts = derivePortfolioCounts(doc);
29984
30248
  header.provenance = buildProvenance(doc, opts);
29985
30249
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
29986
30250
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
@@ -30048,9 +30312,61 @@ function isCanonical(text) {
30048
30312
  const parsed = parseUpg(text);
30049
30313
  return text === serializeCanonical(parsed);
30050
30314
  }
30315
+ function checkHeaderSeal(obj) {
30316
+ const raw = obj;
30317
+ const header = raw?.$upg;
30318
+ const report = {
30319
+ header_present: !!header,
30320
+ counts_checked: false,
30321
+ integrity_checked: false,
30322
+ counts_drift: [],
30323
+ integrity_drift: []
30324
+ };
30325
+ if (!header) return report;
30326
+ if (header.format_version !== UPG_CANONICAL_FORMAT_VERSION) {
30327
+ report.skipped_reason = `format_version "${header.format_version}" is not this serialiser's "${UPG_CANONICAL_FORMAT_VERSION}"; counts keys and body layout are format-version contracts, so neither seal can be judged here.`;
30328
+ return report;
30329
+ }
30330
+ const doc = normalizeDocument(raw);
30331
+ const declaredCounts = header.counts ?? {};
30332
+ const actualCounts = deriveCounts(doc);
30333
+ if (header.counts === void 0) {
30334
+ report.skipped_reason = "header has no `counts` block to compare.";
30335
+ } else {
30336
+ report.counts_checked = true;
30337
+ const fields = /* @__PURE__ */ new Set([...Object.keys(declaredCounts), ...Object.keys(actualCounts)]);
30338
+ for (const field of [...fields].sort()) {
30339
+ const declaredRaw = declaredCounts[field];
30340
+ const declared = typeof declaredRaw === "number" ? declaredRaw : 0;
30341
+ const actual = actualCounts[field] ?? 0;
30342
+ if (declared !== actual) report.counts_drift.push({ field, declared, actual });
30343
+ }
30344
+ }
30345
+ const integrity = header.integrity;
30346
+ if (!integrity || typeof integrity.body !== "string") {
30347
+ report.skipped_reason = report.skipped_reason ?? "header has no `integrity.body` seal to compare.";
30348
+ } else if (integrity.algorithm !== INTEGRITY_ALGORITHM) {
30349
+ report.skipped_reason = report.skipped_reason ?? `integrity.algorithm "${integrity.algorithm}" is not "${INTEGRITY_ALGORITHM}"; the declared digest cannot be recomputed here.`;
30350
+ } else {
30351
+ report.integrity_checked = true;
30352
+ const computed = computeBodyChecksum(doc);
30353
+ if (computed !== integrity.body) {
30354
+ report.integrity_drift.push({
30355
+ algorithm: integrity.algorithm,
30356
+ declared: integrity.body,
30357
+ computed
30358
+ });
30359
+ }
30360
+ }
30361
+ return report;
30362
+ }
30363
+ function checkHeaderSealText(text) {
30364
+ const noBom = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
30365
+ return checkHeaderSeal(JSON.parse(noBom));
30366
+ }
30051
30367
 
30052
30368
  // src/index.ts
30053
- var UPG_VERSION = "0.26.0";
30369
+ var UPG_VERSION = "0.27.0";
30054
30370
  var UPG_FORMAT_VERSION = "0.4.0";
30055
30371
  var MARKDOWN_FORMAT_VERSION = "0.1";
30056
30372
  var UPG_TYPES = getTypes();
@@ -30219,6 +30535,8 @@ export {
30219
30535
  backfillSlug,
30220
30536
  buildTypeAliases,
30221
30537
  canBeChildOf,
30538
+ checkHeaderSeal,
30539
+ checkHeaderSealText,
30222
30540
  coerceProductStage,
30223
30541
  collectSlugsForType,
30224
30542
  compareVersions,
@@ -30227,6 +30545,9 @@ export {
30227
30545
  concernFor,
30228
30546
  concernGatesFor,
30229
30547
  crossProductScope,
30548
+ deriveCounts,
30549
+ derivePortfolioCounts,
30550
+ deriveSingleCounts,
30230
30551
  describeTreePattern,
30231
30552
  edgeCarriesProperties,
30232
30553
  evaluateAntiPatterns,