@unified-product-graph/core 0.10.8 → 0.11.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/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.11.1] - 2026-06-14
11
+
12
+ **Bug fix: the typed classification writer misrouted competitor sources, duplicated instead of upserting, and disagreed with the confidence scale.** The generic cross-edge writers got their in-place upsert in 0.10.6; the typed convenience writer `create_classification_edge` never inherited it, and on a qualified `{product}/{node}` competitor source it wrote the wrong edge type, created a duplicate, and expanded `high` to a different `confidence_5` value than the rest of the graph. Three defects in one call (Data's dogfood brief), all on a path the generic writers already do correctly. No data migration.
13
+
14
+ ### Fixed
15
+ - **`create_classification_edge` now upserts instead of duplicating.** Root cause was the misroute below: a qualified competitor source was mis-typed as the polymorphic `node_classified_as_classification_value`, so the `(source, target, type)` dedup never matched the existing `competitor_…` edge and a second edge was created. Correct routing makes the write land on the existing cell in place (`status: "updated"`).
16
+ - **Qualified-source type routing.** A `{product}/{node}` source whose node is a `competitor` now resolves to `competitor` — via the owning product file, and failing that the portfolio's `instance_of` index (which maps `{pid}/{nid}` to its canonical type without a local product file) — and writes `competitor_classified_as_classification_value`. Bare local sources keep the polymorphic type.
17
+ - **Confidence-scale agreement.** `high` now expands to `confidence_5` value **4** (canonical label `Confident`), not 5 — matching the generic writers and the existing classify-edge population, so two writers can no longer populate the same axis with different numbers for the same word.
18
+
19
+ ### Added
20
+ - **Pinned friendly confidence mapping on the scale (`confidence_5.friendly_aliases = { low: 2, medium: 3, high: 4 }`).** The single, introspectable source of truth — surfaced via `get_scale` — that `create_classification_edge` (and any future friendly-confidence writer) resolves through `friendlyToAssessment(scaleId, word)`. The expansion carries the canonical point label (`Confident`), not the input word.
21
+
22
+ ---
23
+
24
+ ## [0.11.0] - 2026-06-14
25
+
26
+ **Self-documenting competitive history: a competitor's classification change records itself.** The 0.10.x tier classifies competitors against axes and values and carries `confidence` / `assessed_on`, but a re-assessment overwrites the prior value — there was no way to ask *what moved*. This release makes the classification landscape remember its own changes. A minor bump: it widens the `competitor_signal.signal_type` enum (union widening), which spec policy treats as a minor; additive, so old graphs still parse.
27
+
28
+ ### Added
29
+ - **`reclassification` shape on `competitor_signal`.** The `signal_type` enum gains `reclassification`, and the type carries four optional transition properties — `competitor` (the classify-edge source, identifying subject + product), `axis`, `from_value`, `to_value`. A reclassification is one more kind of the dated competitor move the entity already models.
30
+ - **Append-only `signals[]` collection on the portfolio document.** A portfolio-scoped, optional, additive collection holding the classification-history stream. Kept here rather than in `registry.nodes` (so it never pollutes the canonical-vocabulary tier or the landscape/tree reads) and rather than in a product graph (so the auto-emit stays atomic with the portfolio cross-edge write). Round-trips through the canonical serializer; portfolios without it stay byte-identical.
31
+ - **`diff_classification` (new tool, 128 total; local-only).** `diff_classification({ product?, competitor?, since? })` reads the reclassification history and projects each move (`from_value` to `to_value` on an axis) with resolved titles, newest first. "Did Sitecore move from integrated to agentic since last quarter" is one call. Pairs with the 0.10.8 freshness filter (which decides *when* to re-assess); this surfaces *what* changed.
32
+
33
+ ### Changed
34
+ - **Classify writes auto-record a move.** When `create_cross_product_edge` / `batch_create_cross_product_edges` create a classify edge that supersedes a sibling classification of the same competitor on the *same axis* (resolved via registry edge or `axis:` tag), the portfolio store appends a `reclassification` signal — completeness by construction, no caller discipline. Record-only and non-destructive: the superseded edge is kept (a `categorical` axis may legitimately carry several values), so current-state reads are untouched. First-time classifications are not moves; unaxed values are skipped (a single-axis move cannot be proven); an identical transition is logged at most once.
35
+
36
+ ---
37
+
10
38
  ## [0.10.8] - 2026-06-14
11
39
 
12
40
  **Operational tooling for the property layer: coverage audit, write pre-flight, freshness query.** The 218-edge confidence backfill (0.10.6) needed five manual motions the MCP did not expose, mostly `jq` over `portfolio.upg`. This release lands the three near-term ones from Data's tooling-gaps brief. Read-path plus a non-mutating write flag; no schema or data migration.
package/dist/index.cjs CHANGED
@@ -119,6 +119,7 @@ __export(index_exports, {
119
119
  evaluateAntiPatterns: () => evaluateAntiPatterns,
120
120
  findInvalidStatusMigrationTargets: () => findInvalidStatusMigrationTargets,
121
121
  formatUpgText: () => formatUpgText,
122
+ friendlyToAssessment: () => friendlyToAssessment,
122
123
  generateSlug: () => generateSlug,
123
124
  getAnchorEntity: () => getAnchorEntity,
124
125
  getAntiPatternById: () => getAntiPatternById,
@@ -6801,7 +6802,11 @@ var UPG_SCALES = {
6801
6802
  { value: 3, label: "Some evidence", description: "A few data points" },
6802
6803
  { value: 4, label: "Confident", description: "Multiple data sources" },
6803
6804
  { value: 5, label: "Data-backed", description: "Strong quantitative evidence" }
6804
- ]
6805
+ ],
6806
+ // The pinned friendly mapping (0.11.1). `high` is Confident (4), not
6807
+ // Data-backed (5) — 5 is reserved for genuinely quantified claims. Matches
6808
+ // the existing classify-edge population, so nothing needs re-stamping.
6809
+ friendly_aliases: { low: 2, medium: 3, high: 4 }
6805
6810
  },
6806
6811
  // ── Effort ─────────────────────────────────────────────────────────────────
6807
6812
  effort_5: {
@@ -6823,6 +6828,13 @@ var UPG_SCALES = {
6823
6828
  function getScale(scaleId) {
6824
6829
  return UPG_SCALES[scaleId];
6825
6830
  }
6831
+ function friendlyToAssessment(scaleId, word) {
6832
+ const scale = UPG_SCALES[scaleId];
6833
+ const value = scale?.friendly_aliases?.[word];
6834
+ if (value === void 0) return null;
6835
+ const point = scale.points.find((p) => p.value === value);
6836
+ return { value, label: point?.label ?? word, scale_id: scaleId };
6837
+ }
6826
6838
  var PROPERTY_SCALE_MAP = {
6827
6839
  // ── Reach ────────────────────────────────────────────────────────────────
6828
6840
  reach: "reach_5",
@@ -9949,9 +9961,13 @@ var UPG_PROPERTY_SCHEMA = {
9949
9961
  // CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
9950
9962
  competitor_signal: {
9951
9963
  observed_at: { type: "string", description: 'ISO date-time the move was observed. @example "2026-06-10"' },
9952
- 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." },
9964
+ signal_type: { type: "string", enum: ["feature_launch", "pricing_change", "acquisition", "partnership", "market_entry", "reclassification"], description: "Kind of move. `feature_launch` = shipped a feature. `pricing_change` = a plan or tier change. `acquisition` / `partnership` / `market_entry` = strategic moves. `reclassification` = the competitor moved between classification cells on an axis (e.g. ai_integrated to ai_agentic); auto-emitted at the classify-write chokepoint and carries `axis` / `from_value` / `to_value` / `competitor`." },
9953
9965
  summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
9954
9966
  impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
9967
+ competitor: { type: "string", description: "Reclassification only. The qualified id of the competitor that moved, as the classify cross-edge source (e.g. `p_rival/n_acme`). Identifies both the subject and its owning product, so `diff_classification({ product })` can filter the history stream." },
9968
+ axis: { type: "string", description: "Reclassification only. The `classification_axis` id the move is on (e.g. `ca_ai_maturity`). Mirrors the axis the superseded and new classify edges share." },
9969
+ from_value: { type: "string", description: "Reclassification only. The prior `classification_value` id the competitor was classified as before this move (the superseded cell). Absent for a first-time classification (nothing was superseded)." },
9970
+ to_value: { type: "string", description: "Reclassification only. The new `classification_value` id the competitor is classified as after this move (the cell the new classify edge points at)." },
9955
9971
  last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. @example "2026-06-13"' },
9956
9972
  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/"' },
9957
9973
  confidence: {
@@ -28603,7 +28619,11 @@ function portfolioBody(doc) {
28603
28619
  nodes: sortNodes(doc.registry.nodes).map(canonicalNode),
28604
28620
  ...doc.registry.edges && doc.registry.edges.length > 0 ? { edges: sortEdges(doc.registry.edges).map(canonicalEdge) } : {}
28605
28621
  }
28606
- } : {}
28622
+ } : {},
28623
+ // Append-only classification-history stream (0.11.0). Emitted only when
28624
+ // non-empty so existing portfolio files without it stay byte-identical.
28625
+ // Each entry is a competitor_signal node (serialised with the node rules).
28626
+ ...doc.signals && doc.signals.length > 0 ? { signals: sortNodes(doc.signals).map(canonicalNode) } : {}
28607
28627
  };
28608
28628
  }
28609
28629
  function computeBodyChecksum(doc) {
@@ -28719,6 +28739,10 @@ function normalizeDocument(obj) {
28719
28739
  ...Array.isArray(rawRegistry.edges) ? { edges: rawRegistry.edges } : {}
28720
28740
  };
28721
28741
  }
28742
+ const rawSignals = raw.signals;
28743
+ if (Array.isArray(rawSignals) && rawSignals.length > 0) {
28744
+ out2.signals = rawSignals.map((n) => repairNodeDrift(n));
28745
+ }
28722
28746
  return out2;
28723
28747
  }
28724
28748
  const out = {
@@ -28743,7 +28767,7 @@ function isCanonical(text) {
28743
28767
  }
28744
28768
 
28745
28769
  // src/index.ts
28746
- var UPG_VERSION = "0.10.8";
28770
+ var UPG_VERSION = "0.11.1";
28747
28771
  var UPG_FORMAT_VERSION = "0.4.0";
28748
28772
  var MARKDOWN_FORMAT_VERSION = "0.1";
28749
28773
  var UPG_TYPES = getTypes();
@@ -28907,6 +28931,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
28907
28931
  evaluateAntiPatterns,
28908
28932
  findInvalidStatusMigrationTargets,
28909
28933
  formatUpgText,
28934
+ friendlyToAssessment,
28910
28935
  generateSlug,
28911
28936
  getAnchorEntity,
28912
28937
  getAntiPatternById,