@unified-product-graph/core 0.9.25 → 0.10.1
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 +172 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -8
- package/dist/index.d.ts +203 -8
- package/dist/index.js +172 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 (
|
|
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: "
|
|
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
|
},
|
|
@@ -18146,6 +18250,7 @@ for (const fw of UPG_FRAMEWORKS) {
|
|
|
18146
18250
|
|
|
18147
18251
|
// src/grammar/validate.ts
|
|
18148
18252
|
var crossProductEdgeTypeSet = new Set(UPG_CROSS_EDGE_TYPES);
|
|
18253
|
+
var withinProductEdgeTypeSet = new Set(Object.keys(UPG_EDGE_CATALOG));
|
|
18149
18254
|
var CONTENT_DEPTH_WARNING_RULES = /* @__PURE__ */ new Set([
|
|
18150
18255
|
"property-type",
|
|
18151
18256
|
"property-enum",
|
|
@@ -18248,7 +18353,7 @@ function validateUPGDocument(doc) {
|
|
|
18248
18353
|
if (!e.id || typeof e.id !== "string") {
|
|
18249
18354
|
errors.push({ path: `${path}.id`, message: "Edge id is required and must be a string" });
|
|
18250
18355
|
}
|
|
18251
|
-
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type)) {
|
|
18356
|
+
if (typeof e.type === "string" && crossProductEdgeTypeSet.has(e.type) && !withinProductEdgeTypeSet.has(e.type)) {
|
|
18252
18357
|
errors.push({
|
|
18253
18358
|
path: `${path}.type`,
|
|
18254
18359
|
message: `Cross-product edge type "${e.type}" must live in portfolio.cross_edges[], not product edges[].`
|
|
@@ -18535,6 +18640,7 @@ function valueMatchesType(defType, val) {
|
|
|
18535
18640
|
case "boolean":
|
|
18536
18641
|
return kind === "boolean";
|
|
18537
18642
|
case "string[]":
|
|
18643
|
+
case "object[]":
|
|
18538
18644
|
return kind === "string[]";
|
|
18539
18645
|
case "object":
|
|
18540
18646
|
case "assessment":
|
|
@@ -18645,6 +18751,7 @@ var ENTITY_EMOJI = {
|
|
|
18645
18751
|
competitive_battle_card: "\u2694\uFE0F",
|
|
18646
18752
|
competitor: "\u{1F94A}",
|
|
18647
18753
|
competitor_feature: "\u{1F52C}",
|
|
18754
|
+
competitor_signal: "\u{1F4E1}",
|
|
18648
18755
|
compliance_framework: "\u{1F3DF}\uFE0F",
|
|
18649
18756
|
compliance_requirement: "\u26F2",
|
|
18650
18757
|
constraint: "\u{1F512}",
|
|
@@ -20391,8 +20498,8 @@ var MARKET_COMPETITIVE_PLAYBOOK = {
|
|
|
20391
20498
|
seqStep(
|
|
20392
20499
|
6,
|
|
20393
20500
|
"Moves",
|
|
20394
|
-
["competitive_battle_card"],
|
|
20395
|
-
"
|
|
20501
|
+
["competitor_signal", "competitive_battle_card"],
|
|
20502
|
+
"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
20503
|
)
|
|
20397
20504
|
]
|
|
20398
20505
|
};
|
|
@@ -21498,7 +21605,36 @@ var UPG_LENSES = [
|
|
|
21498
21605
|
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
21606
|
},
|
|
21500
21607
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21501
|
-
// 8.
|
|
21608
|
+
// 8. COMPETITIVE LENS - rivals, their dated moves, and where we stand
|
|
21609
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21610
|
+
{
|
|
21611
|
+
id: "competitive",
|
|
21612
|
+
name: "Competitive",
|
|
21613
|
+
description: "The competitive landscape: rivals, their offerings, dated moves, and where we stand",
|
|
21614
|
+
icon: "swords",
|
|
21615
|
+
visible_domains: ["market_intelligence"],
|
|
21616
|
+
// Parity (#38) and signal (#41) edges reach onto our side of the graph;
|
|
21617
|
+
// surface those endpoints so a parity row or a mapped signal shows both ends.
|
|
21618
|
+
always_show_types: ["feature", "capability", "opportunity"],
|
|
21619
|
+
playbook_id: "playbook:market-competitive",
|
|
21620
|
+
benchmark_domains: ["market_intelligence"],
|
|
21621
|
+
intelligence_prompts: [
|
|
21622
|
+
{
|
|
21623
|
+
condition: "competitors.length === 0",
|
|
21624
|
+
structured_condition: { check: { type: "entity_count", entity_type: "competitor", comparison: "zero" } },
|
|
21625
|
+
message: "No competitors mapped yet. Name the 3 to 7 closest alternatives, including the status quo and DIY."
|
|
21626
|
+
},
|
|
21627
|
+
{
|
|
21628
|
+
condition: "competitor_signals.length === 0",
|
|
21629
|
+
structured_condition: { check: { type: "entity_count", entity_type: "competitor_signal", comparison: "zero" } },
|
|
21630
|
+
message: "No competitor signals captured. Log rivals' dated moves (launches, pricing changes) and map each onto the feature it threatens."
|
|
21631
|
+
}
|
|
21632
|
+
],
|
|
21633
|
+
audience: "Product marketers, strategists, and founders sizing up the field",
|
|
21634
|
+
perspective: "Sees the product through its rivals: who competes, what they ship, the dated moves they make, and where we lead or trail."
|
|
21635
|
+
},
|
|
21636
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21637
|
+
// 9. FULL LENS - everything, canonical vocabulary (default)
|
|
21502
21638
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
21503
21639
|
{
|
|
21504
21640
|
id: "full",
|
|
@@ -21718,7 +21854,7 @@ var USER_RESEARCH_GUIDE = {
|
|
|
21718
21854
|
var MARKET_INTELLIGENCE_GUIDE = {
|
|
21719
21855
|
domain_id: "market_intelligence",
|
|
21720
21856
|
anchor_entity: "competitive_analysis",
|
|
21721
|
-
creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "market_trend", "market_segment", "classification_axis", "classification_value"],
|
|
21857
|
+
creation_sequence: ["competitive_analysis", "competitor", "competitor_feature", "competitor_signal", "market_trend", "market_segment", "classification_axis", "classification_value"],
|
|
21722
21858
|
patterns: [
|
|
21723
21859
|
{
|
|
21724
21860
|
name: "Competitive Landscape Map",
|
|
@@ -26215,6 +26351,11 @@ var UPG_REGIONS = [
|
|
|
26215
26351
|
type: "competitor_feature",
|
|
26216
26352
|
role: "leaf"
|
|
26217
26353
|
},
|
|
26354
|
+
{
|
|
26355
|
+
type: "competitor_signal",
|
|
26356
|
+
role: "leaf",
|
|
26357
|
+
notes: "a dated competitor move (feature launch, pricing change, ...) emitted by a competitor"
|
|
26358
|
+
},
|
|
26218
26359
|
{
|
|
26219
26360
|
type: "market_trend",
|
|
26220
26361
|
role: "leaf"
|
|
@@ -26246,6 +26387,7 @@ var UPG_REGIONS = [
|
|
|
26246
26387
|
},
|
|
26247
26388
|
intra_edges: [
|
|
26248
26389
|
"competitor_offers_competitor_feature",
|
|
26390
|
+
"competitor_emits_competitor_signal",
|
|
26249
26391
|
"competitive_analysis_analyses_competitor",
|
|
26250
26392
|
"competitive_analysis_dimensioned_by_classification_axis",
|
|
26251
26393
|
"classification_axis_includes_classification_value",
|
|
@@ -28345,6 +28487,9 @@ function serializeSingleWithHeader(doc, opts) {
|
|
|
28345
28487
|
const summary = deriveSummary(product.description);
|
|
28346
28488
|
if (summary) header.summary = summary;
|
|
28347
28489
|
header.counts = { nodes: doc.nodes?.length ?? 0, edges: doc.edges?.length ?? 0 };
|
|
28490
|
+
if (doc.member_kind === "org_rollup" || doc.member_kind === "watched") {
|
|
28491
|
+
header.member_kind = doc.member_kind;
|
|
28492
|
+
}
|
|
28348
28493
|
header.provenance = buildProvenance(doc, opts);
|
|
28349
28494
|
header.integrity = { algorithm: INTEGRITY_ALGORITHM, body: computeBodyChecksum(doc) };
|
|
28350
28495
|
return JSON.stringify({ $upg: header, ...body }, null, 2) + "\n";
|
|
@@ -28364,8 +28509,14 @@ function serializePortfolioWithHeader(doc, opts) {
|
|
|
28364
28509
|
};
|
|
28365
28510
|
const summary = deriveSummary(org.description);
|
|
28366
28511
|
if (summary) header.summary = summary;
|
|
28512
|
+
const members = doc.products ?? [];
|
|
28513
|
+
const memberKindOf = (p) => p.member_kind ?? "product";
|
|
28514
|
+
const watchedCount = members.filter((p) => memberKindOf(p) === "watched").length;
|
|
28515
|
+
const rollupCount = members.filter((p) => memberKindOf(p) === "org_rollup").length;
|
|
28367
28516
|
header.counts = {
|
|
28368
|
-
products:
|
|
28517
|
+
products: members.filter((p) => memberKindOf(p) === "product").length,
|
|
28518
|
+
...watchedCount > 0 ? { watched_products: watchedCount } : {},
|
|
28519
|
+
...rollupCount > 0 ? { org_rollups: rollupCount } : {},
|
|
28369
28520
|
product_areas: doc.product_areas?.length ?? 0,
|
|
28370
28521
|
portfolios: doc.portfolios?.length ?? 0,
|
|
28371
28522
|
cross_edges: doc.cross_edges?.length ?? 0
|
|
@@ -28419,6 +28570,8 @@ function normalizeDocument(obj) {
|
|
|
28419
28570
|
nodes: (raw.nodes ?? []).map((n) => repairNodeDrift(n)),
|
|
28420
28571
|
edges: raw.edges ?? []
|
|
28421
28572
|
};
|
|
28573
|
+
const memberKind = header?.member_kind ?? raw.member_kind;
|
|
28574
|
+
if (memberKind === "org_rollup" || memberKind === "watched") out.member_kind = memberKind;
|
|
28422
28575
|
if (_integrity) out._integrity = _integrity;
|
|
28423
28576
|
return out;
|
|
28424
28577
|
}
|
|
@@ -28431,7 +28584,7 @@ function isCanonical(text) {
|
|
|
28431
28584
|
}
|
|
28432
28585
|
|
|
28433
28586
|
// src/index.ts
|
|
28434
|
-
var UPG_VERSION = "0.
|
|
28587
|
+
var UPG_VERSION = "0.10.1";
|
|
28435
28588
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
28436
28589
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
28437
28590
|
var UPG_TYPES = getTypes();
|