@unified-product-graph/core 0.9.25 → 0.10.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
@@ -52,7 +52,7 @@ var UPG_DOMAINS = [
52
52
  id: "market_intelligence",
53
53
  label: "Market Intelligence",
54
54
  description: "The competitive landscape your product operates in. Competitors and their competitor features map the field. Market trends track industry shifts. Market segments define addressable audiences. Competitive analyses synthesize the full picture. Classification axes and values express the dimensional structure of the landscape (e.g. CMS Architecture \xD7 Editing Paradigm). Informs Strategy (positioning), Go-To-Market (battle cards), and Business Model (differentiation).",
55
- types: ["competitor", "competitor_feature", "market_trend", "market_segment", "competitive_analysis", "classification_axis", "classification_value"]
55
+ types: ["competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "competitive_analysis", "classification_axis", "classification_value"]
56
56
  },
57
57
  {
58
58
  id: "user_research",
@@ -587,6 +587,7 @@ var UPG_ENTITY_META = [
587
587
  // ── Market Intelligence ──
588
588
  { name: "competitor", type_id: "ent_033", maturity: "stable", since: "0.1.0" },
589
589
  { name: "competitor_feature", type_id: "ent_034", maturity: "stable", since: "0.1.0" },
590
+ { name: "competitor_signal", type_id: "ent_354", maturity: "proposed", since: "0.10.0" },
590
591
  { name: "market_trend", type_id: "ent_035", maturity: "stable", since: "0.1.0" },
591
592
  { name: "market_segment", type_id: "ent_036", maturity: "stable", since: "0.1.0" },
592
593
  { name: "competitive_analysis", type_id: "ent_037", maturity: "stable", since: "0.1.0" },
@@ -1098,6 +1099,24 @@ var UPG_EDGE_CATALOG = {
1098
1099
  // programming concern.
1099
1100
  product_contains_competitive_analysis: { forward_verb: "contains", reverse_verb: "contained_by", classification: "hierarchy", source_type: "product", target_type: "competitive_analysis" },
1100
1101
  competitor_offers_competitor_feature: { forward_verb: "offers", reverse_verb: "offered_by", classification: "hierarchy", source_type: "competitor", target_type: "competitor_feature" },
1102
+ // 0.10.0 (spec issue #38): parity / rivalry edge. Our `feature` rivals a
1103
+ // `competitor_feature`, carrying the parity assessment as EDGE metadata
1104
+ // (parity_status / quality / is_gap / assessed_on / evidence / confidence) so
1105
+ // "where are we behind, by area?" is one traversal, not a scan over the
1106
+ // free-text `our_equivalent`. `carries_properties` puts the assessment on the
1107
+ // edge; the node `parity_status` stays as a denormalised single-rival cache.
1108
+ // Dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so it also spans our
1109
+ // product graph and a watched competitor-intelligence graph. Distinct from
1110
+ // `competitor_feature_inspires_feature` (ideation lineage, not parity).
1111
+ feature_rivals_competitor_feature: { forward_verb: "rivals", reverse_verb: "is_rivalled_by", classification: "cross-domain", source_type: "feature", target_type: "competitor_feature", carries_properties: true },
1112
+ // 0.10.0 (spec issue #41): competitor_signal is a dated competitor move (feature
1113
+ // launch / pricing change / acquisition / partnership / market entry) emitted by a
1114
+ // competitor and mapped onto our portfolio. `emits` is within the watched graph;
1115
+ // `maps_to_feature` / `surfaces_opportunity` cross from the watched signal into our
1116
+ // product graph (dual-registered as cross-edges in UPG_CROSS_EDGE_TYPES).
1117
+ competitor_emits_competitor_signal: { forward_verb: "emits", reverse_verb: "emitted_by", classification: "hierarchy", source_type: "competitor", target_type: "competitor_signal" },
1118
+ competitor_signal_maps_to_feature: { forward_verb: "maps_to", reverse_verb: "targeted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "feature" },
1119
+ competitor_signal_surfaces_opportunity: { forward_verb: "surfaces", reverse_verb: "prompted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "opportunity" },
1101
1120
  competitive_analysis_analyses_competitor: { forward_verb: "analyses", reverse_verb: "analysed_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "competitor" },
1102
1121
  competitive_analysis_identifies_market_trend: { forward_verb: "identifies", reverse_verb: "identified_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_trend" },
1103
1122
  competitive_analysis_scopes_market_segment: { forward_verb: "scopes", reverse_verb: "scoped_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_segment" },
@@ -3018,7 +3037,10 @@ var UPG_CROSS_EDGE_TYPES = [
3018
3037
  "api_contract_speaks_specification",
3019
3038
  "product_exposes_primitive",
3020
3039
  "feature_manipulates_primitive",
3021
- "primitive_stored_as_data_type"
3040
+ "primitive_stored_as_data_type",
3041
+ "feature_rivals_competitor_feature",
3042
+ "competitor_signal_maps_to_feature",
3043
+ "competitor_signal_surfaces_opportunity"
3022
3044
  ];
3023
3045
  var REGISTRY_PRODUCT_ID = "registry";
3024
3046
 
@@ -3306,7 +3328,7 @@ var UPG_VALID_CHILDREN = {
3306
3328
  // each axis hosts its values; values are reusable across the row and column
3307
3329
  // dossiers of a 2-axis matrix.
3308
3330
  competitive_analysis: ["competitor", "market_trend", "market_segment", "classification_axis"],
3309
- competitor: ["competitor_feature"],
3331
+ competitor: ["competitor_feature", "competitor_signal"],
3310
3332
  classification_axis: ["classification_value"],
3311
3333
  // ── UX Research hierarchy ───────────────────────────────────────────────────
3312
3334
  research_study: [
@@ -6161,12 +6183,14 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
6161
6183
  "job_step",
6162
6184
  "desired_outcome",
6163
6185
  "switching_cost",
6164
- // ── Market Intelligence (6 of 7): competitor landscape is reference data;
6186
+ // ── Market Intelligence (7 of 8): competitor landscape is reference data;
6187
+ // competitor_signal is an append-only dated event (no phase ladder);
6165
6188
  // competitive_analysis shipped PUBLISHING lifecycle in Phase C;
6166
6189
  // classification_axis and classification_value are reference taxonomy
6167
6190
  // atoms with no state machine. ─
6168
6191
  "competitor",
6169
6192
  "competitor_feature",
6193
+ "competitor_signal",
6170
6194
  "market_trend",
6171
6195
  "market_segment",
6172
6196
  "classification_axis",
@@ -9516,8 +9540,8 @@ var UPG_PROPERTY_SCHEMA = {
9516
9540
  classification_value: {
9517
9541
  rationale: { type: "string", description: "Short paragraph: why this value earns its own row/column. Longer narrative belongs in `summary_md` or attached `content_piece` nodes." },
9518
9542
  exemplars: { type: "string[]", description: "Free-text examples of products occupying this value. For queryable occupancy, prefer `competitor` nodes with `classified_as` edges. @example ['Sanity', 'Contentful', 'Hygraph']" },
9519
- commitments: { type: "object", description: "The N (typically 2\u20134) load-bearing commitments that define this category. Each commitment is a structural axis; removing it changes the category. Pairs with `capabilities`: commitments are the *load-bearing definition*; capabilities are the *surface offering*. @example [ { name: 'Typed content graph', description: 'Schemas in code; references as edges; content is structured data.' }, { name: 'Real-time backend', description: 'Live queries, CRDT collaboration, sub-second propagation.' }, { name: 'Embeddable studio', description: 'Editor is a library hosted inside the team app, not an external portal.' }, ]" },
9520
- capabilities: { type: "object", description: "Structured capability bullets across the six canonical surfaces. Each surface appears at most once per `classification_value`. Pairs with `commitments`: capabilities describe what the category *offers*; commitments describe what *defines* it. @example [ { surface: 'delivery', bullets: ['GROQ', 'GraphQL', 'REST', 'Live Content API', 'Asset CDN'] }, { surface: 'extensibility', bullets: ['Custom input components', 'Studio plugins'] }, ]" }
9543
+ commitments: { type: "object[]", description: "The N (typically 2\u20134) load-bearing commitments that define this category. Each commitment is a structural axis; removing it changes the category. Pairs with `capabilities`: commitments are the *load-bearing definition*; capabilities are the *surface offering*. @example [ { name: 'Typed content graph', description: 'Schemas in code; references as edges; content is structured data.' }, { name: 'Real-time backend', description: 'Live queries, CRDT collaboration, sub-second propagation.' }, { name: 'Embeddable studio', description: 'Editor is a library hosted inside the team app, not an external portal.' }, ]" },
9544
+ capabilities: { type: "object[]", description: "Structured capability bullets across the six canonical surfaces. Each surface appears at most once per `classification_value`. Pairs with `commitments`: capabilities describe what the category *offers*; commitments describe what *defines* it. @example [ { surface: 'delivery', bullets: ['GROQ', 'GraphQL', 'REST', 'Live Content API', 'Asset CDN'] }, { surface: 'extensibility', bullets: ['Custom input components', 'Studio plugins'] }, ]" }
9521
9545
  },
9522
9546
  // CodeRepositoryProperties: Code repository.
9523
9547
  code_repository: {
@@ -9552,7 +9576,22 @@ var UPG_PROPERTY_SCHEMA = {
9552
9576
  analysis_type: { type: "string", enum: ["feature_comparison", "positioning", "swot", "pricing"], description: "Type of analysis. `feature_comparison` = side-by-side matrix. `positioning` = competitor positioning relative to each other. `swot` = strengths, weaknesses, opportunities, threats. `pricing` = pricing structure comparison." },
9553
9577
  analysis_date: { type: "string", description: 'ISO date conducted. Competitive intelligence decays quickly; track snapshot age. @example "2026-03-15"' },
9554
9578
  framework_id: { type: "string", description: 'Framework ID (references `UPGFramework.id`). @example "porter-five-forces", "swot-analysis", "competitive-matrix"' },
9555
- empty_cells: { type: "object", description: "Empty cells in a 2-axis classification matrix that earn explicit commentary. Only the strategically interesting cells. Opportunity-kind cells are the most valuable; they identify unoccupied strategic space. Each entry references two `classification_value` nodes by id (one from each `classification_axis` child of this `competitive_analysis`). `validate_graph` enforces that the refs resolve to `classification_value` nodes whose parents are distinct `classification_axis` instances. See `ClassificationValueProperties.commitments` for the inverse \"occupied cell\" structural-definition shape. @example [ { axis_a_value_ref: 'val-git-based', axis_b_value_ref: 'val-portable-text', rationale_kind: 'opportunity', rationale_md: 'No technical reason; only adoption inertia. Watching brief.' }, { axis_a_value_ref: 'val-composable', axis_b_value_ref: 'val-wysiwyg', rationale_kind: 'structural', rationale_md: 'Composable rejects HTML blobs; WYSIWYG requires them.' }, ]" }
9579
+ empty_cells: { type: "object[]", description: "Empty cells in a 2-axis classification matrix that earn explicit commentary. Only the strategically interesting cells. Opportunity-kind cells are the most valuable; they identify unoccupied strategic space. Each entry references two `classification_value` nodes by id (one from each `classification_axis` child of this `competitive_analysis`). `validate_graph` enforces that the refs resolve to `classification_value` nodes whose parents are distinct `classification_axis` instances. See `ClassificationValueProperties.commitments` for the inverse \"occupied cell\" structural-definition shape. @example [ { axis_a_value_ref: 'val-git-based', axis_b_value_ref: 'val-portable-text', rationale_kind: 'opportunity', rationale_md: 'No technical reason; only adoption inertia. Watching brief.' }, { axis_a_value_ref: 'val-composable', axis_b_value_ref: 'val-wysiwyg', rationale_kind: 'structural', rationale_md: 'Composable rejects HTML blobs; WYSIWYG requires them.' }, ]" },
9580
+ last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. Distinct from `analysis_date` (when the analysis was conducted). @example "2026-06-13"' },
9581
+ source: { type: "string", description: 'Provenance: where this was observed. A changelog, pricing, or docs URL, an analyst report, or a research note. @example "https://www.contentful.com/changelog/"' },
9582
+ confidence: {
9583
+ type: "assessment",
9584
+ scale_id: "confidence_5",
9585
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9586
+ properties: {
9587
+ value: { type: "number", description: "The numeric value, used for computation." },
9588
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9589
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9590
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9591
+ },
9592
+ required: ["value", "label"]
9593
+ },
9594
+ observed_by: { type: "string", description: 'Provenance: agent or routine id that last wrote this. Absent when hand-authored, the signal that a human (not a poller) is the last writer. @example "competitor-watch-agent"' }
9556
9595
  },
9557
9596
  // CompetitiveBattleCardProperties: CompetitiveBattleCard entity.
9558
9597
  competitive_battle_card: {
@@ -9565,7 +9604,22 @@ var UPG_PROPERTY_SCHEMA = {
9565
9604
  pricing_model: { type: "string", description: 'Pricing model (e.g. "freemium", "per-seat SaaS", "usage-based")' },
9566
9605
  website: { type: "string", description: "Public website URL" },
9567
9606
  strengths: { type: "string[]", description: "Bulleted factual strengths. Each item is a short statement, not prose. @example ['Real-time multiplayer canvas', 'Generous free tier']" },
9568
- weaknesses: { type: "string[]", description: "Bulleted factual weaknesses: gaps, friction, or capabilities materially below market. @example ['Weak API query capabilities', 'No mobile companion']" }
9607
+ weaknesses: { type: "string[]", description: "Bulleted factual weaknesses: gaps, friction, or capabilities materially below market. @example ['Weak API query capabilities', 'No mobile companion']" },
9608
+ last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. Lets a stale record be told apart from a fresh one. @example "2026-06-13"' },
9609
+ source: { type: "string", description: 'Provenance: where this was observed. A changelog, pricing, or docs URL, an analyst report, or a research note. @example "https://www.contentful.com/pricing/"' },
9610
+ confidence: {
9611
+ type: "assessment",
9612
+ scale_id: "confidence_5",
9613
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9614
+ properties: {
9615
+ value: { type: "number", description: "The numeric value, used for computation." },
9616
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9617
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9618
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9619
+ },
9620
+ required: ["value", "label"]
9621
+ },
9622
+ observed_by: { type: "string", description: 'Provenance: agent or routine id that last wrote this. Absent when hand-authored, the signal that a human (not a poller) is the last writer. @example "competitor-watch-agent"' }
9569
9623
  },
9570
9624
  // CompetitorFeatureProperties: Competitor feature or capability.
9571
9625
  competitor_feature: {
@@ -9573,7 +9627,43 @@ var UPG_PROPERTY_SCHEMA = {
9573
9627
  is_gap: { type: "boolean", description: "Gap in our offering. True when `our_equivalent` is absent or materially inferior." },
9574
9628
  quality: { type: "string", enum: ["better", "same", "worse", "missing"], description: "Quality comparison. `better` = ours is meaningfully superior. `same` = roughly equivalent. `worse` = theirs is meaningfully superior. `missing` = we have no equivalent." },
9575
9629
  parity_status: { type: "string", enum: ["ahead", "behind", "parity", "unique_to_us", "unique_to_them"], description: "Parity. More granular than `quality`; captures whether the gap is offensive or defensive. `ahead` = we lead. `behind` = they lead. `parity` = equivalent. `unique_to_us` / `unique_to_them` = only one side offers it." },
9576
- last_updated: { type: "string", description: 'ISO date this assessment was last updated. Competitor feature landscapes change quickly. @example "2026-02-15"' }
9630
+ last_updated: { type: "string", description: 'ISO date this assessment was last updated. Competitor feature landscapes change quickly. @example "2026-02-15"' },
9631
+ source: { type: "string", description: 'Provenance: where this was observed. A changelog, pricing, or docs URL, an analyst report, or a research note. @example "https://www.contentful.com/changelog/"' },
9632
+ confidence: {
9633
+ type: "assessment",
9634
+ scale_id: "confidence_5",
9635
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9636
+ properties: {
9637
+ value: { type: "number", description: "The numeric value, used for computation." },
9638
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9639
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9640
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9641
+ },
9642
+ required: ["value", "label"]
9643
+ },
9644
+ observed_by: { type: "string", description: 'Provenance: agent or routine id that last wrote this. Absent when hand-authored, the signal that a human (not a poller) is the last writer. @example "competitor-watch-agent"' }
9645
+ },
9646
+ // CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
9647
+ competitor_signal: {
9648
+ observed_at: { type: "string", description: 'ISO date-time the move was observed. @example "2026-06-10"' },
9649
+ signal_type: { type: "string", enum: ["feature_launch", "pricing_change", "acquisition", "partnership", "market_entry"], description: "Kind of move. `feature_launch` = shipped a feature. `pricing_change` = a plan or tier change. `acquisition` / `partnership` / `market_entry` = strategic moves." },
9650
+ summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
9651
+ impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
9652
+ last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. @example "2026-06-13"' },
9653
+ source: { type: "string", description: 'Provenance: where this was observed. A changelog, pricing, or docs URL, an analyst report, or a research note. @example "https://www.contentful.com/changelog/"' },
9654
+ confidence: {
9655
+ type: "assessment",
9656
+ scale_id: "confidence_5",
9657
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9658
+ properties: {
9659
+ value: { type: "number", description: "The numeric value, used for computation." },
9660
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9661
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9662
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9663
+ },
9664
+ required: ["value", "label"]
9665
+ },
9666
+ observed_by: { type: "string", description: 'Provenance: agent or routine id that last wrote this. Absent when hand-authored, the signal that a human (not a poller) is the last writer. @example "competitor-watch-agent"' }
9577
9667
  },
9578
9668
  // ComplianceFrameworkProperties: Compliance framework.
9579
9669
  compliance_framework: {
@@ -10798,7 +10888,21 @@ var UPG_PROPERTY_SCHEMA = {
10798
10888
  },
10799
10889
  required: ["value", "label"]
10800
10890
  },
10801
- source: { type: "string", description: 'Source of the trend data: analyst report, research firm, observed behaviour. @example "Gartner Hype Cycle 2025", "Observed in user interviews Q1 2026"' }
10891
+ source: { type: "string", description: 'Source of the trend data: analyst report, research firm, observed behaviour. @example "Gartner Hype Cycle 2025", "Observed in user interviews Q1 2026"' },
10892
+ last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. Lets a stale record be told apart from a fresh one. @example "2026-06-13"' },
10893
+ confidence: {
10894
+ type: "assessment",
10895
+ scale_id: "confidence_5",
10896
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
10897
+ properties: {
10898
+ value: { type: "number", description: "The numeric value, used for computation." },
10899
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
10900
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
10901
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
10902
+ },
10903
+ required: ["value", "label"]
10904
+ },
10905
+ observed_by: { type: "string", description: 'Provenance: agent or routine id that last wrote this. Absent when hand-authored, the signal that a human (not a poller) is the last writer. @example "competitor-watch-agent"' }
10802
10906
  },
10803
10907
  // MarketingCampaignPlanProperties: Marketing campaign plan.
10804
10908
  marketing_campaign_plan: {
@@ -11574,7 +11678,7 @@ var UPG_PROPERTY_SCHEMA = {
11574
11678
  owner: { type: "string", description: "Owning person or team. Backstage marks this required; strongly recommended." },
11575
11679
  lifecycle: { type: "string", enum: ["experimental", "production", "deprecated"], description: 'Service maturity. Answers "how mature is it?". `experimental` = early-stage. `production` = battle-tested. `deprecated` = being phased out.' },
11576
11680
  tags: { type: "string[]", description: 'Free-form filter tags (e.g. ["payments", "critical-path", "team-alpha"]).' },
11577
- links: { type: "object", description: "Named URLs for documentation, dashboards, runbooks." }
11681
+ links: { type: "object[]", description: "Named URLs for documentation, dashboards, runbooks." }
11578
11682
  },
11579
11683
  // ServiceBlueprintProperties: Service blueprint.
11580
11684
  service_blueprint: {
@@ -11923,7 +12027,7 @@ var UPG_PROPERTY_SCHEMA = {
11923
12027
  test_plan: {
11924
12028
  test_scope: { type: "string", description: 'What the plan covers (e.g. "checkout flow", "auth module", "release 2.4").' },
11925
12029
  plan_type: { type: "string", enum: ["release", "regression", "integration", "acceptance", "smoke", "exploratory"], description: "Kind of verification effort this plan governs." },
11926
- environments: { type: "object", description: "Environments the plan exercises (mirrors `TestEnvironmentProperties.env_type`)." },
12030
+ environments: { type: "string[]", enum: ["local", "ci", "staging", "sandbox", "production_mirror"], description: "Environments the plan exercises (mirrors `TestEnvironmentProperties.env_type`)." },
11927
12031
  entry_criteria: { type: "string", description: "Conditions that must hold before execution may begin." },
11928
12032
  pass_criteria: { type: "string", description: "Exit / pass criteria determining whether the plan succeeds." }
11929
12033
  },
@@ -18327,6 +18431,7 @@ function valueMatchesType(defType, val) {
18327
18431
  case "boolean":
18328
18432
  return kind === "boolean";
18329
18433
  case "string[]":
18434
+ case "object[]":
18330
18435
  return kind === "string[]";
18331
18436
  case "object":
18332
18437
  case "assessment":
@@ -18437,6 +18542,7 @@ var ENTITY_EMOJI = {
18437
18542
  competitive_battle_card: "\u2694\uFE0F",
18438
18543
  competitor: "\u{1F94A}",
18439
18544
  competitor_feature: "\u{1F52C}",
18545
+ competitor_signal: "\u{1F4E1}",
18440
18546
  compliance_framework: "\u{1F3DF}\uFE0F",
18441
18547
  compliance_requirement: "\u26F2",
18442
18548
  constraint: "\u{1F512}",
@@ -20183,8 +20289,8 @@ var MARKET_COMPETITIVE_PLAYBOOK = {
20183
20289
  seqStep(
20184
20290
  6,
20185
20291
  "Moves",
20186
- ["competitive_battle_card"],
20187
- "Look where competitors are weak and trends are strong. That intersection is where your moves live: arm the team with battle cards that turn each competitor weakness into a position you can win. (Partnership moves belong in the business & GTM playbook, where `partnership` connects.)"
20292
+ ["competitor_signal", "competitive_battle_card"],
20293
+ "Capture rivals' dated moves as competitor_signals (launches, pricing changes, acquisitions) and map each onto the feature it threatens. Then look where competitors are weak and trends are strong: that intersection is where your moves live. Arm the team with battle cards that turn each competitor weakness into a position you can win. (Partnership moves belong in the business & GTM playbook, where `partnership` connects.)"
20188
20294
  )
20189
20295
  ]
20190
20296
  };
@@ -21290,7 +21396,36 @@ var UPG_LENSES = [
21290
21396
  perspective: "Sees the product as something that needs to reach and resonate with people. Audience-aware, message-focused, always asking what makes someone care."
21291
21397
  },
21292
21398
  // ═══════════════════════════════════════════════════════════════════════════════
21293
- // 8. FULL LENS - everything, canonical vocabulary (default)
21399
+ // 8. COMPETITIVE LENS - rivals, their dated moves, and where we stand
21400
+ // ═══════════════════════════════════════════════════════════════════════════════
21401
+ {
21402
+ id: "competitive",
21403
+ name: "Competitive",
21404
+ description: "The competitive landscape: rivals, their offerings, dated moves, and where we stand",
21405
+ icon: "swords",
21406
+ visible_domains: ["market_intelligence"],
21407
+ // Parity (#38) and signal (#41) edges reach onto our side of the graph;
21408
+ // surface those endpoints so a parity row or a mapped signal shows both ends.
21409
+ always_show_types: ["feature", "capability", "opportunity"],
21410
+ playbook_id: "playbook:market-competitive",
21411
+ benchmark_domains: ["market_intelligence"],
21412
+ intelligence_prompts: [
21413
+ {
21414
+ condition: "competitors.length === 0",
21415
+ structured_condition: { check: { type: "entity_count", entity_type: "competitor", comparison: "zero" } },
21416
+ message: "No competitors mapped yet. Name the 3 to 7 closest alternatives, including the status quo and DIY."
21417
+ },
21418
+ {
21419
+ condition: "competitor_signals.length === 0",
21420
+ structured_condition: { check: { type: "entity_count", entity_type: "competitor_signal", comparison: "zero" } },
21421
+ message: "No competitor signals captured. Log rivals' dated moves (launches, pricing changes) and map each onto the feature it threatens."
21422
+ }
21423
+ ],
21424
+ audience: "Product marketers, strategists, and founders sizing up the field",
21425
+ perspective: "Sees the product through its rivals: who competes, what they ship, the dated moves they make, and where we lead or trail."
21426
+ },
21427
+ // ═══════════════════════════════════════════════════════════════════════════════
21428
+ // 9. FULL LENS - everything, canonical vocabulary (default)
21294
21429
  // ═══════════════════════════════════════════════════════════════════════════════
21295
21430
  {
21296
21431
  id: "full",
@@ -21510,7 +21645,7 @@ var USER_RESEARCH_GUIDE = {
21510
21645
  var MARKET_INTELLIGENCE_GUIDE = {
21511
21646
  domain_id: "market_intelligence",
21512
21647
  anchor_entity: "competitive_analysis",
21513
- creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "market_trend", "market_segment", "classification_axis", "classification_value"],
21648
+ creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "classification_axis", "classification_value"],
21514
21649
  patterns: [
21515
21650
  {
21516
21651
  name: "Competitive Landscape Map",
@@ -26007,6 +26142,11 @@ var UPG_REGIONS = [
26007
26142
  type: "competitor_feature",
26008
26143
  role: "leaf"
26009
26144
  },
26145
+ {
26146
+ type: "competitor_signal",
26147
+ role: "leaf",
26148
+ notes: "a dated competitor move (feature launch, pricing change, ...) emitted by a competitor"
26149
+ },
26010
26150
  {
26011
26151
  type: "market_trend",
26012
26152
  role: "leaf"
@@ -26038,6 +26178,7 @@ var UPG_REGIONS = [
26038
26178
  },
26039
26179
  intra_edges: [
26040
26180
  "competitor_offers_competitor_feature",
26181
+ "competitor_emits_competitor_signal",
26041
26182
  "competitive_analysis_analyses_competitor",
26042
26183
  "competitive_analysis_dimensioned_by_classification_axis",
26043
26184
  "classification_axis_includes_classification_value",
@@ -28137,6 +28278,9 @@ function serializeSingleWithHeader(doc, opts) {
28137
28278
  const summary = deriveSummary(product.description);
28138
28279
  if (summary) header.summary = summary;
28139
28280
  header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
28281
+ if (doc.member_kind === "org_rollup" || doc.member_kind === "watched") {
28282
+ header.member_kind = doc.member_kind;
28283
+ }
28140
28284
  header.provenance = buildProvenance(doc, opts);
28141
28285
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
28142
28286
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
@@ -28156,8 +28300,14 @@ function serializePortfolioWithHeader(doc, opts) {
28156
28300
  };
28157
28301
  const summary = deriveSummary(org.description);
28158
28302
  if (summary) header.summary = summary;
28303
+ const members = doc.products ?? [];
28304
+ const memberKindOf = (p) => p.member_kind ?? "product";
28305
+ const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
28306
+ const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
28159
28307
  header.counts = {
28160
- products: doc.products?.length ?? 0,
28308
+ products: members.filter((p) => memberKindOf(p) === "product").length,
28309
+ ...watchedCount > 0 ? { watched_products: watchedCount } : {},
28310
+ ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
28161
28311
  product_areas: doc.product_areas?.length ?? 0,
28162
28312
  portfolios: doc.portfolios?.length ?? 0,
28163
28313
  cross_edges: doc.cross_edges?.length ?? 0
@@ -28211,6 +28361,8 @@ function normalizeDocument(obj) {
28211
28361
  nodes: (raw.nodes ?? []).map((n) => repairNodeDrift(n)),
28212
28362
  edges: raw.edges ?? []
28213
28363
  };
28364
+ const memberKind = header?.member_kind ?? raw.member_kind;
28365
+ if (memberKind === "org_rollup" || memberKind === "watched") out.member_kind = memberKind;
28214
28366
  if (_integrity) out._integrity = _integrity;
28215
28367
  return out;
28216
28368
  }
@@ -28223,7 +28375,7 @@ function isCanonical(text) {
28223
28375
  }
28224
28376
 
28225
28377
  // src/index.ts
28226
- var UPG_VERSION = "0.9.25";
28378
+ var UPG_VERSION = "0.10.0";
28227
28379
  var UPG_FORMAT_VERSION = "0.4.0";
28228
28380
  var MARKDOWN_FORMAT_VERSION = "0.1";
28229
28381
  var UPG_TYPES = getTypes();