@unified-product-graph/core 0.39.0 → 0.41.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,37 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.41.0] - 2026-09-04
11
+
12
+ **A project could reference the work it delivered, but never hold it.** Teams model in Linear, where an Initiative holds Projects and a Project holds Issues. UPG carried the upper relation and not the lower, so a graph living alongside Linear had to explain itself twice. Plus two defects in tooling shipped earlier this week, both found by a field reporter reading output that looked clean.
13
+
14
+ ### Added
15
+
16
+ - **`project` contains work: `project_contains_epic` / `_feature` / `_user_story` / `_task` / `_bug`** (hierarchy, contains/belongs_to), with all five added to `UPG_VALID_CHILDREN.project`. `UPG_EDGE_PAIR_MAP['project:task']` resolves natively, so `parent_id` works and no adapter has to special-case the pair. The set is exactly the one Linear's issue-type map produces. **This does not reverse 0.33.0**, which ruled containment impossible for the polymorphic `project_delivers_work_item` because containment obliges a `UPG_VALID_CHILDREN` pair keyed by concrete type names and a `node` wildcard can never supply them. That argument forecloses containment for the wildcard, not for concrete pairs, and this is the half the wildcard could not reach. Nothing is migrated: `project_delivers_work_item` remains for targets outside the five, and a graph may hold both edges for one pair, in which case containment is the parent axis and wins.
17
+ - **`project` reaches strategy: `project_advances_key_result`, `project_drives_outcome`, `project_assumes_assumption`, `constraint_constrains_project`, `strategic_theme_pursues_project`** (cross-domain), mirroring `initiative`'s forward verbs so a reader moving between the two granularities is never re-learning the vocabulary. The mirror stops short in two places, both correct: the reverse verbs differ where the relationship differs (`strategic_theme_pursues_initiative` reverses to `pursued_under`, membership in a theme's programme; `_pursues_project` to `pursued_by`, because a project is the vehicle doing the pursuing), and the classifications differ because `initiative` sits in `strategy` beside its targets while `project` sits in `program_mgmt`, which is what makes `cross-domain` the measured answer here and same-domain debt there. These five resolve `provisional` rather than `curated` at the portfolio seam, so they are authorable across graphs with a warning; promotion waits on evidence anyone does. `market_segment`, `value_proposition`, `revenue_stream` and `strategic_question` are deliberately NOT mirrored down: a delivery-scoped project does not enter a market, and copying that reach would make the two types synonyms rather than granularities.
18
+
19
+ ### Fixed
20
+
21
+ - **`validate_graph` reported a clean `0` for every drift class the scope did not name.** `scope` gated detection rather than the response, against its own documented promise that counts are always returned for every class. On this project's tracker `undeclared_property_drift` read 219 at scope `all` and 0 at scope `lifecycle_drift`, same graph, same hash. Detection now runs for every class on every call and `scope` governs which entry arrays come back. Two further halves of the same defect go with it: five counters read `array.length` and so also moved with `limit`, and `structurally_valid` gated on those same lengths, so a scoped call could return `true` over a graph with hundreds of violations. A narrowly scoped call now does the same work as `all` and returns less, which is the honest trade: the counts are the part a CI gate reads.
22
+ - **`--check` counted `.upg` filenames rather than products.** A `readdir` of the workspace root: it counted `portfolio.upg`, which carries no `product` header, along with any unregistered scratch graph, and saw nothing in a registered subfolder. A 54-graph workspace answered 2. The count now follows the rule `list_local_products` enumerates by, because an operator asserting on `--check` in an install script and reading the tool in the session must never be shown two different inventories.
23
+
24
+ ### Documentation
25
+
26
+ - **Wildcard endpoints and cross-product scope, stated where a reader looks.** `cross_product_scope` is derived, never declared, and `node` is not a type and so is never `portfolio_shared`. An edge with a wildcard endpoint is therefore `resident` unless its other, concrete endpoint is portfolio-shared or the edge type is curated by name. That was true and enforced, but recorded only as a mechanism note, so a careful reader could read a ruling as an oversight and propose declaring a scope that cannot be declared. Now stated in `grammar/cross-scope.ts` with both routes to widening, and pinned by tests.
27
+
28
+ ## [0.40.0] - 2026-09-04
29
+
30
+ **Two gates could not see the thing they were built to catch.** Everything here came out of the 0.39.0 editorial pass rather than a feature request: a stale number that survived several releases in plain sight, and a citation check that had quietly stopped detecting one of its own founding defects. Tooling only; no catalog, schema, or entity surface moves.
31
+
32
+ ### Fixed
33
+
34
+ - **`check:count-drift` now sees claims a formatter has wrapped.** Every claim pattern was written with literal spaces, so a line break between two words of a phrase defeated the match. A "61 cross-product edge types" claim — wrong by ten — sat in a Prettier-wrapped caption through several releases while the identical claim one line long was caught every time. Each top-level literal space is now compiled to `\s+` at load, leaving escapes and character classes alone, so patterns stay readable and line numbers stay accurate. Wrapped Markdown had the same exposure.
35
+ - **`check:editorial` can catch its founding defect again.** The edge-shape test drew its type names from the 324 live types and none of the 38 deprecated ones, at *both* ends of the test — so a citation orphaned by a type rename, which names the old (now deprecated) type by construction, was structurally invisible. `task_implements_story_statement`, one of the three fabrications this check was built for, had stopped being detected when the 0.34.0 tail condition landed, because `story_statement` was deprecated at 0.7.0. Two further miscitations were live in published docs and were found by reading rather than by gate. The deprecated names are now in the list; the false-positive fences the tail condition bought are unchanged and pinned by the selftest.
36
+
37
+ ### Changed
38
+
39
+ - **`batch_create_nodes` echoes what the caller wrote.** Successful responses now carry each created node's properties (omitted when empty) and, when refs were used, the alias `ref_map`. Both existed only as re-reads before: keying 163 tokens by their CSS variable meant re-reading the file, and `ref_map` was reported on dry-run and on failure but not on success — the call described its ref resolutions when it refused and when it pretended, and went quiet when it did the work.
40
+
10
41
  ## [0.39.0] - 2026-09-04
