@unified-product-graph/core 0.40.0 → 0.41.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 +34 -0
- package/README.md +1 -1
- package/dist/index.cjs +89 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -1
- package/dist/index.d.ts +94 -1
- package/dist/index.js +89 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.41.1] - 2026-09-20
|
|
11
|
+
|
|
12
|
+
**Everything here is a defect an adversarial sweep of the published 0.41.0 found, and four of the five are mine.** The theme is a tool answering a confident number about a question it never actually asked.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Product discovery never entered `.upg/products/`.** The workspace scan walked one directory level, so the folder `create_product` writes into by default was invisible. A workspace whose `workspace.json` carries `default_product` but no `products[]` therefore had `list_local_products` return `[]` and `--check` report 0, while `switch_product` on that very file succeeded: the discovery tool said nothing was there and the loading tool then loaded it. Discovery now recurses inside the workspace directory, still skipping `_`-prefixed folders, which is the distinction 0.41.0's own comment got wrong by generalising from `_archive/` (unregistered on purpose) to `products/` (the conventional home).
|
|
17
|
+
- **`--check` validated with a lenient parser while startup uses the strict loader.** A document with no `$upg` block, a node missing `type`, or a dangling edge all parse clean and all fail to load, so `--check` returned `ok: true` and exit 0 on workspaces the server then refused to start on. That is worse than no preflight, because an install script asserts on it and reports the environment healthy. It now loads through the same path, and a test pins that its verdict matches what starting the server actually does.
|
|
18
|
+
- **An unknown filter is now an error rather than an empty result.** `query({from})`, `query({traverse})`, `list_nodes({type})`, `export_edges({types})` and `migrate_status({entity_type})` each answered a clean zero for a name the spec has never heard of. The reader cannot tell "you have none of these" from "that name does not exist", and the two need opposite next moves. 0.41.0 made it live: an agent traversing the legacy `project_delivers_work_item` after the containment edges landed gets `total_edges: 0` and concludes the project has no work items. Deprecated names stay accepted, since filtering by one is exactly what a cleanup pass does.
|
|
19
|
+
- **`migrate_status` accepted parameters it does not declare.** `{type, from, to}` — the names its own output uses, and the ones `rename_edge_type` takes — were accepted in silence and reported a successful no-op, as were `{}` and `{zzz: 1}`. Auto mode with no arguments is legitimate, so unknown KEYS are refused rather than required ones demanded.
|
|
20
|
+
- **`migrate_status` would write a status `update_node` rejects.** Same type, same value, opposite outcomes, on the path that writes in bulk — so a single call could put every node of a type out of lifecycle. The target is now validated against the type's lifecycle, and the graph is left untouched when it is refused.
|
|
21
|
+
|
|
22
|
+
### Known
|
|
23
|
+
|
|
24
|
+
- **The shadow pair is real, accepted, and not yet detectable by any tool.** 0.41.0 moved the project-to-work-item carrier, so a graph imported both before and after that release can hold `project_contains_task` and `project_delivers_work_item` for one pair. Both are individually legal, so `validate_graph` is silent; `deduplicate_nodes` is nodes-only. Until 0.42.0 adds a drift class for it, you can enumerate your own with `export_edges`: group the rows by `(source, target)` and keep only the groups whose types include BOTH a `project_contains_*` edge AND `project_delivers_work_item`. Do not simply keep every group larger than one — multiple edges between one pair are ordinarily legal, and on our own graphs that broader rule returns 172 hits and no true positives.
|
|
25
|
+
|
|
26
|
+
## [0.41.0] - 2026-09-04
|
|
27
|
+
|
|
28
|
+
**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.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`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.
|
|
33
|
+
- **`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.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`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.
|
|
38
|
+
- **`--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.
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
|
|
42
|
+
- **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.
|
|
43
|
+
|
|
10
44
|
## [0.40.0] - 2026-09-04
|
|
11
45
|
|
|
12
46
|
**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.
|
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.
|
|
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.
|
|
34279
|
+
var UPG_VERSION = "0.41.1";
|
|
34193
34280
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
34194
34281
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
34195
34282
|
var UPG_TYPES = getTypes();
|