@unified-product-graph/core 0.32.2 → 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 CHANGED
@@ -7,6 +7,48 @@ 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
+
10
52
  ## [0.32.2] - 2026-08-21
11
53
 
12
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.
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. 1082 typed edges. 53 canonical framework definitions, with more added as each is reviewed.
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,
@@ -2726,6 +2727,53 @@ var UPG_EDGE_CATALOG = {
2726
2727
  // contained by product / department / team. Hierarchy edges for person
2727
2728
  // are deliberately omitted; adding them only to satisfy a hierarchy
2728
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" },
2729
2777
  team_staffed_with_role: { forward_verb: "staffed_with", reverse_verb: "staffed_in", classification: "hierarchy", source_type: "team", target_type: "role" },
2730
2778
  team_targets_team_okr: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "team", target_type: "team_okr" },
2731
2779
  team_reflects_in_retrospective: { forward_verb: "reflects_in", reverse_verb: "reflects", classification: "hierarchy", source_type: "team", target_type: "retrospective" },
@@ -2770,7 +2818,31 @@ var UPG_EDGE_CATALOG = {
2770
2818
  program_reported_via_status_report: { forward_verb: "reported_via", reverse_verb: "reports", classification: "hierarchy", source_type: "program", target_type: "status_report" },
2771
2819
  program_contains_epic: { forward_verb: "contains", reverse_verb: "contained_in", classification: "cross-domain", source_type: "program", target_type: "epic" },
2772
2820
  project_implements_initiative: { forward_verb: "implements", reverse_verb: "implemented_by", classification: "cross-domain", source_type: "project", target_type: "initiative" },
2773
- project_delivers_epic: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "cross-domain", source_type: "project", target_type: "epic" },
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 },
2774
2846
  milestone_gates_release: { forward_verb: "gates", reverse_verb: "gated_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
2775
2847
  milestone_triggers_release: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
2776
2848
  deliverable_ships_feature: { forward_verb: "ships", reverse_verb: "shipped_by", classification: "cross-domain", source_type: "deliverable", target_type: "feature" },
@@ -3836,6 +3908,24 @@ var UPG_EDGE_CATALOG = {
3836
3908
  // so the lifecycle is governed rather than a standalone root (0.12.1 refinement).
3837
3909
  operating_lifecycle_defined_by_specification: { forward_verb: "defined_by", reverse_verb: "defines", classification: "semantic", source_type: "operating_lifecycle", target_type: "specification" },
3838
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.
3839
3929
  // Stewardship: a registry specification is governed by a (registry-hostable) organization (0.9.13).
3840
3930
  specification_governed_by_organization: { forward_verb: "governed_by", reverse_verb: "governs", classification: "semantic", source_type: "specification", target_type: "organization" },
3841
3931
  // Operating-lifecycle cross-edges (0.11.6): a product's journey phase realises a canonical
@@ -4050,7 +4140,11 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
4050
4140
  // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
4051
4141
  // bug}. The `work_item` key token names the intended semantic domain; the
4052
4142
  // endpoint is the `node` wildcard.
4053
- "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"
4054
4148
  ];
4055
4149
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
4056
4150
  function isPolymorphicEdge(key) {
@@ -4107,6 +4201,9 @@ var BASE_NODE_FIELD_PRESENCE = {
4107
4201
  external_tool: true,
4108
4202
  external_ref: true,
4109
4203
  external_id: true,
4204
+ external_links: true,
4205
+ created_at: true,
4206
+ updated_at: true,
4110
4207
  properties: true
4111
4208
  };
4112
4209
  var UPG_BASE_NODE_FIELDS = Object.freeze(
@@ -7198,6 +7295,22 @@ var TEMPLATE_LIFECYCLES = [
7198
7295
  fromTemplate("nps_campaign", OPERATIONAL_TEMPLATE),
7199
7296
  fromTemplate("monitor", OPERATIONAL_TEMPLATE),
7200
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),
7201
7314
  fromTemplate("on_call_rotation", OPERATIONAL_TEMPLATE),
7202
7315
  fromTemplate("security_audit", OPERATIONAL_TEMPLATE),
7203
7316
  fromTemplate("penetration_test", OPERATIONAL_TEMPLATE),
@@ -7226,8 +7339,8 @@ var TEMPLATE_LIFECYCLES = [
7226
7339
  fromTemplate("infrastructure_component", MATURITY_TEMPLATE),
7227
7340
  // ── Phase C: WORK_ITEM (todo / in_progress / in_review / done) ──────────────
7228
7341
  fromTemplate("agent_task", WORK_ITEM_TEMPLATE),
7229
- fromTemplate("agent_skill", WORK_ITEM_TEMPLATE),
7230
- fromTemplate("agent_hook", WORK_ITEM_TEMPLATE),
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.
7231
7344
  // UPG-690 (0.21.0): task (exact match) + epic (todo→in_progress→done ⊆ WORK_ITEM,
7232
7345
  // in_review becomes available — non-breaking widening) folded off hand-authored.
7233
7346
  fromTemplate("task", WORK_ITEM_TEMPLATE),
@@ -10292,6 +10405,16 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
10292
10405
  return result;
10293
10406
  }
10294
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
+ ],
10295
10418
  "0.32.0": [
10296
10419
  // C5 — the cadence scheduling edge widens from user_story to the work-item
10297
10420
  // set. No requires_target_type: the destination's target_type is the `node`
@@ -11110,6 +11233,41 @@ function migrateEdge(edge, fromVersion, toVersion, endpoints) {
11110
11233
  // src/grammar/status-migrations.ts
11111
11234
  var UPG_STATUS_MIGRATIONS = {
11112
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
+ },
11113
11271
  service: {
11114
11272
  // Lifecycle: [development, staging, production, deprecated]
11115
11273
  // Authoring habit from before lifecycle adoption: every long-lived
@@ -11559,7 +11717,7 @@ var UPG_PROPERTY_SCHEMA = {
11559
11717
  agent_hook: {
11560
11718
  hook_event: { type: "string", description: "Event that triggers this hook" },
11561
11719
  hook_action: { type: "string", description: "Action performed when the hook fires" },
11562
- 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." },
11563
11721
  execution_count: { type: "number", description: "Number of times this hook has fired", modifier: "snapshot" }
11564
11722
  },
11565
11723
  // AgentSessionProperties: Agent session.
@@ -12096,12 +12254,12 @@ var UPG_PROPERTY_SCHEMA = {
12096
12254
  },
12097
12255
  // CompositionProperties
12098
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." },
12099
12259
  members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
12100
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." },
12101
12261
  published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
12102
- published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." },
12103
- 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." },
12104
- 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`." }
12105
12263
  },
12106
12264
  // ConfigurationAxisProperties: A named dimension along which the product's composition differs.
12107
12265
  configuration_axis: {
@@ -12364,7 +12522,7 @@ var UPG_PROPERTY_SCHEMA = {
12364
12522
  budget: { type: "number", description: "Allocated budget for this program" },
12365
12523
  target_leads: { type: "number", description: "Target number of leads to generate" }
12366
12524
  },
12367
- // DepartmentProperties: Department entity.
12525
+ // DepartmentProperties: Department entity. A department is the single org tier above a team.
12368
12526
  department: {
12369
12527
  headcount: { type: "number", description: "Total number of people in the department", modifier: "snapshot" },
12370
12528
  budget: { type: "number", description: "Annual budget allocated to the department" },
@@ -13518,7 +13676,7 @@ var UPG_PROPERTY_SCHEMA = {
13518
13676
  },
13519
13677
  // ObjectiveProperties: A high-level strategic goal. The O in OKR.
13520
13678
  objective: {
13521
- 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"' },
13522
13680
  progress: { type: "number", description: "Overall progress (0\u2013100)" }
13523
13681
  },
13524
13682
  // ObservationProperties: Discrete observation captured during research. Absorbed from the deprecated Highlight entity.
@@ -13739,7 +13897,7 @@ var UPG_PROPERTY_SCHEMA = {
13739
13897
  logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
13740
13898
  launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
13741
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." },
13742
- key_prefix: { type: "string", description: 'Prefix for keys minted onto this product\'s nodes (e.g. `"LTN"`, giving `LTN-1`, `LTN-2`, \u2026). A product with no prefix mints no keys. @example "LTN"', notes: 'Pairs with `UPGBaseNode.key`, which holds the minted value. The prefix sits on the product because the key\'s uniqueness scope is the product, across entity types; the namespace a tracker calls a "team". 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.' }
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." }
13743
13901
  },
13744
13902
  // ProductAreaProperties: ProductArea entity.
13745
13903
  product_area: {
@@ -14228,7 +14386,7 @@ var UPG_PROPERTY_SCHEMA = {
14228
14386
  // StrategicThemeProperties: StrategicTheme entity.
14229
14387
  strategic_theme: {
14230
14388
  owner: { type: "string", description: "Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
14231
- 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; removal is a later major. `strategic_pillar.time_horizon` stays as-is (a durable pillar horizon is genuinely open-ended, not a dated cycle). @example "Q1 2026", "FY26"' },
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"' },
14232
14390
  description: { type: "string", description: "Short narrative of this time-bound thrust within its pillar" },
14233
14391
  scope: { type: "string", description: "What the theme explicitly includes or excludes" }
14234
14392
  },
@@ -14347,7 +14505,8 @@ var UPG_PROPERTY_SCHEMA = {
14347
14505
  team: {
14348
14506
  team_type: { type: "string", enum: ["product", "engineering", "design", "growth", "customer_success"], description: "Functional area of the team" },
14349
14507
  size: { type: "number", description: "Number of people on the team" },
14350
- 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`." }
14351
14510
  },
14352
14511
  // TeamOkrProperties: TeamOkr entity.
14353
14512
  team_okr: {
@@ -14673,6 +14832,8 @@ var UPG_PROPERTY_SCHEMA = {
14673
14832
  },
14674
14833
  // WorkspaceProperties: Workspace: a spatial thinking space for arranging entities.
14675
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." },
14676
14837
  visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
14677
14838
  purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
14678
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." },
@@ -32878,6 +33039,9 @@ var NODE_KEY_ORDER = [
32878
33039
  "external_tool",
32879
33040
  "external_ref",
32880
33041
  "external_id",
33042
+ "external_links",
33043
+ "created_at",
33044
+ "updated_at",
32881
33045
  "sort_order",
32882
33046
  "properties"
32883
33047
  ];
@@ -33160,7 +33324,7 @@ function checkHeaderSealText(text) {
33160
33324
  }
33161
33325
 
33162
33326
  // src/index.ts
33163
- var UPG_VERSION = "0.32.2";
33327
+ var UPG_VERSION = "0.33.0";
33164
33328
  var UPG_FORMAT_VERSION = "0.4.0";
33165
33329
  var MARKDOWN_FORMAT_VERSION = "0.1";
33166
33330
  var UPG_TYPES = getTypes();
@@ -33246,6 +33410,7 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
33246
33410
  LEGACY_PRODUCT_STAGES,
33247
33411
  MARKDOWN_FORMAT_VERSION,
33248
33412
  MARKET_COMPETITIVE_PLAYBOOK,
33413
+ NODE_KEY_ORDER,
33249
33414
  OPERATIONS_QUALITY_PLAYBOOK,
33250
33415
  PRESENT_UNDER_PROPERTY,
33251
33416
  PRODUCT_DELIVERY_PLAYBOOK,