@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 +28 -0
- package/dist/index.cjs +29 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -4
- package/dist/index.d.ts +68 -4
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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,
|