@unified-product-graph/core 0.32.1 → 0.33.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 +62 -0
- package/README.md +1 -1
- package/dist/index.cjs +218 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +591 -71
- package/dist/index.d.ts +591 -71
- package/dist/index.js +214 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,68 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.33.0] - 2026-08-22
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **`project_delivers_work_item`**, widening and renaming `project_delivers_epic` to the polymorphic work-item endpoint (`semantic`, `deliberate_only`). A tracker dry-run carried 651 project memberships as a vendor property and emitted zero edges, because the default issue type an adapter produces is `task` and the only project edge could not reach it. The verb stays `delivers` and NOT `contains`: a containment verb obliges a `UPG_VALID_CHILDREN` pair, and that map is keyed by concrete type names, so a wildcard endpoint can never discharge the obligation its own verb creates. `UPG_VALID_CHILDREN.project` is untouched, deliberately. A work item is contained once, by its epic or feature, and referenced many times. One rename migration; measured population one, in the CI fixture.
|
|
16
|
+
- **`external_links?: UPGExternalLink[]` on `UPGBaseNode`**, generalising the `ServiceProperties.links` shape. `external_ref` still names THE canonical artifact; this holds everything else. 162 measured links were overflowing into an undeclared vendor bag key. Named `external_links` rather than `external_refs` because the latter is one character from `external_ref` and means the opposite thing.
|
|
17
|
+
- **`created_at` and `updated_at` on `UPGBaseNode`, tagged `@volatile`.** Store metadata, declared so a view query can open a window over them. Two of the six date dimensions a real board filters on are these, and leaving them undeclared would ship a query language that cannot say what the surface most often says.
|
|
18
|
+
- **`clauses?: UPGViewClause[]` on `UPGViewQuery`**, with `UPGViewDimension`, `UPGTimeWindow` and `UPGViewEdgeClause`. One clause list carrying per-clause negation, declared time windows and edge conditions, none of which the named fields can hold. The named fields stay as the positive-only shorthand and are NOT deprecated; the precedence rule is stated: a reader that finds `clauses` uses it and ignores the named fields. A window is evaluated at read time, never frozen at save, so a saved view that says "this quarter" means this quarter to whoever opens it. The team cadence is deliberately NOT a window: it resolves through the active-cycle designation as an edge clause, because field data holds 19 cycles of which every one is dateless and exactly one is `active`.
|
|
19
|
+
- **The D/E addendum (ratified 2026-08-22), three arms on the shapes this release mints.** **`target_designation?: 'viewer'`** on `UPGViewEdgeClause`: the person axis' parallel to the cadence axis' `target_status: ['active']`, naming the reader by role and resolving in their session. A saved view meaning "assigned to me" cannot store an id, because the id it stores is one particular person and the view is then permanently about a colleague; a shipped surface reached for the sentinel `'@me'` inside `target_ids`, which round-trips and whose meaning does not travel. A sentinel in an id field is a private protocol wearing a public shape. **`from_focus.depth` gains `'unbounded'`**: a relative selection over a tree is transitive by nature and has no correct finite depth, so a surface with only a number picks a big one, and a shipped one picked 64, which is indistinguishable from a caller who meant 64 and silently truncates the first deeper graph. **`layout` gains `'tree'` and an advisory `nest_by?: string[]`**: `group_by` partitions a flat set on a value and cannot express nesting, because a tree's levels are edges rather than property values.
|
|
20
|
+
- **`UPGQueryDrivenLayer`**, extended by both `composition` and `workspace`. A layer is query-driven while it is being worked on, not only after it is published; declaring the query only on the published half made the fact something invented at publish time.
|
|
21
|
+
- **`person_member_of_team`** and **`person_reports_to_person`**, both `semantic`, both sourced on `person`. The org half of the ontology had 17 edges reaching `team` and no path from a person to any of them, and `person_reports_to_person` was cited three times in spec prose while existing zero times, once in an anti-pattern entry that instructed users toward an edge the catalog would refuse. Neither is a reversal of the containment-free stance on `person`, which forecloses HIERARCHY edges added to satisfy a hierarchy audit; both are reference edges, and sourcing on `person` makes the containment reading unavailable rather than merely unchosen. Membership is unqualified: `role` is already an entity type, and a role string on the edge would be a second role model beside it.
|
|
22
|
+
- **`team.key_prefix`.** The prefix names a team, which `product.key_prefix`' own summary already said, and a single string could never express a product with two of them.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **`agent_skill` and `agent_hook` move from `WORK_ITEM` to `OPERATIONAL`.** A durable capability and an event trigger were both reaching `in_review` and `done`, and zero of ten field instances had ever left the initial phase. Their nearest siblings `monitor` and `alert_rule` already run OPERATIONAL. Neither becomes lifecycle-free: the `user_story` precedent for that needs a paired lifecycle-bearing entity and there is none. A status migration remaps the WORK_ITEM vocabulary; measured population four, all `todo`.
|
|
26
|
+
- **`specification`'s type summary** is corrected. See Fixed.
|
|
27
|
+
- **`department`'s type summary** records the division ruling: division, org unit and business unit are alternative LABELS for this type, not separate types. That ruling has been right and one line deep in a label array since 0.1.0, and it had already produced two phantom entity types in a briefing. The flat-department constraint is stated with it.
|
|
28
|
+
- **`UPGBaseNode.properties`** documents the namespaced-key rule, extended from the canvas bag to the node bag: a key a tool owns and the spec does not declare is written `<tool>:<key>`. An underscore key is indistinguishable from a misspelled spec property, which no migration can target and no validator can flag. One measured undeclared extension, raw tracker state history on 1,032 nodes, is recorded as DELIBERATELY undeclared with its pull-forward condition, because silence and a decision look identical six months later.
|
|
29
|
+
- **`NODE_KEY_ORDER` is exported**, with a test asserting every base field appears in it. A subset assertion, never equality: two tolerated non-base keys belong there and an equality check would push the next author to delete them.
|
|
30
|
+
- **MCP `create_node` / `batch_create_nodes` / `update_node` / `batch_update_nodes`** accept `archived` and `archived_at`; the create pair also accepts `key`. This mirrors `graph-service`'s existing policy exactly rather than inventing one. Widening the JSON schema alone does nothing, which is the trap this item exists to name: the SDK hand-builds its patch from its own arg interfaces, so an argument arriving without a matching field is never copied. All three layers moved.
|
|
31
|
+
- **`get_import_recipe` serves per-source read caveats** on both the curated and the scaffold path, keyed independently of whether a curated table exists. Linear has no curated recipe and two of the sharpest hazards in the corpus: `list_issues` truncates descriptions mid-markup on 913 of 1,032 issues, and an over-complex query returns HTTP 200 with an error body. Both are one class, a failure that looks like a success.
|
|
32
|
+
|
|
33
|
+
### Deprecated
|
|
34
|
+
- **`objective.timeframe`** (`removeIn` 1.0.0), pointing at `objective_scoped_to_planning_cycle`, which has existed since 0.20.0. **`strategic_theme.time_horizon`** gains the same `removeIn`; it was tagged at 0.20.0 with no removal date and survived twelve minors, which is what an undated deprecation buys. Planning entities relate to time through the scheduling edge and free-text time fields retire, and the class has a stated BOUNDARY so the next author applies a test rather than a slogan: the class is the entities that can be scheduled in a planning cycle, which is these two. `strategic_pillar.time_horizon`, `vision.timeframe`, `roadmap.timeframe`, `roadmap_item.quarter` and `journey_phase.timeframe` are named exemptions. No `drop_props`: dropping the string before the cycle exists destroys the only record of the intent.
|
|
35
|
+
- **`product.key_prefix`** (`removeIn` 1.0.0), superseded by `team.key_prefix`. They are not a default and an override, which describe one fact at two scopes; they describe different facts, and the product field is correct only when the product has exactly one team. Retained as the single-team unasked default and ignored whenever any team declares a prefix, because removal now would strand every graph that has a product prefix and no `team` nodes.
|
|
36
|
+
- **`agent_hook.hook_status`** (`removeIn` 1.0.0), a `*_status` shadow of the base `status` and the class Pattern D collapsed fourteen times at 0.15.0. `error` maps to `paused`: OPERATIONAL has no failure phase, and runtime health is store state rather than a fact about the thing.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- **`specification`'s summary no longer implies a gap that does not exist, and the three edge names an editorial sweep called fabricated are not fabricated.** `UPG_CROSS_ONLY_EDGE_TYPES` has carried `product_implements_specification`, `product_exposes_specification` and `feature_conforms_to_specification` since 0.9.12: product-to-foundation links are portfolio cross-edges by design, because a governed specification is a registry canonical. A citation check that reads only the edge catalog reports every legitimate cross-only edge as a phantom. The gap that DOES remain is stated with its pull-forward condition: a `specification` node inside a single product graph, with no portfolio around it, has no conformance edge available.
|
|
40
|
+
- The workspace archive read tolerance from 0.32.0 retires on schedule; the writer had already flipped, so it was carrying nothing still produced.
|
|
41
|
+
- An orphaned doc comment on `AgentSkillProperties` that documented no field.
|
|
42
|
+
|
|
43
|
+
### Notes
|
|
44
|
+
- Truth: **324 entities · 1084 edges · 69 cross-edges · 25 anti-patterns · 98 tools · 25 polymorphic keys in 14 families**. The lifecycle-phase count that earlier entries carry is deliberately NOT restated here: it is the one figure in that line no check asserts, and it does not reproduce. Counted off the built catalog it is 869 phase rows across 193 lifecycles, or 342 distinct (template, phase) pairs; neither is the 334 carried since 0.32.0, which traces to a planning document rather than to a constant. A number nobody can derive should not ship in a release note, so it is dropped rather than repeated. Wiring a phase count into `check:count-drift` is banked with its condition already met; it is not done in this release because adding a check would change the shape of a release ratified as adding none. No entity minted; four edges added net of one rename; cross-edges unchanged because `product`, `feature` and `person` are not `portfolio_shared`, so nothing here can be eligible.
|
|
45
|
+
- **Portfolio-wide key uniqueness is now DEFINED AND UNENFORCED**, stated on `UPGBaseNode.key`. Within a portfolio one `(prefix, number)` should identify one node; uniqueness is enforced per product, so two products minting under one prefix produce the same citation for two different things. This is measured, not feared: a fixture reproduces the collision through the ordinary create path, including the quiet case where nobody declares a prefix and it is inferred. No per-product check can ever see it, because each product reports its own key as valid. The standing audit is deferred with a dated condition: the first portfolio holding two keyed products, which is also the first moment a real near-miss corpus can be sampled instead of invented.
|
|
46
|
+
- The key-prefix PRECEDENCE is ruled in the spec rather than left to an application ladder: a declared team prefix supersedes the product prefix, and a declared prefix is a candidate before it is observed. Leaving precedence to the ladder would make migration ORDER decide behaviour, per graph, silently.
|
|
47
|
+
- The MCP server may not MINT a key, only accept one. Deriving `max(existing)+1` needs paged reads, and a second unpaged minter is a duplicate-key generator.
|
|
48
|
+
- Banked with conditions: a `rename_bag_key` migration kind; generic patch derivation from `UPG_BASE_NODE_FIELDS` in the SDK; re-classifying `product_conforms_to_a11y_standard` from `hierarchy` to `semantic`; wiring a phase count into `check:count-drift`; `person_holds_role`; the within-graph conformance edge.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [0.32.2] - 2026-08-21
|
|
53
|
+
|
|
54
|
+
Patch. No schema change: no entity, edge, property, enum, or lifecycle added or removed. The 0.32.0 base fields become reachable through the write path that was supposed to carry them.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- **`UPGFileStore.updateNode` round-trips every `UPGBaseNode` field.** It merged a hand-maintained list of seven names and returned the unchanged node as a SUCCESS for anything outside it, so the three fields 0.32.0 added to the shape (`key`, `archived`, `archived_at`) were accepted and discarded. A caller could not tell a lost write from a landed one; the key-minting contention loop in the local graph-service adapter hit exactly this and spent its whole retry budget re-minting a key that could never move. The mergeable set is now DERIVED from the shape, so a field added to `UPGBaseNode` round-trips with no further edit.
|
|
58
|
+
- **An undeclared top-level field is now REFUSED, not dropped.** `updateNode` throws the new `UnknownNodeFieldError` naming the offending keys, and refuses atomically so the valid half of a patch cannot land beside a silently discarded half. Changing `id` throws `ImmutableNodeFieldError`; restating the current `id` stays legal, so spreading a whole node into a patch keeps working. Type-level callers cannot reach either path, since `Partial<UPGBaseNode>` rejects an undeclared key at compile time; the guards fire on runtime-shaped input such as a parsed argument bag.
|
|
59
|
+
- **Drift detection no longer reports a minted key as non-spec.** The canonical top-level field sets behind `top_level_drift` (the load-time counter in `@unified-product-graph/sdk` and the per-node breakdown in `validate_graph`) were two more hand-maintained copies of the same list, and 0.32.0 updated neither, so every node carrying `key`, `archived`, or `archived_at` was counted as holding non-spec top-level fields. Both derive from the shape now.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- **`UPG_BASE_NODE_FIELDS`, `UPG_BASE_NODE_FIELD_SET`, `UPG_BASE_NODE_SPECIAL_MERGE_FIELDS`** on `@unified-product-graph/core`: the `UPGBaseNode` key set as runtime data, and the subset a generic shallow merge must not touch. A TypeScript interface is erased at runtime, which is why four consumers each kept their own copy behind a comment asking the next editor to remember. The list is locked to the interface by a `Record<keyof UPGBaseNode, true>` that is exhaustive in both directions, so adding a field to the shape without extending the list is now a COMPILE error rather than a silent divergence discovered three releases later.
|
|
63
|
+
- `UnknownNodeFieldError` and `ImmutableNodeFieldError` are exported from `@unified-product-graph/sdk`.
|
|
64
|
+
|
|
65
|
+
### Notes
|
|
66
|
+
- Truth unchanged: **324 entities · 1082 edges · 69 cross-edges · 25 anti-patterns · 98 tools · 334 phases**. Zero new tools, zero new checks.
|
|
67
|
+
- Release process: `scripts/upg-release.sh` gains a step that regenerates the five `apps/entopo` spec mirrors from the freshly built train and then re-runs their `--check` forms. Those mirrors sit outside the release train's build filters, so no release ever ran their generators or their gates, and they have gone stale three times; 0.32.1 shipped with the snapshot still reading 0.32.0. The failure resists inspection because the drift test reads a checkout's `dist/`, and against a stale `dist` the comparison is stale-against-stale and reports green.
|
|
68
|
+
- The MCP `update_node` and `batch_update_nodes` tools still do not accept `key`, `archived`, or `archived_at` as arguments, so those fields remain unreachable from the agent surface. Widening a tool's argument schema is additive surface rather than a fix and is deliberately NOT in this patch.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
10
72
|
## [0.32.1] - 2026-08-21
|
|
11
73
|
|
|
12
74
|
Patch. No schema change: no entity, edge, property, enum, or lifecycle added or removed.
|
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. 1084 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
|
@@ -51,6 +51,7 @@ __export(index_exports, {
|
|
|
51
51
|
LEGACY_PRODUCT_STAGES: () => LEGACY_PRODUCT_STAGES,
|
|
52
52
|
MARKDOWN_FORMAT_VERSION: () => MARKDOWN_FORMAT_VERSION,
|
|
53
53
|
MARKET_COMPETITIVE_PLAYBOOK: () => MARKET_COMPETITIVE_PLAYBOOK,
|
|
54
|
+
NODE_KEY_ORDER: () => NODE_KEY_ORDER,
|
|
54
55
|
OPERATIONS_QUALITY_PLAYBOOK: () => OPERATIONS_QUALITY_PLAYBOOK,
|
|
55
56
|
PRESENT_UNDER_PROPERTY: () => PRESENT_UNDER_PROPERTY,
|
|
56
57
|
PRODUCT_DELIVERY_PLAYBOOK: () => PRODUCT_DELIVERY_PLAYBOOK,
|
|
@@ -70,6 +71,9 @@ __export(index_exports, {
|
|
|
70
71
|
UPG_APPROACHES_BY_ID: () => UPG_APPROACHES_BY_ID,
|
|
71
72
|
UPG_AREA_TAXONOMY: () => UPG_AREA_TAXONOMY,
|
|
72
73
|
UPG_AREA_TAXONOMY_BY_COVERAGE_KEY: () => UPG_AREA_TAXONOMY_BY_COVERAGE_KEY,
|
|
74
|
+
UPG_BASE_NODE_FIELDS: () => UPG_BASE_NODE_FIELDS,
|
|
75
|
+
UPG_BASE_NODE_FIELD_SET: () => UPG_BASE_NODE_FIELD_SET,
|
|
76
|
+
UPG_BASE_NODE_SPECIAL_MERGE_FIELDS: () => UPG_BASE_NODE_SPECIAL_MERGE_FIELDS,
|
|
73
77
|
UPG_CANONICAL_FORMAT_VERSION: () => UPG_CANONICAL_FORMAT_VERSION,
|
|
74
78
|
UPG_CONTAINMENT_FREE_TYPES: () => UPG_CONTAINMENT_FREE_TYPES,
|
|
75
79
|
UPG_COUNT_BENCHMARKS: () => UPG_COUNT_BENCHMARKS,
|
|
@@ -2723,6 +2727,53 @@ var UPG_EDGE_CATALOG = {
|
|
|
2723
2727
|
// contained by product / department / team. Hierarchy edges for person
|
|
2724
2728
|
// are deliberately omitted; adding them only to satisfy a hierarchy
|
|
2725
2729
|
// audit would invert the orthogonality the type was introduced to express.
|
|
2730
|
+
// ── Org membership and reporting (0.33.0) ───────────────────────────────────
|
|
2731
|
+
// Both edges are `semantic` REFERENCE edges and neither is a reversal of
|
|
2732
|
+
// the containment-free stance recorded immediately above. That note forecloses
|
|
2733
|
+
// HIERARCHY edges added ONLY to satisfy a hierarchy audit; these are neither.
|
|
2734
|
+
// UPG_CONTAINMENT_FREE_TYPES, UPG_VALID_CHILDREN, get_tree and G2b are all
|
|
2735
|
+
// untouched, and node_assigned_to_person was minted cross-domain onto `person`
|
|
2736
|
+
// at 0.32.0 on the same reading.
|
|
2737
|
+
//
|
|
2738
|
+
// DIRECTION IS LOAD-BEARING, not a preference. get_tree reads SOURCE as parent
|
|
2739
|
+
// (see the team_contains_team note above), so a team -> person edge would sit
|
|
2740
|
+
// one classification change away from putting people in the org tree. Sourcing
|
|
2741
|
+
// on `person` makes the containment reading unavailable rather than merely
|
|
2742
|
+
// unchosen. `team_includes_person` classified hierarchy is the shape to refuse.
|
|
2743
|
+
//
|
|
2744
|
+
// CLASSIFICATION IS `semantic` AND NOT `cross-domain`, which the 0.33.0 design
|
|
2745
|
+
// recommended before the guardrail was consulted. `person` and `team` both live
|
|
2746
|
+
// in the team_org domain, so a `cross-domain` label would contradict itself: the
|
|
2747
|
+
// edge crosses no domain boundary, and T1.7 forbids NEW same-domain cross-domain
|
|
2748
|
+
// edges precisely so that debt stops growing. Nothing the design argument
|
|
2749
|
+
// depends on moves: `semantic` enters neither UPG_VALID_CHILDREN nor get_tree,
|
|
2750
|
+
// and G2b is not engaged.
|
|
2751
|
+
// Membership is UNQUALIFIED: no role property on the edge. `role` is already an
|
|
2752
|
+
// entity type with team_staffed_with_role, so a role string here would be a
|
|
2753
|
+
// second role model beside a node type. The consequence is stated rather than
|
|
2754
|
+
// hidden: the graph can say a team has a slot and that a person is on the team,
|
|
2755
|
+
// and not that the person fills the slot. `person_holds_role` is banked, with
|
|
2756
|
+
// its condition the first field graph carrying both where the join is asked for.
|
|
2757
|
+
//
|
|
2758
|
+
// Neither is cross_product_eligible: `person` is not portfolio_shared, so the
|
|
2759
|
+
// 0.18.0 both-endpoints gate rejects the flag regardless of any team-scoping
|
|
2760
|
+
// ruling.
|
|
2761
|
+
person_member_of_team: { forward_verb: "member_of", reverse_verb: "has_member", classification: "semantic", source_type: "person", target_type: "team" },
|
|
2762
|
+
// Cited three times in spec source since 0.17.2 and existing zero times: the
|
|
2763
|
+
// team_contains_team note above, the team-nesting domain guide, and an
|
|
2764
|
+
// anti_patterns entry that instructs users to model a cross-department
|
|
2765
|
+
// reporting line with this edge. An anti-pattern guide is instructional by
|
|
2766
|
+
// construction, so that third site was telling users to use an edge the catalog
|
|
2767
|
+
// would refuse. Minting makes all three true and needs no prose edit.
|
|
2768
|
+
//
|
|
2769
|
+
// semantic, not hierarchy, and the temptation is real: a reporting line IS a
|
|
2770
|
+
// hierarchy in ordinary English, which is exactly why classifying it that way
|
|
2771
|
+
// would put `person` into containment and become the reversal the sibling edge
|
|
2772
|
+
// above is shaped to avoid. The org chart is a reference structure over
|
|
2773
|
+
// containment-free people. Same classification as the membership edge above, so
|
|
2774
|
+
// the person / team / department triangle is decided once rather than three
|
|
2775
|
+
// times.
|
|
2776
|
+
person_reports_to_person: { forward_verb: "reports_to", reverse_verb: "has_report", classification: "semantic", source_type: "person", target_type: "person" },
|
|
2726
2777
|
team_staffed_with_role: { forward_verb: "staffed_with", reverse_verb: "staffed_in", classification: "hierarchy", source_type: "team", target_type: "role" },
|
|
2727
2778
|
team_targets_team_okr: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "team", target_type: "team_okr" },
|
|
2728
2779
|
team_reflects_in_retrospective: { forward_verb: "reflects_in", reverse_verb: "reflects", classification: "hierarchy", source_type: "team", target_type: "retrospective" },
|
|
@@ -2767,7 +2818,31 @@ var UPG_EDGE_CATALOG = {
|
|
|
2767
2818
|
program_reported_via_status_report: { forward_verb: "reported_via", reverse_verb: "reports", classification: "hierarchy", source_type: "program", target_type: "status_report" },
|
|
2768
2819
|
program_contains_epic: { forward_verb: "contains", reverse_verb: "contained_in", classification: "cross-domain", source_type: "program", target_type: "epic" },
|
|
2769
2820
|
project_implements_initiative: { forward_verb: "implements", reverse_verb: "implemented_by", classification: "cross-domain", source_type: "project", target_type: "initiative" },
|
|
2770
|
-
|
|
2821
|
+
// WIDENED AT 0.33.0, renamed from project_delivers_epic. The epic-only
|
|
2822
|
+
// endpoint could not hold a real tracker import: a Linear dry-run carried 651
|
|
2823
|
+
// project memberships as `properties.linear_project_id` and emitted zero
|
|
2824
|
+
// edges, because the default issue type an adapter produces is `task` and the
|
|
2825
|
+
// only project edge could not reach it. Endpoint-polymorphic over the
|
|
2826
|
+
// work-item set {feature, epic, user_story, task, bug} by the same
|
|
2827
|
+
// construction as planning_cycle_schedules_work_item: the `work_item` token
|
|
2828
|
+
// names the intended semantic domain, the endpoint is the `node` wildcard.
|
|
2829
|
+
//
|
|
2830
|
+
// NOT a `contains` verb, and UPG_VALID_CHILDREN.project is deliberately
|
|
2831
|
+
// untouched. `contains` is this catalog's containment verb and every edge
|
|
2832
|
+
// using it is classification: 'hierarchy', which under G2b obliges a matching
|
|
2833
|
+
// UPG_VALID_CHILDREN pair. That map is Record<parent, child[]> of concrete
|
|
2834
|
+
// type names, so a polymorphic endpoint can never discharge the obligation
|
|
2835
|
+
// its own verb would create. A work item is CONTAINED once (by its epic or
|
|
2836
|
+
// feature) and REFERENCED many times; the parent axis is containment and the
|
|
2837
|
+
// project axis is a reference. Giving project a second containment claim over
|
|
2838
|
+
// the same task is the two-trees problem.
|
|
2839
|
+
//
|
|
2840
|
+
// classification moves cross-domain -> semantic with the widening: pinned to
|
|
2841
|
+
// project -> epic it was a cross-domain pair, and once the endpoint is the
|
|
2842
|
+
// wildcard it is a reference relation between a planning container and
|
|
2843
|
+
// arbitrary work. deliberate_only because project membership is authored,
|
|
2844
|
+
// never inferred from co-occurrence.
|
|
2845
|
+
project_delivers_work_item: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "semantic", source_type: "project", target_type: "node", deliberate_only: true },
|
|
2771
2846
|
milestone_gates_release: { forward_verb: "gates", reverse_verb: "gated_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
|
|
2772
2847
|
milestone_triggers_release: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
|
|
2773
2848
|
deliverable_ships_feature: { forward_verb: "ships", reverse_verb: "shipped_by", classification: "cross-domain", source_type: "deliverable", target_type: "feature" },
|
|
@@ -3833,6 +3908,24 @@ var UPG_EDGE_CATALOG = {
|
|
|
3833
3908
|
// so the lifecycle is governed rather than a standalone root (0.12.1 refinement).
|
|
3834
3909
|
operating_lifecycle_defined_by_specification: { forward_verb: "defined_by", reverse_verb: "defines", classification: "semantic", source_type: "operating_lifecycle", target_type: "specification" },
|
|
3835
3910
|
operating_lifecycle_contains_operating_stage: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "operating_lifecycle", target_type: "operating_stage" },
|
|
3911
|
+
// Conformance (0.33.0, RE-OPENED). 0.33.0 planned to mint
|
|
3912
|
+
// product_conforms_to_specification and feature_conforms_to_specification here.
|
|
3913
|
+
// It does not, and the reason is a fact the design missed: the product-to-
|
|
3914
|
+
// foundation links are NOT absent, they live one tier up. See
|
|
3915
|
+
// UPG_CROSS_ONLY_EDGE_TYPES in shapes/document.ts, which has carried
|
|
3916
|
+
// `product_implements_specification`, `product_exposes_specification` and
|
|
3917
|
+
// `feature_conforms_to_specification` as portfolio-native cross-product edges
|
|
3918
|
+
// since 0.9.12, exactly as the section comment above says. The tiers are
|
|
3919
|
+
// disjoint by construction, so minting the same key here is a hard collision,
|
|
3920
|
+
// and minting only the product half would split one question across two tiers
|
|
3921
|
+
// under two near-synonymous names.
|
|
3922
|
+
//
|
|
3923
|
+
// What survives is the real gap, stated here rather than closed: a
|
|
3924
|
+
// `specification` node held INSIDE a single product graph, with no portfolio,
|
|
3925
|
+
// has no conformance edge, because the cross-only tier needs a registry target.
|
|
3926
|
+
// Whether that case is closed by widening the tier or by a catalog edge under a
|
|
3927
|
+
// non-colliding key is a design question and is banked, with its condition the
|
|
3928
|
+
// first single-product graph that models a specification it conforms to.
|
|
3836
3929
|
// Stewardship: a registry specification is governed by a (registry-hostable) organization (0.9.13).
|
|
3837
3930
|
specification_governed_by_organization: { forward_verb: "governed_by", reverse_verb: "governs", classification: "semantic", source_type: "specification", target_type: "organization" },
|
|
3838
3931
|
// Operating-lifecycle cross-edges (0.11.6): a product's journey phase realises a canonical
|
|
@@ -4047,7 +4140,11 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
|
|
|
4047
4140
|
// endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
|
|
4048
4141
|
// bug}. The `work_item` key token names the intended semantic domain; the
|
|
4049
4142
|
// endpoint is the `node` wildcard.
|
|
4050
|
-
"planning_cycle_schedules_work_item"
|
|
4143
|
+
"planning_cycle_schedules_work_item",
|
|
4144
|
+
// Project membership (0.33.0, widened from project_delivers_epic):
|
|
4145
|
+
// endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
|
|
4146
|
+
// bug}. Same construction and same reasoning as the cadence edge above.
|
|
4147
|
+
"project_delivers_work_item"
|
|
4051
4148
|
];
|
|
4052
4149
|
var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
|
|
4053
4150
|
function isPolymorphicEdge(key) {
|
|
@@ -4085,6 +4182,42 @@ function isLegacyProductStage(stage) {
|
|
|
4085
4182
|
return typeof stage === "string" && stage.toLowerCase() in LEGACY_PRODUCT_STAGES;
|
|
4086
4183
|
}
|
|
4087
4184
|
|
|
4185
|
+
// src/shapes/base-node.ts
|
|
4186
|
+
var BASE_NODE_FIELD_PRESENCE = {
|
|
4187
|
+
id: true,
|
|
4188
|
+
type: true,
|
|
4189
|
+
title: true,
|
|
4190
|
+
slug: true,
|
|
4191
|
+
aliases: true,
|
|
4192
|
+
key: true,
|
|
4193
|
+
description: true,
|
|
4194
|
+
tags: true,
|
|
4195
|
+
status: true,
|
|
4196
|
+
archived: true,
|
|
4197
|
+
archived_at: true,
|
|
4198
|
+
source_id: true,
|
|
4199
|
+
source_type: true,
|
|
4200
|
+
mapping_confidence: true,
|
|
4201
|
+
external_tool: true,
|
|
4202
|
+
external_ref: true,
|
|
4203
|
+
external_id: true,
|
|
4204
|
+
external_links: true,
|
|
4205
|
+
created_at: true,
|
|
4206
|
+
updated_at: true,
|
|
4207
|
+
properties: true
|
|
4208
|
+
};
|
|
4209
|
+
var UPG_BASE_NODE_FIELDS = Object.freeze(
|
|
4210
|
+
Object.keys(BASE_NODE_FIELD_PRESENCE)
|
|
4211
|
+
);
|
|
4212
|
+
var UPG_BASE_NODE_FIELD_SET = new Set(UPG_BASE_NODE_FIELDS);
|
|
4213
|
+
var UPG_BASE_NODE_SPECIAL_MERGE_FIELDS = /* @__PURE__ */ new Set([
|
|
4214
|
+
"id",
|
|
4215
|
+
"type",
|
|
4216
|
+
"slug",
|
|
4217
|
+
"aliases",
|
|
4218
|
+
"properties"
|
|
4219
|
+
]);
|
|
4220
|
+
|
|
4088
4221
|
// src/shapes/document.ts
|
|
4089
4222
|
var UPG_CROSS_ONLY_EDGE_TYPES = [
|
|
4090
4223
|
// Peer overlap: "these two products share / overlap on this thing." Symmetric,
|
|
@@ -7162,6 +7295,22 @@ var TEMPLATE_LIFECYCLES = [
|
|
|
7162
7295
|
fromTemplate("nps_campaign", OPERATIONAL_TEMPLATE),
|
|
7163
7296
|
fromTemplate("monitor", OPERATIONAL_TEMPLATE),
|
|
7164
7297
|
fromTemplate("alert_rule", OPERATIONAL_TEMPLATE),
|
|
7298
|
+
// 0.33.0: agent_skill and agent_hook move here from WORK_ITEM. A durable
|
|
7299
|
+
// capability and an event trigger were both reaching `in_review` and `done`,
|
|
7300
|
+
// which is the wrong shape rather than a missing one: neither is work that gets
|
|
7301
|
+
// reviewed and accepted, and zero of the ten field instances across the corpus
|
|
7302
|
+
// had ever left the initial phase. Their nearest siblings are the two lines
|
|
7303
|
+
// directly above, and `monitor` and `alert_rule` are the same kind of thing:
|
|
7304
|
+
// something that runs continuously, can be paused, and is eventually retired.
|
|
7305
|
+
//
|
|
7306
|
+
// NOT lifecycle-free. The `user_story` precedent for lifecycle-freedom is the
|
|
7307
|
+
// Statement/Implementation split, where the paired `task` carries the
|
|
7308
|
+
// lifecycle. There is no paired entity here, so lifecycle-freedom would leave
|
|
7309
|
+
// `agent_skill` unable to say it is disabled and would entrench
|
|
7310
|
+
// `AgentHookProperties.hook_status` (now @deprecated) as the permanent status
|
|
7311
|
+
// axis, which is the *_status shadow Pattern D collapsed fourteen times.
|
|
7312
|
+
fromTemplate("agent_skill", OPERATIONAL_TEMPLATE),
|
|
7313
|
+
fromTemplate("agent_hook", OPERATIONAL_TEMPLATE),
|
|
7165
7314
|
fromTemplate("on_call_rotation", OPERATIONAL_TEMPLATE),
|
|
7166
7315
|
fromTemplate("security_audit", OPERATIONAL_TEMPLATE),
|
|
7167
7316
|
fromTemplate("penetration_test", OPERATIONAL_TEMPLATE),
|
|
@@ -7190,8 +7339,8 @@ var TEMPLATE_LIFECYCLES = [
|
|
|
7190
7339
|
fromTemplate("infrastructure_component", MATURITY_TEMPLATE),
|
|
7191
7340
|
// ── Phase C: WORK_ITEM (todo / in_progress / in_review / done) ──────────────
|
|
7192
7341
|
fromTemplate("agent_task", WORK_ITEM_TEMPLATE),
|
|
7193
|
-
|
|
7194
|
-
|
|
7342
|
+
// agent_skill and agent_hook moved WORK_ITEM -> OPERATIONAL at 0.33.0; see the
|
|
7343
|
+
// OPERATIONAL block below. agent_task stays: a task IS a work item.
|
|
7195
7344
|
// UPG-690 (0.21.0): task (exact match) + epic (todo→in_progress→done ⊆ WORK_ITEM,
|
|
7196
7345
|
// in_review becomes available — non-breaking widening) folded off hand-authored.
|
|
7197
7346
|
fromTemplate("task", WORK_ITEM_TEMPLATE),
|
|
@@ -10256,6 +10405,16 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
|
|
|
10256
10405
|
return result;
|
|
10257
10406
|
}
|
|
10258
10407
|
var UPG_EDGE_MIGRATIONS = {
|
|
10408
|
+
"0.33.0": [
|
|
10409
|
+
// G1 — the project membership edge widens from epic to the work-item set.
|
|
10410
|
+
// Same construction as the 0.32.0 cadence rename directly below, and for the
|
|
10411
|
+
// same measured reason: the adapters default an unrecognised issue to `task`,
|
|
10412
|
+
// so the type a project most needed to deliver was the one type it could not
|
|
10413
|
+
// reach. No requires_target_type, because the destination's target_type is
|
|
10414
|
+
// the `node` wildcard and endpoint guards check the catalog's declared type
|
|
10415
|
+
// rather than the concrete instance. Gate only on the unwidened source.
|
|
10416
|
+
{ kind: "rename", from: "project_delivers_epic", to: "project_delivers_work_item", requires_source_type: "project", reason: "0.33.0: a project could only deliver an epic, while a Linear dry-run carried 651 project memberships as properties.linear_project_id and emitted zero edges. Widened to the polymorphic work-item endpoint; no flip, source unchanged. Measured live population of the old type at release: one, in the CI saturation fixture, so this rename is a vocabulary change rather than a data migration." }
|
|
10417
|
+
],
|
|
10259
10418
|
"0.32.0": [
|
|
10260
10419
|
// C5 — the cadence scheduling edge widens from user_story to the work-item
|
|
10261
10420
|
// set. No requires_target_type: the destination's target_type is the `node`
|
|
@@ -11074,6 +11233,41 @@ function migrateEdge(edge, fromVersion, toVersion, endpoints) {
|
|
|
11074
11233
|
// src/grammar/status-migrations.ts
|
|
11075
11234
|
var UPG_STATUS_MIGRATIONS = {
|
|
11076
11235
|
// ── Engineering / Operations ─────────────────────────────────────────────
|
|
11236
|
+
// ── Agent automation: WORK_ITEM -> OPERATIONAL (0.33.0) ──────────────────
|
|
11237
|
+
// agent_skill and agent_hook carried WORK_ITEM until 0.33.0, so every graph
|
|
11238
|
+
// written before it holds WORK_ITEM phase ids against an OPERATIONAL
|
|
11239
|
+
// lifecycle. Measured population at release: 4 nodes, all `todo`. The other
|
|
11240
|
+
// five entries cover the phases those graphs could legally have reached.
|
|
11241
|
+
//
|
|
11242
|
+
// in_review maps to `active` rather than to a review phase because OPERATIONAL
|
|
11243
|
+
// has none: a hook awaiting review is still a hook that is running or about to.
|
|
11244
|
+
// cancelled maps to `sunset` rather than `completed`: OPERATIONAL has no
|
|
11245
|
+
// cancelled bucket and a discontinued trigger is wound down, not finished.
|
|
11246
|
+
agent_skill: {
|
|
11247
|
+
todo: "planning",
|
|
11248
|
+
backlog: "planning",
|
|
11249
|
+
in_progress: "active",
|
|
11250
|
+
in_review: "active",
|
|
11251
|
+
done: "completed",
|
|
11252
|
+
cancelled: "sunset"
|
|
11253
|
+
},
|
|
11254
|
+
agent_hook: {
|
|
11255
|
+
todo: "planning",
|
|
11256
|
+
backlog: "planning",
|
|
11257
|
+
in_progress: "active",
|
|
11258
|
+
in_review: "active",
|
|
11259
|
+
done: "completed",
|
|
11260
|
+
cancelled: "sunset",
|
|
11261
|
+
// The AgentHookProperties.hook_status vocabulary, @deprecated at 0.33.0 as a
|
|
11262
|
+
// *_status shadow of the base `status`. `error` maps to `paused` and NOT to a
|
|
11263
|
+
// failure phase: OPERATIONAL has none, and runtime health is store or
|
|
11264
|
+
// telemetry state rather than a fact about the thing. That is the same cut
|
|
11265
|
+
// that kept composition.rev, which is a fact, and excluded a concurrency
|
|
11266
|
+
// token, which is not. The failure detail belongs in `description` or a
|
|
11267
|
+
// diagnostic property.
|
|
11268
|
+
disabled: "paused",
|
|
11269
|
+
error: "paused"
|
|
11270
|
+
},
|
|
11077
11271
|
service: {
|
|
11078
11272
|
// Lifecycle: [development, staging, production, deprecated]
|
|
11079
11273
|
// Authoring habit from before lifecycle adoption: every long-lived
|
|
@@ -11523,7 +11717,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
11523
11717
|
agent_hook: {
|
|
11524
11718
|
hook_event: { type: "string", description: "Event that triggers this hook" },
|
|
11525
11719
|
hook_action: { type: "string", description: "Action performed when the hook fires" },
|
|
11526
|
-
hook_status: { type: "string", enum: ["active", "disabled", "error"], description: "Operational status of the hook" },
|
|
11720
|
+
hook_status: { type: "string", enum: ["active", "disabled", "error"], description: "Operational status of the hook. @deprecated since 0.33.0, removeIn 1.0.0. Use the base `status` field. `agent_hook` moved from the WORK_ITEM lifecycle to OPERATIONAL at 0.33.0, whose phases carry this axis directly: `active` maps to `active` and `disabled` maps to `paused`. A `*_status` property beside the base `status` is the shadow that Pattern D collapsed fourteen times at 0.15.0, and a type that models its own status in a property is a type whose lifecycle did not fit. `error` maps to `paused`: OPERATIONAL has no failure phase, and runtime health is store or telemetry state rather than a fact about the thing, which is the same cut that keeps `composition.rev` and excludes a concurrency token. Put the failure detail in `description` or a diagnostic property." },
|
|
11527
11721
|
execution_count: { type: "number", description: "Number of times this hook has fired", modifier: "snapshot" }
|
|
11528
11722
|
},
|
|
11529
11723
|
// AgentSessionProperties: Agent session.
|
|
@@ -12060,12 +12254,12 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12060
12254
|
},
|
|
12061
12255
|
// CompositionProperties
|
|
12062
12256
|
composition: {
|
|
12257
|
+
member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
|
|
12258
|
+
presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
|
|
12063
12259
|
members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
|
|
12064
12260
|
rev: { type: "number", description: "Monotonic revision, bumped on each republish of the same slug.", notes: "A published revision is a fact about the composition, user-visible as a new print from the same plate, so it is serialised. Worth distinguishing from a store's concurrency token, which shares the name in some backends but is a fact about the table rather than about the thing, and is not spec data." },
|
|
12065
12261
|
published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
|
|
12066
|
-
published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
|
|
12067
|
-
member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the composition shows. `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A composition with no `member_query` is authored, which is what every composition written before 0.32.0 is." },
|
|
12068
|
-
presentation: { type: "object", description: "Advisory rendering intent for the composition as a whole. A consumer may ignore it entirely and still be conformant." }
|
|
12262
|
+
published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
|
|
12069
12263
|
},
|
|
12070
12264
|
// ConfigurationAxisProperties: A named dimension along which the product's composition differs.
|
|
12071
12265
|
configuration_axis: {
|
|
@@ -12328,7 +12522,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
12328
12522
|
budget: { type: "number", description: "Allocated budget for this program" },
|
|
12329
12523
|
target_leads: { type: "number", description: "Target number of leads to generate" }
|
|
12330
12524
|
},
|
|
12331
|
-
// DepartmentProperties: Department entity.
|
|
12525
|
+
// DepartmentProperties: Department entity. A department is the single org tier above a team.
|
|
12332
12526
|
department: {
|
|
12333
12527
|
headcount: { type: "number", description: "Total number of people in the department", modifier: "snapshot" },
|
|
12334
12528
|
budget: { type: "number", description: "Annual budget allocated to the department" },
|
|
@@ -13482,7 +13676,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13482
13676
|
},
|
|
13483
13677
|
// ObjectiveProperties: A high-level strategic goal. The O in OKR.
|
|
13484
13678
|
objective: {
|
|
13485
|
-
timeframe: { type: "string", description: 'Planning timeframe (e.g. "Q1 2026", "H1 2026")' },
|
|
13679
|
+
timeframe: { type: "string", description: 'Planning timeframe (e.g. "Q1 2026", "H1 2026"). @deprecated since 0.33.0, removeIn 1.0.0. Promote the period to a `planning_cycle` node and link it with the `objective_scoped_to_planning_cycle` edge, which has existed since 0.20.0 and points at a shared, dated, nestable interval instead of a drifting per-objective string. Until promoted, the value is a display label and nothing schedules on it. The promotion is documented rather than automated and no `drop_props` migration ships: which cycle a free-text string means, and whether one exists yet, is a judgement, and dropping the string before the cycle exists destroys the only record of the intent. @example "Q1 2026", "H1 2026"' },
|
|
13486
13680
|
progress: { type: "number", description: "Overall progress (0\u2013100)" }
|
|
13487
13681
|
},
|
|
13488
13682
|
// ObservationProperties: Discrete observation captured during research. Absorbed from the deprecated Highlight entity.
|
|
@@ -13703,7 +13897,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
13703
13897
|
logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
|
|
13704
13898
|
launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
|
|
13705
13899
|
described_configuration: { type: "string", description: 'The product configuration this graph describes, named in plain language. A label for readers: nothing reads it to gate, filter, or alter how a tool interprets the graph. @example "Enterprise plan, split-navigation flag on"', notes: "A product's composition often differs by feature flag, plan tier, permission level or beta programme: surfaces appear, disappear, or are replaced by different surfaces with different occupants. A graph that models one of those without saying which is qualified by a condition nobody wrote down, and every fact in it inherits that silence. It makes no claim about which configuration most customers are on. Where a product genuinely ships several and the differences matter, declare a `configuration_axis` instead and let each fact say which values it holds under." },
|
|
13706
|
-
key_prefix: { type: "string", description: 'Prefix for keys minted onto this product\'s nodes (e.g. `"LTN"`, giving `LTN-1`, `LTN-2`,
|
|
13900
|
+
key_prefix: { type: "string", description: 'Prefix for keys minted onto this product\'s nodes (e.g. `"LTN"`, giving `LTN-1`, `LTN-2`, ...). Retained as the single-team unasked default. @deprecated since 0.33.0, removeIn 1.0.0. Use `team.key_prefix`. This field\'s own summary called the prefix "the namespace a tracker calls a team", and a single string cannot express a product with two of them: a two-team product has never been expressible, which is a defect rather than an unused feature. Ignored whenever any team in the product declares a prefix. Deprecated rather than removed because removal would strand every graph that has a product prefix and no `team` nodes. @example "LTN"', notes: "Pairs with `UPGBaseNode.key`, which holds the minted value and states the resolution order normatively. Only the prefix is serialised: the next number is `max(existing) + 1`, derived from the graph, because a counter is a fact about a store rather than about the product. WHY THIS IS NOT A DEFAULT WITH `team.key_prefix` AS AN OVERRIDE. A default and an override describe one fact at two scopes. These describe different facts, and this one is correct only when the product has exactly one team, which is an accident of arity rather than a scope. Keeping it live as a fallback would also reintroduce the defect: a prefix that still resolves wins on ladder order, so a legacy-prefixed product with two teams would offer no choice at all and mint everything under the legacy prefix silently, with migration order deciding the behaviour per graph." }
|
|
13707
13901
|
},
|
|
13708
13902
|
// ProductAreaProperties: ProductArea entity.
|
|
13709
13903
|
product_area: {
|
|
@@ -14192,7 +14386,7 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
14192
14386
|
// StrategicThemeProperties: StrategicTheme entity.
|
|
14193
14387
|
strategic_theme: {
|
|
14194
14388
|
owner: { type: "string", description: "Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
|
|
14195
|
-
time_horizon: { type: "string", description: 'Bounded period this theme is active (a theme is time-bound, within a pillar). @deprecated since 0.20.0. Promote the period to a `planning_cycle` node and link it with the `strategic_theme_scoped_to_planning_cycle` edge, which points at a shared, dated, nestable interval instead of a drifting per-theme string. Kept (not removed) for back-compat
|
|
14389
|
+
time_horizon: { type: "string", description: 'Bounded period this theme is active (a theme is time-bound, within a pillar). @deprecated since 0.20.0, removeIn 1.0.0. Promote the period to a `planning_cycle` node and link it with the `strategic_theme_scoped_to_planning_cycle` edge, which points at a shared, dated, nestable interval instead of a drifting per-theme string. Kept (not removed) for back-compat. The removal version is stated at 0.33.0 because an undated deprecation is how this field survived twelve minors; the promotion is documented rather than automated and no `drop_props` migration ships. `strategic_pillar.time_horizon` stays as-is (a durable pillar horizon is genuinely open-ended, not a dated cycle). @example "Q1 2026", "FY26"' },
|
|
14196
14390
|
description: { type: "string", description: "Short narrative of this time-bound thrust within its pillar" },
|
|
14197
14391
|
scope: { type: "string", description: "What the theme explicitly includes or excludes" }
|
|
14198
14392
|
},
|
|
@@ -14311,7 +14505,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
14311
14505
|
team: {
|
|
14312
14506
|
team_type: { type: "string", enum: ["product", "engineering", "design", "growth", "customer_success"], description: "Functional area of the team" },
|
|
14313
14507
|
size: { type: "number", description: "Number of people on the team" },
|
|
14314
|
-
mission: { type: "string", description: "Team's mission statement" }
|
|
14508
|
+
mission: { type: "string", description: "Team's mission statement" },
|
|
14509
|
+
key_prefix: { type: "string", description: 'Prefix this team mints node keys with (e.g. `"ENTP"`, giving `ENTP-1`, `ENTP-2`, ...). @example "ENTP"', notes: "SUPERSEDES `product.key_prefix` FOR THIS PRODUCT. The moment any team declares a prefix, the product-level prefix stops being consulted and the declared team prefixes become the candidate set a create surface offers. A product-level prefix that still resolved would win on order alone, and a multi-team product would then silently mint everything under it, which is the defect this field exists to end. A DECLARED PREFIX IS A CANDIDATE BEFORE IT IS OBSERVED. Candidates derived only from keys that already exist cannot see a team's first create, which is the one that most needs asking about. UNIQUENESS IS PRODUCT-SCOPED, AND A TEAM PREFIX DOES NOT WIDEN IT. The key sequence runs per product across entity types, so two teams in one product share one number line and never collide with each other. A team prefix names a team WITHIN that scope; it does not create a sequence that spans products. MINTING IS PRODUCT-SCOPED (normative, 0.33.0). `team` is `portfolio_shared`, so one team node can be referenced from two products. Minting is not portfolio-scoped with it. The rule, and it is a requirement rather than a recommendation: 1. A portfolio-shared team's prefix is NOT a minting candidate in a second product. The first product a team mints under is the only product that prefix mints in. 2. Refusal is NO-KEY. The node is created without a key. Refusal MUST NOT be an exception: a keyless create is a legal outcome everywhere else in the ladder, and throwing here would break creates that succeed today on every surface. Throwing stays reserved for a broken invariant, not for policy. 3. The rule applies on the MINT path, not the picker path, and it covers the INFERRED case. A prefix that was never requested by anyone, and was derived from keys that already exist in the second product, is refused on the same terms as one a caller named. A guard that only inspects an explicitly requested prefix misses the quiet path, and the quiet path is the one a fixture reproduces. WHY THE SCOPE STOPS AT THE PRODUCT. Key uniqueness is enforced by a `(product_id, key)` index, which permits the same key under two product ids by construction. A prefix that minted in two products would therefore run two independent sequences under one name and hand two different nodes the same citation, with nothing objecting. Portfolio-shared team minting is DEFERRED until a supra-product uniqueness design exists, rather than approximated. This rule states the CONTRACT. The behaviour belongs to whatever mints keys, which is not this package: see `UPGBaseNode.key`." }
|
|
14315
14510
|
},
|
|
14316
14511
|
// TeamOkrProperties: TeamOkr entity.
|
|
14317
14512
|
team_okr: {
|
|
@@ -14637,6 +14832,8 @@ var UPG_PROPERTY_SCHEMA = {
|
|
|
14637
14832
|
},
|
|
14638
14833
|
// WorkspaceProperties: Workspace: a spatial thinking space for arranging entities.
|
|
14639
14834
|
workspace: {
|
|
14835
|
+
member_query: { type: "object", description: "When present, membership is DERIVED: members are produced by running this query rather than authored by placement.", notes: "This is the portable statement of what the layer shows. On a composition, `CompositionMember.href` remains the publishing tool's own resolved route and stays opaque to everyone else; a member may carry both, and then the href is a fast path while the query is the meaning. A consumer that cannot parse the href can still render the layer, which is the whole reason the declaration is here rather than in a tool-namespaced bag key. A layer with no `member_query` is authored, which is what every composition written before 0.32.0 is. DECLARED ON BOTH HALVES OF THE PAIR since 0.33.0. A layer is query-driven while it is being worked on, not only once it is published, so declaring the query only on the durable composition would make it a fact invented at publish time rather than one recorded." },
|
|
14836
|
+
presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
|
|
14640
14837
|
visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
|
|
14641
14838
|
purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
|
|
14642
14839
|
workspace_purpose: { type: "string", enum: ["discovery", "planning", "retrospective", "design", "research", "strategy", "general"], description: "What the workspace is for. Drives template suggestions and surfaces in workspace browsers.", notes: "`discovery` is persona, job and opportunity exploration. `planning` covers roadmap and decision sessions. `retrospective` is reflection on shipped work. `design` is experience or UI exploration. `research` organises study data and synthesis. `strategy` is high-level direction setting. `general` is the catch-all." },
|
|
@@ -32842,6 +33039,9 @@ var NODE_KEY_ORDER = [
|
|
|
32842
33039
|
"external_tool",
|
|
32843
33040
|
"external_ref",
|
|
32844
33041
|
"external_id",
|
|
33042
|
+
"external_links",
|
|
33043
|
+
"created_at",
|
|
33044
|
+
"updated_at",
|
|
32845
33045
|
"sort_order",
|
|
32846
33046
|
"properties"
|
|
32847
33047
|
];
|
|
@@ -33124,7 +33324,7 @@ function checkHeaderSealText(text) {
|
|
|
33124
33324
|
}
|
|
33125
33325
|
|
|
33126
33326
|
// src/index.ts
|
|
33127
|
-
var UPG_VERSION = "0.
|
|
33327
|
+
var UPG_VERSION = "0.33.0";
|
|
33128
33328
|
var UPG_FORMAT_VERSION = "0.4.0";
|
|
33129
33329
|
var MARKDOWN_FORMAT_VERSION = "0.1";
|
|
33130
33330
|
var UPG_TYPES = getTypes();
|
|
@@ -33210,6 +33410,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
33210
33410
|
LEGACY_PRODUCT_STAGES,
|
|
33211
33411
|
MARKDOWN_FORMAT_VERSION,
|
|
33212
33412
|
MARKET_COMPETITIVE_PLAYBOOK,
|
|
33413
|
+
NODE_KEY_ORDER,
|
|
33213
33414
|
OPERATIONS_QUALITY_PLAYBOOK,
|
|
33214
33415
|
PRESENT_UNDER_PROPERTY,
|
|
33215
33416
|
PRODUCT_DELIVERY_PLAYBOOK,
|
|
@@ -33229,6 +33430,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
|
|
|
33229
33430
|
UPG_APPROACHES_BY_ID,
|
|
33230
33431
|
UPG_AREA_TAXONOMY,
|
|
33231
33432
|
UPG_AREA_TAXONOMY_BY_COVERAGE_KEY,
|
|
33433
|
+
UPG_BASE_NODE_FIELDS,
|
|
33434
|
+
UPG_BASE_NODE_FIELD_SET,
|
|
33435
|
+
UPG_BASE_NODE_SPECIAL_MERGE_FIELDS,
|
|
33232
33436
|
UPG_CANONICAL_FORMAT_VERSION,
|
|
33233
33437
|
UPG_CONTAINMENT_FREE_TYPES,
|
|
33234
33438
|
UPG_COUNT_BENCHMARKS,
|