@unified-product-graph/core 0.10.7 → 0.11.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/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.11.0] - 2026-06-14
11
+
12
+ **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.
13
+
14
+ ### Added
15
+ - **`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.
16
+ - **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.
17
+ - **`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.
18
+
19
+ ### Changed
20
+ - **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.
21
+
22
+ ---
23
+
24
+ ## [0.10.8] - 2026-06-14
25
+
26
+ **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.
27
+
28
+ ### Added
29
+ - **`audit_property_coverage` (new tool, 127 total; local-only).** Given an `edge_type` and the `required_keys` that should be present, returns the portfolio cross-edges that lack any of them (`missing: [{ edge_id, source, target, source_title?, target_title?, missing_keys }]`) plus, by default, the edges whose present values fail the type property schema (`malformed`). The completeness check that distinguishes "I ran the writes" from "the data is actually backfilled," with resolved titles, without a shell.
30
+ - **`dry_run` flag on `create_cross_product_edge` and `batch_create_cross_product_edges`.** Forecasts the write (`would: create | update | unchanged`, and `would_counts` for the batch) without mutating the portfolio document. The pre-flight that makes a large backfill safe to reason about before it runs. Backed by a non-mutating `previewCrossEdge` on the portfolio store that mirrors `addCrossEdge`'s create/update/unchanged decision (including the property-upsert merge) exactly.
31
+
32
+ ### Changed
33
+ - **`list_portfolio_cross_edges` gained a freshness filter.** `older_than_days` (relative to now) and `assessed_before` (absolute ISO date) return the stale set: edges whose `properties.assessed_on` is older than the cutoff, or absent entirely (never assessed counts as stalest). The read path for "which cells need re-checking" — the trigger of a self-updating competitive tier.
34
+
10
35
  ## [0.10.7] - 2026-06-14
11
36
 
12
37
  **Makes the classification landscape renderable: a portfolio tree, title resolution, and projected/paginated cross-edge reads.** 0.10.6 made classifications *queryable* (traversal, distribution, idempotent upsert). But nothing on the MCP surface rendered the result as a tree, and the nearest read overflowed the transport cap and returned opaque ids — the trees were only producible by dropping to `jq` over a dumped portfolio file, a path no fresh agent can find. This release closes that discoverability gap. Read-path only; no schema or data migration.
package/dist/index.cjs CHANGED
@@ -9949,9 +9949,13 @@ var UPG_PROPERTY_SCHEMA = {
9949
9949
  // CompetitorSignalProperties: Competitor signal: a dated competitor move mapped onto our portfolio.
9950
9950
  competitor_signal: {
9951
9951
  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." },
9952
+ 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
9953
  summary: { type: "string", description: "One-line factual summary of the move (what shipped, not marketing copy)." },
9954
9954
  impact: { type: "string", enum: ["high", "medium", "low"], description: "Expected impact on our position." },
9955
+ 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." },
9956
+ 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." },
9957
+ 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)." },
9958
+ 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
9959
  last_updated: { type: "string", description: 'Provenance: ISO date-time this record was last observed or refreshed. @example "2026-06-13"' },
9956
9960
  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
9961
  confidence: {
@@ -28603,7 +28607,11 @@ function portfolioBody(doc) {
28603
28607
  nodes: sortNodes(doc.registry.nodes).map(canonicalNode),
28604
28608
  ...doc.registry.edges && doc.registry.edges.length > 0 ? { edges: sortEdges(doc.registry.edges).map(canonicalEdge) } : {}
28605
28609
  }
28606
- } : {}
28610
+ } : {},
28611
+ // Append-only classification-history stream (0.11.0). Emitted only when
28612
+ // non-empty so existing portfolio files without it stay byte-identical.
28613
+ // Each entry is a competitor_signal node (serialised with the node rules).
28614
+ ...doc.signals && doc.signals.length > 0 ? { signals: sortNodes(doc.signals).map(canonicalNode) } : {}
28607
28615
  };
28608
28616
  }
28609
28617
  function computeBodyChecksum(doc) {
@@ -28719,6 +28727,10 @@ function normalizeDocument(obj) {
28719
28727
  ...Array.isArray(rawRegistry.edges) ? { edges: rawRegistry.edges } : {}
28720
28728
  };
28721
28729
  }
28730
+ const rawSignals = raw.signals;
28731
+ if (Array.isArray(rawSignals) && rawSignals.length > 0) {
28732
+ out2.signals = rawSignals.map((n) => repairNodeDrift(n));
28733
+ }
28722
28734
  return out2;
28723
28735
  }
28724
28736
  const out = {
@@ -28743,7 +28755,7 @@ function isCanonical(text) {
28743
28755
  }
28744
28756
 
28745
28757
  // src/index.ts
28746
- var UPG_VERSION = "0.10.7";
28758
+ var UPG_VERSION = "0.11.0";
28747
28759
  var UPG_FORMAT_VERSION = "0.4.0";
28748
28760
  var MARKDOWN_FORMAT_VERSION = "0.1";
28749
28761
  var UPG_TYPES = getTypes();