11
42
 
12
43
  **A write said "done" while the disk disagreed, and a design system could not point at itself.** Six findings from a second Sanity field brief, written while populating a Studio design layer through roughly two thousand MCP calls across seven graph PRs. One is a correctness bug in the workflow this project's own README recommends; the rest are the design layer's missing vocabulary. Additive throughout.
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. 1094 typed edges. 53 canonical framework definitions, with more added as each is reviewed.
10
+ 324 entity types across 37 domains. 1104 typed edges. 53 canonical framework definitions, with more added as each is reviewed.
11
11
 
12
12
  ## Install
13
13
 
package/dist/index.cjs CHANGED
@@ -3030,6 +3030,93 @@ var UPG_EDGE_CATALOG = {
3030
3030
  // arbitrary work. deliberate_only because project membership is authored,
3031
3031
  // never inferred from co-occurrence.
3032
3032
  project_delivers_work_item: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "semantic", source_type: "project", target_type: "node", deliberate_only: true },
3033
+ // ── project as a first-class home for work (0.41.0, Captain-ratified) ──────
3034
+ //
3035
+ // Teams model in Linear, where an Initiative holds Projects and a Project
3036
+ // holds Issues; UPG must express that natively or every sync leaves a reader
3037
+ // guessing which node is which. `project_implements_initiative` already
3038
+ // carries the upper relation (read from the project's side). These five carry
3039
+ // the lower one, and the set is exactly what the Linear issue-type map
3040
+ // produces: an issue becomes a feature, bug, task, user_story or epic.
3041
+ //
3042
+ // WHY THESE ARE CONTAINMENT AND `project_delivers_work_item` IS NOT.
3043
+ // 0.33.0 widened `project_delivers_epic` into the polymorphic
3044
+ // `project_delivers_work_item` and ruled, correctly, that it could not be a
3045
+ // `contains` verb: containment obliges a `UPG_VALID_CHILDREN` pair keyed by
3046
+ // CONCRETE type names, which a wildcard endpoint can never supply. That
3047
+ // argument forecloses containment for the wildcard; it does not foreclose
3048
+ // containment for concrete pairs, which is what these are. So this is not a
3049
+ // reversal of that ruling — it is the half the wildcard could not reach.
3050
+ //
3051
+ // A KNOWN SHADOW PAIR, ACCEPTED DELIBERATELY (Captain, 2026-09-04). Both an
3052
+ // authored `project_contains_task` and a `project_delivers_work_item` can
3053
+ // name the same pair. That duplication is real and is taken on purpose, to
3054
+ // avoid a migration on live `project_delivers_work_item` instances. The
3055
+ // precedence is stated so it is never ambiguous: CONTAINMENT IS THE PARENT
3056
+ // AXIS AND WINS; the polymorphic edge stays the reference for targets outside
3057
+ // the concrete set. If a real graph is ever observed holding both for one
3058
+ // pair, that is the signal to retire the polymorphic edge for these five
3059
+ // types and migrate — the option deliberately not taken here.
3060
+ project_contains_epic: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "project", target_type: "epic" },
3061
+ project_contains_feature: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "project", target_type: "feature" },
3062
+ project_contains_user_story: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "project", target_type: "user_story" },
3063
+ project_contains_task: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "project", target_type: "task" },
3064
+ project_contains_bug: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "project", target_type: "bug" },
3065
+ // ── project's strategic reach (0.41.0) ────────────────────────────────────
3066
+ //
3067
+ // The parity half. `initiative` and `project` are two granularities of the
3068
+ // same idea, and teams pick one: Linear-shaped teams put the dated, owned,
3069
+ // key-result-linked bet in a PROJECT, while the standard's `initiative`
3070
+ // carried all the strategic reach and `project` reached milestone,
3071
+ // deliverable and a wildcard. A team choosing the honest name for its
3072
+ // granularity should not lose its connection to strategy.
3073
+ //
3074
+ // Mirrors of initiative's edges on the FORWARD verbs, so a reader moving
3075
+ // between the two granularities is never re-learning the vocabulary. The
3076
+ // mirror is deliberately not total, and the two places it stops are both
3077
+ // correct rather than sloppy:
3078
+ //
3079
+ // REVERSE VERBS. `strategic_theme_pursues_initiative` reverses to
3080
+ // `pursued_under`, which reads as membership in a theme's programme.
3081
+ // `strategic_theme_pursues_project` reverses to `pursued_by`, because a
3082
+ // project is the vehicle doing the pursuing, not a subdivision of the
3083
+ // theme. Same forward verb, different relationship on the way back.
3084
+ //
3085
+ // CLASSIFICATION. Initiative's five are causal / cross-domain / hierarchy
3086
+ // / semantic; all five of these are `cross-domain`. That is domain
3087
+ // arithmetic, not inconsistency: `initiative` sits in `strategy` alongside
3088
+ // its targets, so a `cross-domain` classification there would be T1.7
3089
+ // same-domain debt, while `project` is `program_mgmt` and every target
3090
+ // here is `strategy`, so `cross-domain` is the measured truth and adds
3091
+ // nothing to that debt.
3092
+ //
3093
+ // CROSS-PRODUCT SCOPE, ruled deliberately (2026-09-04, raised by Troi during
3094
+ // the 0.41.0 editorial pass). Three of initiative's carry
3095
+ // `cross_product_eligible: true` and none of these do, which looks at first
3096
+ // like reinstating at the portfolio seam the reach this change removes
3097
+ // elsewhere. It is not: `key_result`, `outcome` and `strategic_theme` are
3098
+ // all `portfolio_shared`, so these edges already resolve `provisional` and
3099
+ // ARE authorable across graphs today, with a warning, not rejected. The
3100
+ // difference is curated (hard-allow, silent) versus provisional (allowed,
3101
+ // warned). Left provisional on purpose: the curated set is a ratified
3102
+ // snapshot that grows on measured need, and no field evidence yet shows
3103
+ // anyone authoring these across graphs. Promote to curated when it appears,
3104
+ // which is a one-flag change per edge.
3105
+ //
3106
+ // Deliberately NOT mirrored: `initiative_enters_market_segment`,
3107
+ // `_realises_value_proposition`, `_unlocks_revenue_stream` and
3108
+ // `_raises_strategic_question`. A delivery-scoped project does not enter a
3109
+ // market or unlock a revenue stream; that reach belongs to the coarser level,
3110
+ // and copying it down would make the two types synonyms rather than
3111
+ // granularities. `capability` needs no edge either: a project reaches it
3112
+ // through the features it now contains (`capability_implemented_by_feature`),
3113
+ // and `decision` already reaches a project through the polymorphic
3114
+ // `decision_influences_node`.
3115
+ project_advances_key_result: { forward_verb: "advances", reverse_verb: "advanced_by", classification: "cross-domain", source_type: "project", target_type: "key_result" },
3116
+ project_drives_outcome: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "project", target_type: "outcome" },
3117
+ project_assumes_assumption: { forward_verb: "assumes", reverse_verb: "assumed_by", classification: "cross-domain", source_type: "project", target_type: "assumption" },
3118
+ constraint_constrains_project: { forward_verb: "constrains", reverse_verb: "constrained_by", classification: "cross-domain", source_type: "constraint", target_type: "project" },
3119
+ strategic_theme_pursues_project: { forward_verb: "pursues", reverse_verb: "pursued_by", classification: "cross-domain", source_type: "strategic_theme", target_type: "project" },
3033
3120
  milestone_gates_release: { forward_verb: "gates", reverse_verb: "gated_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
3034
3121
  milestone_triggers_release: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
3035
3122
  deliverable_ships_feature: { forward_verb: "ships", reverse_verb: "shipped_by", classification: "cross-domain", source_type: "deliverable", target_type: "feature" },
@@ -8022,7 +8109,7 @@ var UPG_VALID_CHILDREN = {
8022
8109
  subscription: ["invoice"],
8023
8110
  // ── Program Management hierarchy ─────────────────────────────────────────────
8024
8111
  program: ["project", "risk_register", "change_request", "resource_allocation", "status_report"],
8025
- project: ["milestone", "deliverable"],
8112
+ project: ["milestone", "deliverable", "epic", "feature", "user_story", "task", "bug"],
8026
8113
  risk_register: ["risk"],
8027
8114
  // ── Accessibility hierarchy ──────────────────────────────────────────────────
8028
8115
  a11y_standard: ["a11y_guideline", "a11y_audit", "a11y_annotation"],
@@ -34189,7 +34276,7 @@ function checkHeaderSealText(text) {
34189
34276
  }
34190
34277
 
34191
34278
  // src/index.ts
34192
- var UPG_VERSION = "0.39.0";
34279
+ var UPG_VERSION = "0.41.0";
34193
34280
  var UPG_FORMAT_VERSION = "0.4.0";
34194
34281
  var MARKDOWN_FORMAT_VERSION = "0.1";
34195
34282
  var UPG_TYPES = getTypes();