@unified-product-graph/core 0.33.0 → 0.34.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,55 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.34.0] - 2026-08-22
11
+
12
+ Minor, additive, with **one stated compile-time break** (see Changed). Zero new entity types. Two edges, one tool, one check, three optional fields, and seven normative rulings on key machinery. The through-line: 0.33.0 gave the graph reach and left the reaching unmeasured, so most of this release is places where a shape existed, a consumer existed, and nothing joined them.
13
+
14
+ ### Added
15
+ - **`document_transcludes_node`**, polymorphic, `semantic`, `deliberate_only`, with **no position property**. `describes` says a document is ABOUT a thing; transclusion says it RENDERS that node where the prose sits, so the value a reader sees is current by construction. Collapsing them would lose the only property transclusion is bought for. The item halved on discovery: the inline anchor form already existed, was parsed with source line numbers, and was published in the paper. What was missing was the BINDING, now normative: an anchor in a `document` body IS a transclusion anchor and a conformant parser writes the edge beside it. Measured before minting: 43 document nodes, 28 of them (65%) with zero outbound edge, and the canonical case was unexpressible because `metric` is not among the nine `document_describes_*` targets. No `anchor_line`, deliberately: a line number is wrong after the next paragraph above it and nothing reports the drift. The anchor IS the position.
16
+ - **`person_holds_role`**, `semantic`, `person` to `role`, verbs `holds` / `held_by`, unqualified. Minted on a grammar-versus-catalog inconsistency readable entirely inside the spec rather than on the field condition it was banked against, and that distinction is stated rather than blurred: `role` terminates at `filled` and `vacant`, `team_staffed_with_role` runs team to slot, `person_member_of_team` runs person to team, and nothing connected a person to a role. **A graph could legally mark a role `filled` and no edge could say by whom.** A terminal phase no edge can substantiate is dead schema, and worse than a phantom edge name because a phantom is inert while `filled` is a value graphs carry. `semantic` and not `cross-domain`: person, role and team are one domain, and the guardrail forbids new same-domain offenders. Direction is person to role, so the containment reading stays unavailable rather than merely unchosen.
17
+ - **`orphan_disposition?: 'root' | 'hide'` on `UPGViewPresentation`, absent means `'root'`.** 0.33.0's `nest_by` made nesting portable and left the FAILURE of nesting unportable. Measured on a real imported tracker graph of 1,118 nodes: one tree selects 218 members and renders ONE card under `hide`; three more go 185 to 1, one goes 11 to 1. The orphans ARE the dataset, because tracker-imported work wires through reference-axis relations these trees never traverse. Absent-means-root makes "everything the scope admits is visible somewhere" a checkable invariant, and the majority that prefers hiding pays for it EXPLICITLY, which is the right way round for a default deciding whether data disappears. Reclassified from application chassis to spec BY MEASUREMENT: running the two dispositions changes which nodes APPEAR, not where pixels go.
18
+ - **`provenance?: UPGEdgeProvenance` on `UPGEdge`** (`tool?`, `from?`, `at?`), **ungated**. The gate question is settled by precedent rather than by argument: `mapping_confidence` is already an ungated provenance field on every edge. `carries_properties` exists to keep semantic edges free of domain PAYLOAD; provenance is a fact about the record, and requiring an edge to opt in to having a history is the wrong shape. Nested because `UPGEdge.source` already means the source NODE, so a flat `source_id` would sit one character away meaning something else. The node side has carried provenance since the beginning; a bulk emission of 651 derived edges had nowhere on the edge to record what it derived them from.
19
+ - **`upsert_composition`** (98 to 99 tools). ONE tool, not the three asked for: `get_node` already returns a node AND its edges, so the focus join is served, and the id IS the slug, so address-by-slug works. The only thing the generic path gets actively WRONG is `rev`, which `update_node` writes from the caller's argument, silently. Write semantics are MIRRORED from the shipped `graph-service` contract rather than designed: `rev` re-derived inside the write and incremented only on a transition into `published`; `rev` as an optional precondition returning a stale-revision outcome with the stored value; omitting `members` preserving the arrangement; node and focus edges written together. The tool declares the view shapes INLINE in its own input schema, which is half its justification: they are `object` in the runtime registry, so schema introspection otherwise hands an agent three opaque blobs.
20
+ - **`duplicate-key-across-products`**, a fifth `scope: 'portfolio'` anti-pattern (25 to 26), **ENFORCED**. The same `(prefix, number)` pair identifying nodes in two products of one portfolio. Mechanically decidable, which is unusual in this catalog and is why it can ship enforced; every other portfolio entry approximates a per-node rule. What was staged is the CORPUS, not the enforcement: there is no defined-then-enforced mechanic and a registered detector that declines to fire is worse than either real option. **This release also discovered that a registered anti-pattern had no evaluator behind it** and wired one, so the claim and the code now agree.
21
+ - **`UPG_PHASE_COUNT` = 342**, distinct (template, phase) pairs, wired into `check:count-drift`. The last figure in the truth line that no check asserted, and therefore the only one that drifted. 869 counts phase ROWS and double-counts every shared template; 334 was transcribed forward since 0.32.0 and reproduces under neither computation. The definition lives on the export so the gate and the docket row cannot be derived twice.
22
+ - **`UPG_POLYMORPHIC_EDGE_FAMILIES`** is now an export, and `EDGE_KEY_ORDER` / `CROSS_EDGE_KEY_ORDER` join `NODE_KEY_ORDER` in being exported and asserted.
23
+
24
+ ### Changed
25
+ - **`UPGViewClause` is a discriminated union on `dimension`**, so the `type` axis carries `UPGEntityType[]` and every other axis keeps `string[]`. **The wire format does not change at all: the JSON is identical, no migration, no fixture.** What changes is compile-time, and **this is a break; calling it additive would be dishonest.** Two measured facts make it acceptable: the field shipped ONE release ago, and the only known consumer already casts at this exact boundary with a comment explaining that the tiers disagree, so the break lands where someone has already written down why. A stated break with one known site beats a silent narrowing with none. The round-trip rule ships regardless of the union, because a union constrains authors and not JSON arriving from a file: **the clause list is authoritative, the shorthand is a positive-only projection, and a consumer that cannot represent a clause MUST REFUSE rather than narrow.**
26
+ - **A declared team prefix supersedes `product.key_prefix` and NOTHING ELSE.** The candidate set is the UNION of declared and observed prefixes, and the picker keeps asking while more than one stands. The shipped 0.33.0 sentence made ANY declaration replace the candidate set outright, while its own next sentence explains the rule entirely in terms of a single string that cannot represent two teams. An OBSERVED prefix is not that: it is evidence of a namespace already in use. Measured live: the only keyed graph in the estate carries two observed prefixes, 370 keys under one and 662 under the other, and declares neither. Under the wider reading one team declaring the smaller prefix collapses the candidate set, the picker disappears, and every later create mints under it, including the other 662 keys' worth of work. Shipped as a CHANGE rather than a patch correction because implementers had already built to the wider text.
27
+ - **Four further key-machinery rulings become normative.** The HOME PRODUCT is decided by EVIDENCE rather than by a stored marker (derivable, needs no migration, cannot go stale, cannot disagree with the keys). The UNDECIDABLE case refuses BOTH mints with no invented tiebreak, because creation order is not recorded and `max(existing)` measures import volume rather than precedence. "IN SCOPE" is ENGINE-DEFINED with a floor (every product the engine can enumerate for this caller) and a security ceiling (never a product the caller could not otherwise read); portfolio altitude is measurably the wrong altitude, since deleting the portfolio document changes nothing about minting. The honest consequence is stated rather than left to be discovered: **the invariant is scope-relative, so two engines can legitimately disagree about whether one mint is safe.** And `team.key_prefix` is IMMUTABLE once anything has minted under it IN THAT PRODUCT, refusal-shaped, scoped per product because a portfolio-shared team must stay free to declare in a product it has never minted in.
28
+ - **`check:editorial`'s fingerprint is hardened in five places.** It now attributes polymorphic edges through the family partition (a wildcard edge was attributed to NOBODY, so the 0.33.0 work-item widening moved zero fingerprints on `task`, `bug`, `user_story` or `feature`); hashes the deprecation clause rather than its truthiness; hashes the description of `object`-typed properties, where the description IS the entire declared shape; reads BOTH edge registries, where it had been reading one while its sibling function was fixed a release ago; and reads four more prose fields from the published docs. The sensitivity change was discharged with a per-arm before/after run against real published releases rather than asserted.
29
+ - **GitLab emits project membership for every work-item type**, not only epics. It was the only one of five adapters gating the emission on a concrete type, so an ordinary issue resolving to `task` or `bug` carried the project id and never reached the seam. Keyed on the work-item allowlist, never on the wildcard.
30
+ - **The field-graph migration pass is a named release step** with the releasing lane as its owner. It walks every graph, reports which migrations are due, and NEVER rewrites: when a graph is migrated the version bump is that pass's output rather than an incidental edit, and "not migrated, here is why" is a legitimate outcome. A report and not a gate, because blocking a release on a field graph being behind makes the honest answer the expensive one.
31
+
32
+ ### Fixed
33
+ - **`UPGCrossEdge.properties` was never in `CROSS_EDGE_KEY_ORDER`**, and the cross-edge canonicaliser never treated it as an open bag. Declared since 0.10.0. Nothing was corrupt, because unlisted keys fall to a deterministic sorted tail, but two writers emitting the same parity assessment with their keys in a different order produced different bytes, which breaks this format's writer-agnosticism contract. Found by the new subset assertion on its first run, eleven minors after the node twin got the same guard. Measured before fixing, because a canonical-order edit rewrites bytes: **zero of the 61 cross-edges in the estate carry properties**, so the fix is free today and would not have been after the first parity assessment landed.
34
+ - **The cross-only tier's declared target contract** said these edges point at a registry entry. All three live instances target a product-qualified id and the registry is EMPTY, so a reader following the comment would look for something that has never existed. Both forms are legitimate; the field has only produced the second.
35
+ - **`UPGEdge.properties`' JSDoc named one edge as carrying the gate; eleven set it.** One word, "currently", doing the work of a gate nobody wired, wrong for ten mints.
36
+ - **A citation check called correct prose a fabricated edge name.** Widening the editorial read set surfaced it immediately: a data-product name that begins with a real entity type matched a head-only test. A real edge name ends in the type it points AT, so the check now requires both ends. Ruled rather than allowlisted, and rather than asked of the doc author: the prose was correct and the detector was not.
37
+
38
+ ### Notes
39
+ - Truth: **324 entities · 1086 edges · 69 cross-edges · 26 anti-patterns · 99 tools · 26 polymorphic keys in 15 families · 342 lifecycle phases**, the last of these gated for the first time. No entity minted. Cross-edges unchanged because `document`, `person` and `role` are not `portfolio_shared`, so nothing here can be eligible.
40
+ - **Zero spec-side migrations.** Both new edges are new types with nothing to convert, all three new fields are optional, and the view-clause union changes no bytes.
41
+ - **`UPG_CROSS_ONLY_EDGE_TYPES` has NO runtime consumer**, recorded exhaustively. No writer reads it. The tier is a declared vocabulary that nothing validates at write time, which changes what a non-colliding key would buy: discipline, not a guarantee.
42
+ - **The within-graph conformance edge is NOT minted**, and its banked condition is restated to the case that is actually live. It is not "a specification in a graph with no portfolio", which no graph instantiates. It is the HOLDER PRODUCT: the graph that holds a specification has three siblings reaching it through the cross tier and no way to state its own relation to it. The verb the live case wants is `defines` rather than `conforms_to`.
43
+ - **Migration note corrections carried on `external_links`.** The overflow residue is **179, not 162** (a second bag key was never counted), and **524 of 528 nodes duplicate their own canonical reference inside the parked bag**, so a straight copy would put the canonical artifact into the non-canonical list on 99% of that field's first real population. Subtract before copying.
44
+ - **The labeled-fixture doctrine is discharged for the first time.** The new check ships with a corpus whose provenance is machine-readable and required, whose near-misses are all labelled CONSTRUCTED, and which carries a dated obligation to re-grade precision and recall on the first sampled collision. Until then the recall figure is a claim about the author's imagination, and the corpus says so in its own output rather than in a plan nobody opens.
45
+ - **A limitation this release demonstrates rather than describes.** The seven key-machinery rulings are the most consequential text in it and not one of them moves an editorial fingerprint, because prose is excluded by design. The gate that exists to catch documentation going stale cannot see the change most likely to be built against wrongly.
46
+ - Banked with conditions, unchanged: `rename_bag_key`; SDK patch derivation from `UPG_BASE_NODE_FIELDS` (this release adds no base-node field, so the trigger did not fire); re-classifying the a11y conformance edge from `hierarchy` to `semantic`; widening `document_describes_*` to the node wildcard, whose own evidence is real and separate; the within-graph conformance edge. Newly banked: an entity-description export so the editorial gate can hash descriptions on BOTH sides of a release comparison, which is what stops that arm being inert. **The `NODE_KEY_ORDER` subset assertion is CLOSED: it shipped in 0.33.0.** The "0.33.0 one-release tolerances due at 0.34.0" do not exist; 0.33.0 created none.
47
+
48
+ ## [0.33.1] - 2026-08-22
49
+
50
+ Patch, docs only. No schema change: no entity, edge, property, enum, lifecycle or tool added, removed or altered. One normative contradiction, published in 0.33.0, is resolved.
51
+
52
+ ### Fixed
53
+ - **`UPGBaseNode.key`'s `@remarks` no longer contradicts itself.** The `WHICH PREFIX` paragraph ended *"A product where nothing declares a prefix mints no keys"*, which deletes the inferred rung. Two paragraphs later, in the SAME block, the portfolio-uniqueness note names *"the quiet case where nobody declares a prefix and it is inferred from existing keys"*, and the product-scoped minting rule applies itself *"on the MINT path including a prefix that was INFERRED from existing keys rather than requested by any caller"*. `team.key_prefix` rule 3 presupposes the same rung: a refusal described as covering the inferred case is meaningless if inference never mints. The field settles which sentence is true — the only keyed graph in the estate carries 1,032 keys minted by inference in a product that declares no prefix at all, and under the deleted rung that graph could not exist. The paragraph now states the ladder as built, in four rungs: a create names its prefix explicitly; otherwise the prefixes declared by the product's teams are the candidates; otherwise the prefixes observed on the product's existing keys are; a product with none of the three mints no keys. `team.key_prefix` is untouched and was already correct.
54
+
55
+ ### Notes
56
+ - **Truth is unmoved: 324 entities · 1084 edges · 69 cross-edges · 25 anti-patterns · 98 tools · 25 polymorphic keys in 14 families.** Prose only, so nothing moves a count, a migration, a fixture or an editorial fingerprint. That last one is worth stating rather than assuming: `check:editorial` excludes prose by design, so the most consequential text change a release can make is invisible to the gate that exists to catch documentation going stale.
57
+ - **What this patch deliberately does NOT change.** `team.key_prefix`'s suppression paragraph makes a declared team prefix replace the candidate set outright. Applied to an OBSERVED prefix that no team has claimed, that instruction is a live hazard: in a graph carrying two observed prefixes and no declaration, one team declaring one of them collapses the candidate set to that one and every later create mints under it silently, including work belonging to the other. Narrowing it is a change to what implementations must do rather than a correction to what they were told, so it is not smuggled into a patch. Interim guidance for anyone minting today: declare every prefix a product uses, or declare none.
58
+
10
59
  ## [0.33.0] - 2026-08-22
11
60
 
12
61
  Minor, additive. Zero new entity types, zero new tools, zero new checks. The through-line: 0.32.0 gave the graph the right shapes and left three of them unreachable, and most of this release is reach rather than shape.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  The TypeScript core for the **Unified Product Graph (UPG)**, an open specification that gives product tools a shared vocabulary for entities, edges, and frameworks.
9
9
 
10
- 324 entity types across 37 domains. 1084 typed edges. 53 canonical framework definitions, with more added as each is reviewed.
10
+ 324 entity types across 37 domains. 1086 typed edges. 53 canonical framework definitions, with more added as each is reviewed.
11
11
 
12
12
  ## Install
13
13