@unified-product-graph/core 0.32.2 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2439,6 +2439,53 @@ var UPG_EDGE_CATALOG = {
2439
2439
  // contained by product / department / team. Hierarchy edges for person
2440
2440
  // are deliberately omitted; adding them only to satisfy a hierarchy
2441
2441
  // audit would invert the orthogonality the type was introduced to express.
2442
+ // ── Org membership and reporting (0.33.0) ───────────────────────────────────
2443
+ // Both edges are `semantic` REFERENCE edges and neither is a reversal of
2444
+ // the containment-free stance recorded immediately above. That note forecloses
2445
+ // HIERARCHY edges added ONLY to satisfy a hierarchy audit; these are neither.
2446
+ // UPG_CONTAINMENT_FREE_TYPES, UPG_VALID_CHILDREN, get_tree and G2b are all
2447
+ // untouched, and node_assigned_to_person was minted cross-domain onto `person`
2448
+ // at 0.32.0 on the same reading.
2449
+ //
2450
+ // DIRECTION IS LOAD-BEARING, not a preference. get_tree reads SOURCE as parent
2451
+ // (see the team_contains_team note above), so a team -> person edge would sit
2452
+ // one classification change away from putting people in the org tree. Sourcing
2453
+ // on `person` makes the containment reading unavailable rather than merely
2454
+ // unchosen. `team_includes_person` classified hierarchy is the shape to refuse.
2455
+ //
2456
+ // CLASSIFICATION IS `semantic` AND NOT `cross-domain`, which the 0.33.0 design
2457
+ // recommended before the guardrail was consulted. `person` and `team` both live
2458
+ // in the team_org domain, so a `cross-domain` label would contradict itself: the
2459
+ // edge crosses no domain boundary, and T1.7 forbids NEW same-domain cross-domain
2460
+ // edges precisely so that debt stops growing. Nothing the design argument
2461
+ // depends on moves: `semantic` enters neither UPG_VALID_CHILDREN nor get_tree,
2462
+ // and G2b is not engaged.
2463
+ // Membership is UNQUALIFIED: no role property on the edge. `role` is already an
2464
+ // entity type with team_staffed_with_role, so a role string here would be a
2465
+ // second role model beside a node type. The consequence is stated rather than
2466
+ // hidden: the graph can say a team has a slot and that a person is on the team,
2467
+ // and not that the person fills the slot. `person_holds_role` is banked, with
2468
+ // its condition the first field graph carrying both where the join is asked for.
2469
+ //
2470
+ // Neither is cross_product_eligible: `person` is not portfolio_shared, so the
2471
+ // 0.18.0 both-endpoints gate rejects the flag regardless of any team-scoping
2472
+ // ruling.
2473
+ person_member_of_team: { forward_verb: "member_of", reverse_verb: "has_member", classification: "semantic", source_type: "person", target_type: "team" },
2474
+ // Cited three times in spec source since 0.17.2 and existing zero times: the
2475
+ // team_contains_team note above, the team-nesting domain guide, and an
2476
+ // anti_patterns entry that instructs users to model a cross-department
2477
+ // reporting line with this edge. An anti-pattern guide is instructional by
2478
+ // construction, so that third site was telling users to use an edge the catalog
2479
+ // would refuse. Minting makes all three true and needs no prose edit.
2480
+ //
2481
+ // semantic, not hierarchy, and the temptation is real: a reporting line IS a
2482
+ // hierarchy in ordinary English, which is exactly why classifying it that way
2483
+ // would put `person` into containment and become the reversal the sibling edge
2484
+ // above is shaped to avoid. The org chart is a reference structure over
2485
+ // containment-free people. Same classification as the membership edge above, so
2486
+ // the person / team / department triangle is decided once rather than three
2487
+ // times.
2488
+ person_reports_to_person: { forward_verb: "reports_to", reverse_verb: "has_report", classification: "semantic", source_type: "person", target_type: "person" },
2442
2489
  team_staffed_with_role: { forward_verb: "staffed_with", reverse_verb: "staffed_in", classification: "hierarchy", source_type: "team", target_type: "role" },
2443
2490
  team_targets_team_okr: { forward_verb: "targets", reverse_verb: "targeted_by", classification: "hierarchy", source_type: "team", target_type: "team_okr" },
2444
2491
  team_reflects_in_retrospective: { forward_verb: "reflects_in", reverse_verb: "reflects", classification: "hierarchy", source_type: "team", target_type: "retrospective" },
@@ -2483,7 +2530,31 @@ var UPG_EDGE_CATALOG = {
2483
2530
  program_reported_via_status_report: { forward_verb: "reported_via", reverse_verb: "reports", classification: "hierarchy", source_type: "program", target_type: "status_report" },
2484
2531
  program_contains_epic: { forward_verb: "contains", reverse_verb: "contained_in", classification: "cross-domain", source_type: "program", target_type: "epic" },
2485
2532
  project_implements_initiative: { forward_verb: "implements", reverse_verb: "implemented_by", classification: "cross-domain", source_type: "project", target_type: "initiative" },
2486
- project_delivers_epic: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "cross-domain", source_type: "project", target_type: "epic" },
2533
+ // WIDENED AT 0.33.0, renamed from project_delivers_epic. The epic-only
2534
+ // endpoint could not hold a real tracker import: a Linear dry-run carried 651
2535
+ // project memberships as `properties.linear_project_id` and emitted zero
2536
+ // edges, because the default issue type an adapter produces is `task` and the
2537
+ // only project edge could not reach it. Endpoint-polymorphic over the
2538
+ // work-item set {feature, epic, user_story, task, bug} by the same
2539
+ // construction as planning_cycle_schedules_work_item: the `work_item` token
2540
+ // names the intended semantic domain, the endpoint is the `node` wildcard.
2541
+ //
2542
+ // NOT a `contains` verb, and UPG_VALID_CHILDREN.project is deliberately
2543
+ // untouched. `contains` is this catalog's containment verb and every edge
2544
+ // using it is classification: 'hierarchy', which under G2b obliges a matching
2545
+ // UPG_VALID_CHILDREN pair. That map is Record<parent, child[]> of concrete
2546
+ // type names, so a polymorphic endpoint can never discharge the obligation
2547
+ // its own verb would create. A work item is CONTAINED once (by its epic or
2548
+ // feature) and REFERENCED many times; the parent axis is containment and the
2549
+ // project axis is a reference. Giving project a second containment claim over
2550
+ // the same task is the two-trees problem.
2551
+ //
2552
+ // classification moves cross-domain -> semantic with the widening: pinned to
2553
+ // project -> epic it was a cross-domain pair, and once the endpoint is the
2554
+ // wildcard it is a reference relation between a planning container and
2555
+ // arbitrary work. deliberate_only because project membership is authored,
2556
+ // never inferred from co-occurrence.
2557
+ project_delivers_work_item: { forward_verb: "delivers", reverse_verb: "delivered_by", classification: "semantic", source_type: "project", target_type: "node", deliberate_only: true },
2487
2558
  milestone_gates_release: { forward_verb: "gates", reverse_verb: "gated_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
2488
2559
  milestone_triggers_release: { forward_verb: "triggers", reverse_verb: "triggered_by", classification: "cross-domain", source_type: "milestone", target_type: "release" },
2489
2560
  deliverable_ships_feature: { forward_verb: "ships", reverse_verb: "shipped_by", classification: "cross-domain", source_type: "deliverable", target_type: "feature" },
@@ -3549,6 +3620,24 @@ var UPG_EDGE_CATALOG = {
3549
3620
  // so the lifecycle is governed rather than a standalone root (0.12.1 refinement).
3550
3621
  operating_lifecycle_defined_by_specification: { forward_verb: "defined_by", reverse_verb: "defines", classification: "semantic", source_type: "operating_lifecycle", target_type: "specification" },
3551
3622
  operating_lifecycle_contains_operating_stage: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "operating_lifecycle", target_type: "operating_stage" },
3623
+ // Conformance (0.33.0, RE-OPENED). 0.33.0 planned to mint
3624
+ // product_conforms_to_specification and feature_conforms_to_specification here.
3625
+ // It does not, and the reason is a fact the design missed: the product-to-
3626
+ // foundation links are NOT absent, they live one tier up. See
3627
+ // UPG_CROSS_ONLY_EDGE_TYPES in shapes/document.ts, which has carried
3628
+ // `product_implements_specification`, `product_exposes_specification` and
3629
+ // `feature_conforms_to_specification` as portfolio-native cross-product edges
3630
+ // since 0.9.12, exactly as the section comment above says. The tiers are
3631
+ // disjoint by construction, so minting the same key here is a hard collision,
3632
+ // and minting only the product half would split one question across two tiers
3633
+ // under two near-synonymous names.
3634
+ //
3635
+ // What survives is the real gap, stated here rather than closed: a
3636
+ // `specification` node held INSIDE a single product graph, with no portfolio,
3637
+ // has no conformance edge, because the cross-only tier needs a registry target.
3638
+ // Whether that case is closed by widening the tier or by a catalog edge under a
3639
+ // non-colliding key is a design question and is banked, with its condition the
3640
+ // first single-product graph that models a specification it conforms to.
3552
3641
  // Stewardship: a registry specification is governed by a (registry-hostable) organization (0.9.13).
3553
3642
  specification_governed_by_organization: { forward_verb: "governed_by", reverse_verb: "governs", classification: "semantic", source_type: "specification", target_type: "organization" },
3554
3643
  // Operating-lifecycle cross-edges (0.11.6): a product's journey phase realises a canonical
@@ -3763,7 +3852,11 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
3763
3852
  // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
3764
3853
  // bug}. The `work_item` key token names the intended semantic domain; the
3765
3854
  // endpoint is the `node` wildcard.
3766
- "planning_cycle_schedules_work_item"
3855
+ "planning_cycle_schedules_work_item",
3856
+ // Project membership (0.33.0, widened from project_delivers_epic):
3857
+ // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
3858
+ // bug}. Same construction and same reasoning as the cadence edge above.
3859
+ "project_delivers_work_item"
3767
3860
  ];
3768
3861
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
3769
3862
  function isPolymorphicEdge(key) {
@@ -3820,6 +3913,9 @@ var BASE_NODE_FIELD_PRESENCE = {
3820
3913
  external_tool: true,
3821
3914
  external_ref: true,
3822
3915
  external_id: true,
3916
+ external_links: true,
3917
+ created_at: true,
3918
+ updated_at: true,
3823
3919
  properties: true
3824
3920
  };
3825
3921
  var UPG_BASE_NODE_FIELDS = Object.freeze(
@@ -6911,6 +7007,22 @@ var TEMPLATE_LIFECYCLES = [
6911
7007
  fromTemplate("nps_campaign", OPERATIONAL_TEMPLATE),
6912
7008
  fromTemplate("monitor", OPERATIONAL_TEMPLATE),
6913
7009
  fromTemplate("alert_rule", OPERATIONAL_TEMPLATE),
7010
+ // 0.33.0: agent_skill and agent_hook move here from WORK_ITEM. A durable
7011
+ // capability and an event trigger were both reaching `in_review` and `done`,
7012
+ // which is the wrong shape rather than a missing one: neither is work that gets
7013
+ // reviewed and accepted, and zero of the ten field instances across the corpus
7014
+ // had ever left the initial phase. Their nearest siblings are the two lines
7015
+ // directly above, and `monitor` and `alert_rule` are the same kind of thing:
7016
+ // something that runs continuously, can be paused, and is eventually retired.
7017
+ //
7018
+ // NOT lifecycle-free. The `user_story` precedent for lifecycle-freedom is the
7019
+ // Statement/Implementation split, where the paired `task` carries the
7020
+ // lifecycle. There is no paired entity here, so lifecycle-freedom would leave
7021
+ // `agent_skill` unable to say it is disabled and would entrench
7022
+ // `AgentHookProperties.hook_status` (now @deprecated) as the permanent status
7023
+ // axis, which is the *_status shadow Pattern D collapsed fourteen times.
7024
+ fromTemplate("agent_skill", OPERATIONAL_TEMPLATE),
7025
+ fromTemplate("agent_hook", OPERATIONAL_TEMPLATE),
6914
7026
  fromTemplate("on_call_rotation", OPERATIONAL_TEMPLATE),
6915
7027
  fromTemplate("security_audit", OPERATIONAL_TEMPLATE),
6916
7028
  fromTemplate("penetration_test", OPERATIONAL_TEMPLATE),
@@ -6939,8 +7051,8 @@ var TEMPLATE_LIFECYCLES = [
6939
7051
  fromTemplate("infrastructure_component", MATURITY_TEMPLATE),
6940
7052
  // ── Phase C: WORK_ITEM (todo / in_progress / in_review / done) ──────────────
6941
7053
  fromTemplate("agent_task", WORK_ITEM_TEMPLATE),
6942
- fromTemplate("agent_skill", WORK_ITEM_TEMPLATE),
6943
- fromTemplate("agent_hook", WORK_ITEM_TEMPLATE),
7054
+ // agent_skill and agent_hook moved WORK_ITEM -> OPERATIONAL at 0.33.0; see the
7055
+ // OPERATIONAL block below. agent_task stays: a task IS a work item.
6944
7056
  // UPG-690 (0.21.0): task (exact match) + epic (todo→in_progress→done ⊆ WORK_ITEM,
6945
7057
  // in_review becomes available — non-breaking widening) folded off hand-authored.
6946
7058
  fromTemplate("task", WORK_ITEM_TEMPLATE),
@@ -10005,6 +10117,16 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
10005
10117
  return result;
10006
10118
  }
10007
10119
  var UPG_EDGE_MIGRATIONS = {
10120
+ "0.33.0": [
10121
+ // G1 — the project membership edge widens from epic to the work-item set.
10122
+ // Same construction as the 0.32.0 cadence rename directly below, and for the
10123
+ // same measured reason: the adapters default an unrecognised issue to `task`,
10124
+ // so the type a project most needed to deliver was the one type it could not
10125
+ // reach. No requires_target_type, because the destination's target_type is
10126
+ // the `node` wildcard and endpoint guards check the catalog's declared type
10127
+ // rather than the concrete instance. Gate only on the unwidened source.
10128
+ { 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." }
10129
+ ],
10008
10130
  "0.32.0": [
10009
10131
  // C5 — the cadence scheduling edge widens from user_story to the work-item
10010
10132
  // set. No requires_target_type: the destination's target_type is the `node`
@@ -10823,6 +10945,41 @@ function migrateEdge(edge, fromVersion, toVersion, endpoints) {
10823
10945
  // src/grammar/status-migrations.ts
10824
10946
  var UPG_STATUS_MIGRATIONS = {
10825
10947
  // ── Engineering / Operations ─────────────────────────────────────────────
10948
+ // ── Agent automation: WORK_ITEM -> OPERATIONAL (0.33.0) ──────────────────
10949
+ // agent_skill and agent_hook carried WORK_ITEM until 0.33.0, so every graph
10950
+ // written before it holds WORK_ITEM phase ids against an OPERATIONAL
10951
+ // lifecycle. Measured population at release: 4 nodes, all `todo`. The other
10952
+ // five entries cover the phases those graphs could legally have reached.
10953
+ //
10954
+ // in_review maps to `active` rather than to a review phase because OPERATIONAL
10955
+ // has none: a hook awaiting review is still a hook that is running or about to.
10956
+ // cancelled maps to `sunset` rather than `completed`: OPERATIONAL has no
10957
+ // cancelled bucket and a discontinued trigger is wound down, not finished.
10958
+ agent_skill: {
10959
+ todo: "planning",
10960
+ backlog: "planning",
10961
+ in_progress: "active",
10962
+ in_review: "active",
10963
+ done: "completed",
10964
+ cancelled: "sunset"
10965
+ },
10966
+ agent_hook: {
10967
+ todo: "planning",
10968
+ backlog: "planning",
10969
+ in_progress: "active",
10970
+ in_review: "active",
10971
+ done: "completed",
10972
+ cancelled: "sunset",
10973
+ // The AgentHookProperties.hook_status vocabulary, @deprecated at 0.33.0 as a
10974
+ // *_status shadow of the base `status`. `error` maps to `paused` and NOT to a
10975
+ // failure phase: OPERATIONAL has none, and runtime health is store or
10976
+ // telemetry state rather than a fact about the thing. That is the same cut
10977
+ // that kept composition.rev, which is a fact, and excluded a concurrency
10978
+ // token, which is not. The failure detail belongs in `description` or a
10979
+ // diagnostic property.
10980
+ disabled: "paused",
10981
+ error: "paused"
10982
+ },
10826
10983
  service: {
10827
10984
  // Lifecycle: [development, staging, production, deprecated]
10828
10985
  // Authoring habit from before lifecycle adoption: every long-lived
@@ -11272,7 +11429,7 @@ var UPG_PROPERTY_SCHEMA = {
11272
11429
  agent_hook: {
11273
11430
  hook_event: { type: "string", description: "Event that triggers this hook" },
11274
11431
  hook_action: { type: "string", description: "Action performed when the hook fires" },
11275
- hook_status: { type: "string", enum: ["active", "disabled", "error"], description: "Operational status of the hook" },
11432
+ 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." },
11276
11433
  execution_count: { type: "number", description: "Number of times this hook has fired", modifier: "snapshot" }
11277
11434
  },
11278
11435
  // AgentSessionProperties: Agent session.
@@ -11809,12 +11966,12 @@ var UPG_PROPERTY_SCHEMA = {
11809
11966
  },
11810
11967
  // CompositionProperties
11811
11968
  composition: {
11969
+ 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." },
11970
+ presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
11812
11971
  members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
11813
11972
  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." },
11814
11973
  published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
11815
- published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." },
11816
- 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." },
11817
- presentation: { type: "object", description: "Advisory rendering intent for the composition as a whole. A consumer may ignore it entirely and still be conformant." }
11974
+ published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
11818
11975
  },
11819
11976
  // ConfigurationAxisProperties: A named dimension along which the product's composition differs.
11820
11977
  configuration_axis: {
@@ -12077,7 +12234,7 @@ var UPG_PROPERTY_SCHEMA = {
12077
12234
  budget: { type: "number", description: "Allocated budget for this program" },
12078
12235
  target_leads: { type: "number", description: "Target number of leads to generate" }
12079
12236
  },
12080
- // DepartmentProperties: Department entity.
12237
+ // DepartmentProperties: Department entity. A department is the single org tier above a team.
12081
12238
  department: {
12082
12239
  headcount: { type: "number", description: "Total number of people in the department", modifier: "snapshot" },
12083
12240
  budget: { type: "number", description: "Annual budget allocated to the department" },
@@ -13231,7 +13388,7 @@ var UPG_PROPERTY_SCHEMA = {
13231
13388
  },
13232
13389
  // ObjectiveProperties: A high-level strategic goal. The O in OKR.
13233
13390
  objective: {
13234
- timeframe: { type: "string", description: 'Planning timeframe (e.g. "Q1 2026", "H1 2026")' },
13391
+ 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"' },
13235
13392
  progress: { type: "number", description: "Overall progress (0\u2013100)" }
13236
13393
  },
13237
13394
  // ObservationProperties: Discrete observation captured during research. Absorbed from the deprecated Highlight entity.
@@ -13452,7 +13609,7 @@ var UPG_PROPERTY_SCHEMA = {
13452
13609
  logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
13453
13610
  launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
13454
13611
  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." },
13455
- 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.' }
13612
+ 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." }
13456
13613
  },
13457
13614
  // ProductAreaProperties: ProductArea entity.
13458
13615
  product_area: {
@@ -13941,7 +14098,7 @@ var UPG_PROPERTY_SCHEMA = {
13941
14098
  // StrategicThemeProperties: StrategicTheme entity.
13942
14099
  strategic_theme: {
13943
14100
  owner: { type: "string", description: "Owning person or team. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
13944
- 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"' },
14101
+ 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"' },
13945
14102
  description: { type: "string", description: "Short narrative of this time-bound thrust within its pillar" },
13946
14103
  scope: { type: "string", description: "What the theme explicitly includes or excludes" }
13947
14104
  },
@@ -14060,7 +14217,8 @@ var UPG_PROPERTY_SCHEMA = {
14060
14217
  team: {
14061
14218
  team_type: { type: "string", enum: ["product", "engineering", "design", "growth", "customer_success"], description: "Functional area of the team" },
14062
14219
  size: { type: "number", description: "Number of people on the team" },
14063
- mission: { type: "string", description: "Team's mission statement" }
14220
+ mission: { type: "string", description: "Team's mission statement" },
14221
+ 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`." }
14064
14222
  },
14065
14223
  // TeamOkrProperties: TeamOkr entity.
14066
14224
  team_okr: {
@@ -14386,6 +14544,8 @@ var UPG_PROPERTY_SCHEMA = {
14386
14544
  },
14387
14545
  // WorkspaceProperties: Workspace: a spatial thinking space for arranging entities.
14388
14546
  workspace: {
14547
+ 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." },
14548
+ presentation: { type: "object", description: "Advisory rendering intent for the layer as a whole. A consumer may ignore it entirely and still be conformant." },
14389
14549
  visibility: { type: "string", enum: ["private", "shared", "public"], description: "Who can see this workspace" },
14390
14550
  purpose: { type: "string", description: "Free-text description. Pairs with the closed-enum `workspace_purpose`." },
14391
14551
  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." },
@@ -32591,6 +32751,9 @@ var NODE_KEY_ORDER = [
32591
32751
  "external_tool",
32592
32752
  "external_ref",
32593
32753
  "external_id",
32754
+ "external_links",
32755
+ "created_at",
32756
+ "updated_at",
32594
32757
  "sort_order",
32595
32758
  "properties"
32596
32759
  ];
@@ -32873,7 +33036,7 @@ function checkHeaderSealText(text) {
32873
33036
  }
32874
33037
 
32875
33038
  // src/index.ts
32876
- var UPG_VERSION = "0.32.2";
33039
+ var UPG_VERSION = "0.33.1";
32877
33040
  var UPG_FORMAT_VERSION = "0.4.0";
32878
33041
  var MARKDOWN_FORMAT_VERSION = "0.1";
32879
33042
  var UPG_TYPES = getTypes();
@@ -32958,6 +33121,7 @@ export {
32958
33121
  LEGACY_PRODUCT_STAGES,
32959
33122
  MARKDOWN_FORMAT_VERSION,
32960
33123
  MARKET_COMPETITIVE_PLAYBOOK,
33124
+ NODE_KEY_ORDER,
32961
33125
  OPERATIONS_QUALITY_PLAYBOOK,
32962
33126
  PRESENT_UNDER_PROPERTY,
32963
33127
  PRODUCT_DELIVERY_PLAYBOOK,