@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.cjs CHANGED
@@ -260,7 +260,7 @@ var UPG_DOMAINS = [
260
260
  id: "market_intelligence",
261
261
  label: "Market Intelligence",
262
262
  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).",
263
- types: ["competitor", "competitor_feature", "market_trend", "market_segment", "competitive_analysis", "classification_axis", "classification_value"]
263
+ types: ["competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "competitive_analysis", "classification_axis", "classification_value"]
264
264
  },
265
265
  {
266
266
  id: "user_research",
@@ -795,6 +795,7 @@ var UPG_ENTITY_META = [
795
795
  // ── Market Intelligence ──
796
796
  { name: "competitor", type_id: "ent_033", maturity: "stable", since: "0.1.0" },
797
797
  { name: "competitor_feature", type_id: "ent_034", maturity: "stable", since: "0.1.0" },
798
+ { name: "competitor_signal", type_id: "ent_354", maturity: "proposed", since: "0.10.0" },
798
799
  { name: "market_trend", type_id: "ent_035", maturity: "stable", since: "0.1.0" },
799
800
  { name: "market_segment", type_id: "ent_036", maturity: "stable", since: "0.1.0" },
800
801
  { name: "competitive_analysis", type_id: "ent_037", maturity: "stable", since: "0.1.0" },
@@ -1306,6 +1307,24 @@ var UPG_EDGE_CATALOG = {
1306
1307
  // programming concern.
1307
1308
  product_contains_competitive_analysis: { forward_verb: "contains", reverse_verb: "contained_by", classification: "hierarchy", source_type: "product", target_type: "competitive_analysis" },
1308
1309
  competitor_offers_competitor_feature: { forward_verb: "offers", reverse_verb: "offered_by", classification: "hierarchy", source_type: "competitor", target_type: "competitor_feature" },
1310
+ // 0.10.0 (spec issue #38): parity / rivalry edge. Our `feature` rivals a
1311
+ // `competitor_feature`, carrying the parity assessment as EDGE metadata
1312
+ // (parity_status / quality / is_gap / assessed_on / evidence / confidence) so
1313
+ // "where are we behind, by area?" is one traversal, not a scan over the
1314
+ // free-text `our_equivalent`. `carries_properties` puts the assessment on the
1315
+ // edge; the node `parity_status` stays as a denormalised single-rival cache.
1316
+ // Dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so it also spans our
1317
+ // product graph and a watched competitor-intelligence graph. Distinct from
1318
+ // `competitor_feature_inspires_feature` (ideation lineage, not parity).
1319
+ 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 },
1320
+ // 0.10.0 (spec issue #41): competitor_signal is a dated competitor move (feature
1321
+ // launch / pricing change / acquisition / partnership / market entry) emitted by a
1322
+ // competitor and mapped onto our portfolio. `emits` is within the watched graph;
1323
+ // `maps_to_feature` / `surfaces_opportunity` cross from the watched signal into our
1324
+ // product graph (dual-registered as cross-edges in UPG_CROSS_EDGE_TYPES).
1325
+ competitor_emits_competitor_signal: { forward_verb: "emits", reverse_verb: "emitted_by", classification: "hierarchy", source_type: "competitor", target_type: "competitor_signal" },
1326
+ competitor_signal_maps_to_feature: { forward_verb: "maps_to", reverse_verb: "targeted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "feature" },
1327
+ competitor_signal_surfaces_opportunity: { forward_verb: "surfaces", reverse_verb: "prompted_by_signal", classification: "cross-domain", source_type: "competitor_signal", target_type: "opportunity" },
1309
1328
  competitive_analysis_analyses_competitor: { forward_verb: "analyses", reverse_verb: "analysed_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "competitor" },
1310
1329
  competitive_analysis_identifies_market_trend: { forward_verb: "identifies", reverse_verb: "identified_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_trend" },
1311
1330
  competitive_analysis_scopes_market_segment: { forward_verb: "scopes", reverse_verb: "scoped_in", classification: "hierarchy", source_type: "competitive_analysis", target_type: "market_segment" },
@@ -3226,7 +3245,10 @@ var UPG_CROSS_EDGE_TYPES = [
3226
3245
  "api_contract_speaks_specification",
3227
3246
  "product_exposes_primitive",
3228
3247
  "feature_manipulates_primitive",
3229
- "primitive_stored_as_data_type"
3248
+ "primitive_stored_as_data_type",
3249
+ "feature_rivals_competitor_feature",
3250
+ "competitor_signal_maps_to_feature",
3251
+ "competitor_signal_surfaces_opportunity"
3230
3252
  ];
3231
3253
  var REGISTRY_PRODUCT_ID = "registry";
3232
3254
 
@@ -3514,7 +3536,7 @@ var UPG_VALID_CHILDREN = {
3514
3536
  // each axis hosts its values; values are reusable across the row and column
3515
3537
  // dossiers of a 2-axis matrix.
3516
3538
  competitive_analysis: ["competitor", "market_trend", "market_segment", "classification_axis"],
3517
- competitor: ["competitor_feature"],
3539
+ competitor: ["competitor_feature", "competitor_signal"],
3518
3540
  classification_axis: ["classification_value"],
3519
3541
  // ── UX Research hierarchy ───────────────────────────────────────────────────
3520
3542
  research_study: [
@@ -6369,12 +6391,14 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
6369
6391
  "job_step",
6370
6392
  "desired_outcome",
6371
6393
  "switching_cost",
6372
- // ── Market Intelligence (6 of 7): competitor landscape is reference data;
6394
+ // ── Market Intelligence (7 of 8): competitor landscape is reference data;
6395
+ // competitor_signal is an append-only dated event (no phase ladder);
6373
6396
  // competitive_analysis shipped PUBLISHING lifecycle in Phase C;
6374
6397
  // classification_axis and classification_value are reference taxonomy
6375
6398
  // atoms with no state machine. ─
6376
6399
  "competitor",
6377
6400
  "competitor_feature",
6401
+ "competitor_signal",
6378
6402
  "market_trend",
6379
6403
  "market_segment",
6380
6404
  "classification_axis",
@@ -9724,8 +9748,8 @@ var UPG_PROPERTY_SCHEMA = {
9724
9748
  classification_value: {
9725
9749
  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." },
9726
9750
  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']" },
9727
- 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.' }, ]" },
9728
- 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'] }, ]" }
9751
+ 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.' }, ]" },
9752
+ 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'] }, ]" }
9729
9753
  },
9730
9754
  // CodeRepositoryProperties: Code repository.
9731
9755
  code_repository: {
@@ -9760,7 +9784,22 @@ var UPG_PROPERTY_SCHEMA = {
9760
9784
  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." },
9761
9785
  analysis_date: { type: "string", description: 'ISO date conducted. Competitive intelligence decays quickly; track snapshot age. @example "2026-03-15"' },
9762
9786
  framework_id: { type: "string", description: 'Framework ID (references `UPGFramework.id`). @example "porter-five-forces", "swot-analysis", "competitive-matrix"' },
9763
- 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.' }, ]" }
9787
+ 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.' }, ]" },
9788
+ 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"' },
9789
+ 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/"' },
9790
+ confidence: {
9791
+ type: "assessment",
9792
+ scale_id: "confidence_5",
9793
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9794
+ properties: {
9795
+ value: { type: "number", description: "The numeric value, used for computation." },
9796
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9797
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9798
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9799
+ },
9800
+ required: ["value", "label"]
9801
+ },
9802
+ 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"' }
9764
9803
  },
9765
9804
  // CompetitiveBattleCardProperties: CompetitiveBattleCard entity.
9766
9805
  competitive_battle_card: {
@@ -9773,7 +9812,22 @@ var UPG_PROPERTY_SCHEMA = {
9773
9812
  pricing_model: { type: "string", description: 'Pricing model (e.g. "freemium", "per-seat SaaS", "usage-based")' },
9774
9813
  website: { type: "string", description: "Public website URL" },
9775
9814
  strengths: { type: "string[]", description: "Bulleted factual strengths. Each item is a short statement, not prose. @example ['Real-time multiplayer canvas', 'Generous free tier']" },
9776
- weaknesses: { type: "string[]", description: "Bulleted factual weaknesses: gaps, friction, or capabilities materially below market. @example ['Weak API query capabilities', 'No mobile companion']" }
9815
+ weaknesses: { type: "string[]", description: "Bulleted factual weaknesses: gaps, friction, or capabilities materially below market. @example ['Weak API query capabilities', 'No mobile companion']" },
9816
+ 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"' },
9817
+ 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/"' },
9818
+ confidence: {
9819
+ type: "assessment",
9820
+ scale_id: "confidence_5",
9821
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9822
+ properties: {
9823
+ value: { type: "number", description: "The numeric value, used for computation." },
9824
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9825
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9826
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9827
+ },
9828
+ required: ["value", "label"]
9829
+ },
9830
+ 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"' }
9777
9831
  },
9778
9832
  // CompetitorFeatureProperties: Competitor feature or capability.
9779
9833
  competitor_feature: {
@@ -9781,7 +9835,43 @@ var UPG_PROPERTY_SCHEMA = {
9781
9835
  is_gap: { type: "boolean", description: "Gap in our offering. True when `our_equivalent` is absent or materially inferior." },
9782
9836
  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." },
9783
9837
  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." },
9784
- last_updated: { type: "string", description: 'ISO date this assessment was last updated. Competitor feature landscapes change quickly. @example "2026-02-15"' }
9838
+ last_updated: { type: "string", description: 'ISO date this assessment was last updated. Competitor feature landscapes change quickly. @example "2026-02-15"' },
9839
+ 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/"' },
9840
+ confidence: {
9841
+ type: "assessment",
9842
+ scale_id: "confidence_5",
9843
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9844
+ properties: {
9845
+ value: { type: "number", description: "The numeric value, used for computation." },
9846
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9847
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9848
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9849
+ },
9850
+ required: ["value", "label"]
9851
+ },
9852
+ 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"' }
9853
+ },
9854
+ // CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
9855
+ competitor_signal: {
9856
+ observed_at: { type: "string", description: 'ISO date-time the move was observed. @example "2026-06-10"' },
9857
+ 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." },
9858
+ summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
9859
+ impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
9860
+ last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. @example "2026-06-13"' },
9861
+ 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/"' },
9862
+ confidence: {
9863
+ type: "assessment",
9864
+ scale_id: "confidence_5",
9865
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
9866
+ properties: {
9867
+ value: { type: "number", description: "The numeric value, used for computation." },
9868
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
9869
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
9870
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
9871
+ },
9872
+ required: ["value", "label"]
9873
+ },
9874
+ 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"' }
9785
9875
  },
9786
9876
  // ComplianceFrameworkProperties: Compliance framework.
9787
9877
  compliance_framework: {
@@ -11006,7 +11096,21 @@ var UPG_PROPERTY_SCHEMA = {
11006
11096
  },
11007
11097
  required: ["value", "label"]
11008
11098
  },
11009
- 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"' }
11099
+ 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"' },
11100
+ 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"' },
11101
+ confidence: {
11102
+ type: "assessment",
11103
+ scale_id: "confidence_5",
11104
+ description: "Provenance: how sure we are, on the canonical confidence_5 scale. Carries both a numeric value and a high / medium / low label.",
11105
+ properties: {
11106
+ value: { type: "number", description: "The numeric value, used for computation." },
11107
+ label: { type: "string", description: "The qualitative label (what the assessor meant)." },
11108
+ scale_id: { type: "string", description: "Which assessment scale this was rated on (optional)." },
11109
+ normalized: { type: "number", description: "Normalized 0-1 value for cross-tool comparison (optional)." }
11110
+ },
11111
+ required: ["value", "label"]
11112
+ },
11113
+ 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"' }
11010
11114
  },
11011
11115
  // MarketingCampaignPlanProperties: Marketing campaign plan.
11012
11116
  marketing_campaign_plan: {
@@ -11782,7 +11886,7 @@ var UPG_PROPERTY_SCHEMA = {
11782
11886
  owner: { type: "string", description: "Owning person or team. Backstage marks this required; strongly recommended." },
11783
11887
  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.' },
11784
11888
  tags: { type: "string[]", description: 'Free-form filter tags (e.g. ["payments", "critical-path", "team-alpha"]).' },
11785
- links: { type: "object", description: "Named URLs for documentation, dashboards, runbooks." }
11889
+ links: { type: "object[]", description: "Named URLs for documentation, dashboards, runbooks." }
11786
11890
  },
11787
11891
  // ServiceBlueprintProperties: Service blueprint.
11788
11892
  service_blueprint: {
@@ -12131,7 +12235,7 @@ var UPG_PROPERTY_SCHEMA = {
12131
12235
  test_plan: {
12132
12236
  test_scope: { type: "string", description: 'What the plan covers (e.g. "checkout flow", "auth module", "release 2.4").' },
12133
12237
  plan_type: { type: "string", enum: ["release", "regression", "integration", "acceptance", "smoke", "exploratory"], description: "Kind of verification effort this plan governs." },
12134
- environments: { type: "object", description: "Environments the plan exercises (mirrors `TestEnvironmentProperties.env_type`)." },
12238
+ environments: { type: "string[]", enum: ["local", "ci", "staging", "sandbox", "production_mirror"], description: "Environments the plan exercises (mirrors `TestEnvironmentProperties.env_type`)." },
12135
12239
  entry_criteria: { type: "string", description: "Conditions that must hold before execution may begin." },
12136
12240
  pass_criteria: { type: "string", description: "Exit / pass criteria determining whether the plan succeeds." }
12137
12241
  },
@@ -18535,6 +18639,7 @@ function valueMatchesType(defType, val) {
18535
18639
  case "boolean":
18536
18640
  return kind === "boolean";
18537
18641
  case "string[]":
18642
+ case "object[]":
18538
18643
  return kind === "string[]";
18539
18644
  case "object":
18540
18645
  case "assessment":
@@ -18645,6 +18750,7 @@ var ENTITY_EMOJI = {
18645
18750
  competitive_battle_card: "\u2694\uFE0F",
18646
18751
  competitor: "\u{1F94A}",
18647
18752
  competitor_feature: "\u{1F52C}",
18753
+ competitor_signal: "\u{1F4E1}",
18648
18754
  compliance_framework: "\u{1F3DF}\uFE0F",
18649
18755
  compliance_requirement: "\u26F2",
18650
18756
  constraint: "\u{1F512}",
@@ -20391,8 +20497,8 @@ var MARKET_COMPETITIVE_PLAYBOOK = {
20391
20497
  seqStep(
20392
20498
  6,
20393
20499
  "Moves",
20394
- ["competitive_battle_card"],
20395
- "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.)"
20500
+ ["competitor_signal", "competitive_battle_card"],
20501
+ "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.)"
20396
20502
  )
20397
20503
  ]
20398
20504
  };
@@ -21498,7 +21604,36 @@ var UPG_LENSES = [
21498
21604
  perspective: "Sees the product as something that needs to reach and resonate with people. Audience-aware, message-focused, always asking what makes someone care."
21499
21605
  },
21500
21606
  // ═══════════════════════════════════════════════════════════════════════════════
21501
- // 8. FULL LENS - everything, canonical vocabulary (default)
21607
+ // 8. COMPETITIVE LENS - rivals, their dated moves, and where we stand
21608
+ // ═══════════════════════════════════════════════════════════════════════════════
21609
+ {
21610
+ id: "competitive",
21611
+ name: "Competitive",
21612
+ description: "The competitive landscape: rivals, their offerings, dated moves, and where we stand",
21613
+ icon: "swords",
21614
+ visible_domains: ["market_intelligence"],
21615
+ // Parity (#38) and signal (#41) edges reach onto our side of the graph;
21616
+ // surface those endpoints so a parity row or a mapped signal shows both ends.
21617
+ always_show_types: ["feature", "capability", "opportunity"],
21618
+ playbook_id: "playbook:market-competitive",
21619
+ benchmark_domains: ["market_intelligence"],
21620
+ intelligence_prompts: [
21621
+ {
21622
+ condition: "competitors.length === 0",
21623
+ structured_condition: { check: { type: "entity_count", entity_type: "competitor", comparison: "zero" } },
21624
+ message: "No competitors mapped yet. Name the 3 to 7 closest alternatives, including the status quo and DIY."
21625
+ },
21626
+ {
21627
+ condition: "competitor_signals.length === 0",
21628
+ structured_condition: { check: { type: "entity_count", entity_type: "competitor_signal", comparison: "zero" } },
21629
+ message: "No competitor signals captured. Log rivals' dated moves (launches, pricing changes) and map each onto the feature it threatens."
21630
+ }
21631
+ ],
21632
+ audience: "Product marketers, strategists, and founders sizing up the field",
21633
+ perspective: "Sees the product through its rivals: who competes, what they ship, the dated moves they make, and where we lead or trail."
21634
+ },
21635
+ // ═══════════════════════════════════════════════════════════════════════════════
21636
+ // 9. FULL LENS - everything, canonical vocabulary (default)
21502
21637
  // ═══════════════════════════════════════════════════════════════════════════════
21503
21638
  {
21504
21639
  id: "full",
@@ -21718,7 +21853,7 @@ var USER_RESEARCH_GUIDE = {
21718
21853
  var MARKET_INTELLIGENCE_GUIDE = {
21719
21854
  domain_id: "market_intelligence",
21720
21855
  anchor_entity: "competitive_analysis",
21721
- creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "market_trend", "market_segment", "classification_axis", "classification_value"],
21856
+ creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "classification_axis", "classification_value"],
21722
21857
  patterns: [
21723
21858
  {
21724
21859
  name: "Competitive Landscape Map",
@@ -26215,6 +26350,11 @@ var UPG_REGIONS = [
26215
26350
  type: "competitor_feature",
26216
26351
  role: "leaf"
26217
26352
  },
26353
+ {
26354
+ type: "competitor_signal",
26355
+ role: "leaf",
26356
+ notes: "a dated competitor move (feature launch, pricing change, ...) emitted by a competitor"
26357
+ },
26218
26358
  {
26219
26359
  type: "market_trend",
26220
26360
  role: "leaf"
@@ -26246,6 +26386,7 @@ var UPG_REGIONS = [
26246
26386
  },
26247
26387
  intra_edges: [
26248
26388
  "competitor_offers_competitor_feature",
26389
+ "competitor_emits_competitor_signal",
26249
26390
  "competitive_analysis_analyses_competitor",
26250
26391
  "competitive_analysis_dimensioned_by_classification_axis",
26251
26392
  "classification_axis_includes_classification_value",
@@ -28345,6 +28486,9 @@ function serializeSingleWithHeader(doc, opts) {
28345
28486
  const summary = deriveSummary(product.description);
28346
28487
  if (summary) header.summary = summary;
28347
28488
  header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
28489
+ if (doc.member_kind === "org_rollup" || doc.member_kind === "watched") {
28490
+ header.member_kind = doc.member_kind;
28491
+ }
28348
28492
  header.provenance = buildProvenance(doc, opts);
28349
28493
  header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
28350
28494
  return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
@@ -28364,8 +28508,14 @@ function serializePortfolioWithHeader(doc, opts) {
28364
28508
  };
28365
28509
  const summary = deriveSummary(org.description);
28366
28510
  if (summary) header.summary = summary;
28511
+ const members = doc.products ?? [];
28512
+ const memberKindOf = (p) => p.member_kind ?? "product";
28513
+ const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
28514
+ const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
28367
28515
  header.counts = {
28368
- products: doc.products?.length ?? 0,
28516
+ products: members.filter((p) => memberKindOf(p) === "product").length,
28517
+ ...watchedCount > 0 ? { watched_products: watchedCount } : {},
28518
+ ...rollupCount > 0 ? { org_rollups: rollupCount } : {},
28369
28519
  product_areas: doc.product_areas?.length ?? 0,
28370
28520
  portfolios: doc.portfolios?.length ?? 0,
28371
28521
  cross_edges: doc.cross_edges?.length ?? 0
@@ -28419,6 +28569,8 @@ function normalizeDocument(obj) {
28419
28569
  nodes: (raw.nodes ?? []).map((n) => repairNodeDrift(n)),
28420
28570
  edges: raw.edges ?? []
28421
28571
  };
28572
+ const memberKind = header?.member_kind ?? raw.member_kind;
28573
+ if (memberKind === "org_rollup" || memberKind === "watched") out.member_kind = memberKind;
28422
28574
  if (_integrity) out._integrity = _integrity;
28423
28575
  return out;
28424
28576
  }
@@ -28431,7 +28583,7 @@ function isCanonical(text) {
28431
28583
  }
28432
28584
 
28433
28585
  // src/index.ts
28434
- var UPG_VERSION = "0.9.25";
28586
+ var UPG_VERSION = "0.10.0";
28435
28587
  var UPG_FORMAT_VERSION = "0.4.0";
28436
28588
  var MARKDOWN_FORMAT_VERSION = "0.1";
28437
28589
  var UPG_TYPES = getTypes();