@unified-product-graph/core 0.31.0 → 0.32.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/dist/index.cjs CHANGED
@@ -775,7 +775,7 @@ var UPG_DOMAINS = [
775
775
  id: "workspace",
776
776
  label: "Workspace",
777
777
  description: "Spatial thinking spaces for arranging entities, debating decisions, and committing to the graph. Workspaces are transient canvases that sit alongside all other domains, letting you compose and explore relationships before they become permanent graph structure. A framework exercise is a structured workspace: one run of a framework (MoSCoW, RICE, Kano, \u2026) applied to a chosen set of entities, with each entity's result recorded on the exercise-to-entity edge rather than the entity itself. A composition is the durable other half: a named, published view assembled from a canvas, with a stable slug people link to and a revision history, whose frozen arrangement holds pointers rather than copied content.",
778
- types: ["workspace", "framework_exercise", "composition"]
778
+ types: ["workspace", "framework_exercise", "composition", "capture"]
779
779
  },
780
780
  {
781
781
  id: "foundations",
@@ -1263,7 +1263,10 @@ var UPG_ENTITY_META = [
1263
1263
  // ── Workspace ──
1264
1264
  { name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" },
1265
1265
  { name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" },
1266
- { name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" }
1266
+ { name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" },
1267
+ // A dated, hashed rendition of a node that is already in the graph. Enters
1268
+ // `proposed` per the maturity rubric (precedent: composition 0.31.0).
1269
+ { name: "capture", type_id: "ent_362", maturity: "proposed", since: "0.32.0" }
1267
1270
  ];
1268
1271
  var DEFERRED_PROPOSED_BY_CONTRACT = {
1269
1272
  // user_story (UPG-571) and experiment_run (UPG-664) graduated to stable in
@@ -1551,6 +1554,24 @@ var UPG_EDGE_CATALOG = {
1551
1554
  // the catalog's singular-source/target grammar (polymorphic targets
1552
1555
  // would be a separate catalog-mechanic change).
1553
1556
  competitive_analysis_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "competitive_analysis", target_type: "classification_axis" },
1557
+ // A product's own classification axes (0.32.0). Before this the only parent a
1558
+ // classification_axis could have was a competitive_analysis, so a taxonomy the
1559
+ // product uses on its OWN entities — the grouped-label case, one named group
1560
+ // over a set of values — had to hang off a competitive analysis it has nothing
1561
+ // to do with, or float parentless.
1562
+ //
1563
+ // Hierarchy, so the axis has a home in get_tree, on the same honest verb the
1564
+ // competitive_analysis edge already uses; and it earns its
1565
+ // UPG_VALID_CHILDREN.product pair the same way product_defines_configuration_axis
1566
+ // did at 0.30.0 — axes are few per product, and visibility of a mechanism
1567
+ // matters more than the tree cost.
1568
+ //
1569
+ // This is what makes one level of label grouping expressible with NO new label
1570
+ // surface: the group is the axis, the labels are its values, and carrying one
1571
+ // is node_classified_as_classification_value, all of it stable since 0.4.0.
1572
+ // Ungrouped freeform labels stay in `tags`. The line between them: if the
1573
+ // group has a name a person would filter by, it is an axis; otherwise a tag.
1574
+ product_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "product", target_type: "classification_axis" },
1554
1575
  classification_axis_includes_classification_value: { forward_verb: "includes", reverse_verb: "value_of", classification: "hierarchy", source_type: "classification_axis", target_type: "classification_value" },
1555
1576
  // 0.10.2: also dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so a
1556
1577
  // competitor can be classified directly against a `registry/{classification_value}`
@@ -1790,10 +1811,20 @@ var UPG_EDGE_CATALOG = {
1790
1811
  // iterations; a cycle contains its cooldown), mirroring team_contains_team.
1791
1812
  planning_cycle_contains_planning_cycle: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "planning_cycle", target_type: "planning_cycle" },
1792
1813
  // E3: scheduling work into a cycle is a deliberate authoring act, not a
1793
- // containment nesting — user_story keeps its feature/epic parent and is merely
1794
- // referenced by the cycle. deliberate_only so the generic-inference chokepoints
1795
- // never auto-materialise it.
1796
- planning_cycle_schedules_user_story: { forward_verb: "schedules", reverse_verb: "scheduled_in", classification: "semantic", source_type: "planning_cycle", target_type: "user_story", deliberate_only: true },
1814
+ // containment nesting — the work item keeps its feature/epic parent and is
1815
+ // merely referenced by the cycle. deliberate_only so the generic-inference
1816
+ // chokepoints never auto-materialise it.
1817
+ //
1818
+ // WIDENED AT 0.32.0, renamed from planning_cycle_schedules_user_story. The
1819
+ // story-only endpoint could not hold a real tracker import: the adapters
1820
+ // default an unrecognised issue to `task`, so the type a cycle most needed to
1821
+ // schedule was the one type it could not reach. Endpoint-polymorphic over the
1822
+ // work-item set {feature, epic, user_story, task, bug} by the same
1823
+ // construction as work_item_blocks_work_item in the same 0.20.0 batch — the
1824
+ // `work_item` token names the intended semantic domain, the endpoint is the
1825
+ // `node` wildcard. Three typed edges were the alternative and would have
1826
+ // re-opened a decision this vocabulary already took for the same entity set.
1827
+ planning_cycle_schedules_work_item: { forward_verb: "schedules", reverse_verb: "scheduled_in", classification: "semantic", source_type: "planning_cycle", target_type: "node", deliberate_only: true },
1797
1828
  // E2: the OKR cycle-scoping anchor. objective -> planning_cycle. planning_cycle
1798
1829
  // is portfolio_shared, so this passes the cross-scope gate and ships as
1799
1830
  // PROVISIONAL (allowed cross-product with a write-time warning, self-maintaining,
@@ -2918,6 +2949,16 @@ var UPG_EDGE_CATALOG = {
2918
2949
  // NOT cross_product_eligible: a portfolio-level composition spanning several
2919
2950
  // product graphs is conceivable but unevidenced. Same YAGNI call as above.
2920
2951
  composition_focuses_node: { forward_verb: "focuses", reverse_verb: "focused_in", classification: "cross-domain", source_type: "composition", target_type: "node" },
2952
+ // What a capture is a picture OF (0.32.0). Polymorphic because anything in the
2953
+ // graph can be rendered — a surface, a screen, a report, a canvas — and being
2954
+ // captured carries no structural role, so the endpoint collapses to the
2955
+ // wildcard rather than enumerating a list that would never finish.
2956
+ //
2957
+ // This edge is also where the preservation-is-not-permission rule lands for
2958
+ // renditions: a consumer renders the captures the GRAPH says exist, never the
2959
+ // ones some tool's opaque bag still remembers. A capture that was deleted is a
2960
+ // node that was deleted, and the spec needs no second deletion vocabulary.
2961
+ capture_renders_node: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "cross-domain", source_type: "capture", target_type: "node" },
2921
2962
  // ── P14 edges (replacing foreign-key properties) ─────────────────────────
2922
2963
  feature_addresses_job: { forward_verb: "addresses", reverse_verb: "addressed_by", classification: "cross-domain", source_type: "feature", target_type: "job" },
2923
2964
  feature_drives_key_result: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature", target_type: "key_result" },
@@ -3034,6 +3075,23 @@ var UPG_EDGE_CATALOG = {
3034
3075
  node_owned_by_stakeholder: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "stakeholder" },
3035
3076
  node_owned_by_department: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "department", cross_product_eligible: true },
3036
3077
  node_owned_by_person: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "person" },
3078
+ // ── Cross-domain: Assignment (0.32.0) ───────────────────────────────────────
3079
+ // Assignment is NOT ownership, and 0.12.0 ruled otherwise. That ruling routed
3080
+ // task.assignee and bug.assignee into node_owned_by_person, and it is
3081
+ // superseded here rather than quietly widened, because the two differ in a way
3082
+ // the field measures.
3083
+ //
3084
+ // ASSIGNMENT HAS A TIME INTERVAL AND OWNERSHIP DOES NOT. A shipped ops plane
3085
+ // models assignment as a claim carrying claimed-at, released-at and a partial
3086
+ // unique index enforcing one live claim; nothing in the spec gives ownership
3087
+ // that shape. And a real 184-item board ran with 157 items (85%) unassigned
3088
+ // while every one of them was owned in the ordinary sense. One edge meaning
3089
+ // both cannot express "owned by the team, assigned to nobody", which is the
3090
+ // modal state of a working board.
3091
+ //
3092
+ // Not cross_product_eligible, matching node_owned_by_person: an assignee is a
3093
+ // person in this graph's own roster.
3094
+ node_assigned_to_person: { forward_verb: "assigned_to", reverse_verb: "assigned", classification: "cross-domain", source_type: "node", target_type: "person" },
3037
3095
  // ── Cross-domain: Architecture (DDD) ─────────────────────────────────────────
3038
3096
  // Service, domain_event, domain_entity, aggregate, read_model, api_contract,
3039
3097
  // value_object, command, and data_model are DDD building blocks that all
@@ -3978,7 +4036,18 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
3978
4036
  // onto a canvas or shown in a published view, and neither carries a
3979
4037
  // structural role, so both collapse polymorphic rather than enumerating.
3980
4038
  "workspace_arranges_node",
3981
- "composition_focuses_node"
4039
+ "composition_focuses_node",
4040
+ // Universal assignment (0.32.0): who is working on this, as distinct from who
4041
+ // owns it. Wildcard source for the same reason node_owned_by_person has one.
4042
+ "node_assigned_to_person",
4043
+ // Capture subject (0.32.0): anything in the graph can be rendered, and being
4044
+ // rendered carries no structural role.
4045
+ "capture_renders_node",
4046
+ // Cadence scheduling (0.32.0, widened from planning_cycle_schedules_user_story):
4047
+ // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
4048
+ // bug}. The `work_item` key token names the intended semantic domain; the
4049
+ // endpoint is the `node` wildcard.
4050
+ "planning_cycle_schedules_work_item"
3982
4051
  ];
3983
4052
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
3984
4053
  function isPolymorphicEdge(key) {
@@ -4173,6 +4242,11 @@ var UPG_VALID_CHILDREN = {
4173
4242
  // the product renders. Few per product, and top-level because an axis is a
4174
4243
  // product-wide fact rather than a child of any one feature or surface.
4175
4244
  "configuration_axis",
4245
+ // Classification (0.32.0): the product's own taxonomies — the grouped-label
4246
+ // case, one named group over a set of values. Same reasoning as the
4247
+ // configuration axis above, and until now the only parent a
4248
+ // classification_axis could have was a competitive_analysis.
4249
+ "classification_axis",
4176
4250
  // Engineering
4177
4251
  "bounded_context",
4178
4252
  "code_repository",
@@ -4457,7 +4531,7 @@ var UPG_VALID_CHILDREN = {
4457
4531
  // contains iterations; a cycle contains its cooldown) via
4458
4532
  // planning_cycle_contains_planning_cycle. It does NOT contain user_story as a
4459
4533
  // child: scheduling work into a cycle is the semantic, deliberate-only
4460
- // planning_cycle_schedules_user_story edge, so a story keeps its feature/epic
4534
+ // planning_cycle_schedules_work_item edge, so the item keeps its feature/epic
4461
4535
  // containment parent and is merely referenced by the cycle.
4462
4536
  planning_cycle: ["planning_cycle"],
4463
4537
  // ── Engineering hierarchy ───────────────────────────────────────────────────
@@ -4803,7 +4877,12 @@ var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
4803
4877
  // part of the product, it is a lens onto one. This is also what keeps the
4804
4878
  // type to two edges rather than three: no provenance edge is minted (that is
4805
4879
  // `workspace_produced_node`, reused) and no parent edge is needed.
4806
- "composition"
4880
+ "composition",
4881
+ // A capture has no structural parent for the same reason: it anchors to what
4882
+ // it RENDERS, via `capture_renders_node`. Giving it a product parent would
4883
+ // claim a capture is part of the product rather than a picture of one, and
4884
+ // would mint a containment edge whose only job is to satisfy the hierarchy.
4885
+ "capture"
4807
4886
  ]);
4808
4887
  function isContainmentFreeType(entityType) {
4809
4888
  return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
@@ -6909,7 +6988,8 @@ var WORK_ITEM_TEMPLATE = {
6909
6988
  initial_phase: "todo",
6910
6989
  terminal_phases: ["done", "cancelled"],
6911
6990
  phases: [
6912
- { id: "todo", status_category: "unstarted", label: "To Do", description: "Identified but not yet started.", transitions_to: ["in_progress", "cancelled"] },
6991
+ { id: "backlog", status_category: "backlog", label: "Backlog", description: "Accepted or set aside deliberately: real work, not scheduled. Distinct from `todo`, which is committed and merely not started.", transitions_to: ["todo", "in_progress", "cancelled"] },
6992
+ { id: "todo", status_category: "unstarted", label: "To Do", description: "Identified but not yet started.", transitions_to: ["in_progress", "backlog", "cancelled"] },
6913
6993
  { id: "in_progress", status_category: "started", label: "In Progress", description: "Actively being worked on.", transitions_to: ["in_review", "todo", "cancelled"] },
6914
6994
  { id: "in_review", status_category: "started", label: "In Review", description: "Work completed, awaiting review or acceptance.", transitions_to: ["done", "in_progress", "cancelled"] },
6915
6995
  { id: "done", status_category: "completed", label: "Done", description: "Completed and accepted.", transitions_to: [] },
@@ -7540,7 +7620,11 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
7540
7620
  // from experiment_runs (which have lifecycles); hypothesis_evidence was
7541
7621
  // lifecycle-free here but is deprecated at v0.4.0 ──────────────────────────────────────
7542
7622
  "evidence",
7543
- "learning"
7623
+ "learning",
7624
+ // ── Workspace (1 of 4): a capture is a fact about a moment, not a workflow.
7625
+ // It is taken, and from then on it either still matches its subject or it
7626
+ // does not — which is what `content_hash` answers, not a phase. ─────────
7627
+ "capture"
7544
7628
  ]);
7545
7629
  function isLifecycleFreeType(entityType) {
7546
7630
  return UPG_LIFECYCLE_FREE_TYPES.has(entityType);
@@ -8444,6 +8528,46 @@ function getDeprecatedTypes() {
8444
8528
  return deprecated;
8445
8529
  }
8446
8530
  var UPG_PROPERTY_MIGRATIONS = {
8531
+ "0.32.0": [
8532
+ // C3 — archive becomes a base-node axis. WorkspaceProperties.archived /
8533
+ // archived_at shipped the orthogonal-archive idea for exactly one type;
8534
+ // field data showed it is general (a tracker held 559 archived-completed
8535
+ // items beside 18 live-completed ones), so the pair moves to UPGBaseNode
8536
+ // and the workspace properties are @deprecated. Lift, not drop: the value
8537
+ // is real and its new home means the same thing.
8538
+ {
8539
+ kind: "lift_property_to_top_level",
8540
+ type: "workspace",
8541
+ from_property: "archived",
8542
+ to: "archived",
8543
+ reason: "0.32.0: archive generalised from WorkspaceProperties to UPGBaseNode.archived. Same fact, wider home; the workspace property is @deprecated."
8544
+ },
8545
+ {
8546
+ kind: "lift_property_to_top_level",
8547
+ type: "workspace",
8548
+ from_property: "archived_at",
8549
+ to: "archived_at",
8550
+ reason: "0.32.0: pairs with the archived lift above. UPGBaseNode.archived_at is the canonical home."
8551
+ },
8552
+ // C4 — the duplicate label surfaces. TaskProperties.labels and
8553
+ // BugProperties.labels were declared "applied uniformly across work item
8554
+ // types" and duplicated base-node `tags` with no consumer anywhere. Three
8555
+ // label surfaces (base tags, per-type tags, per-type labels) is two too
8556
+ // many; grouped labels are classification_axis + classification_value, and
8557
+ // ungrouped ones are `tags`.
8558
+ {
8559
+ kind: "drop_props",
8560
+ type: "task",
8561
+ drop_props: ["labels"],
8562
+ reason: "0.32.0: TaskProperties.labels duplicated base-node `tags` and had no consumers. Freeform labels live in `tags`; grouped labels are classification_axis + node_classified_as_classification_value."
8563
+ },
8564
+ {
8565
+ kind: "drop_props",
8566
+ type: "bug",
8567
+ drop_props: ["labels"],
8568
+ reason: "0.32.0: BugProperties.labels duplicated base-node `tags` and had no consumers. Same resolution as task."
8569
+ }
8570
+ ],
8447
8571
  // ── v0.21.0: UPG-690 D.1 — collapse residual *_status shadows into status ────
8448
8572
  //
8449
8573
  // Six `*_status` enum properties that re-encode their entity's own lifecycle
@@ -9579,6 +9703,48 @@ function getSplitMigrations(fromVersion, toVersion) {
9579
9703
  return result;
9580
9704
  }
9581
9705
  var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
9706
+ "0.32.0": [
9707
+ // C2 — assignment gets its own edge, superseding the 0.12.0 rules that
9708
+ // routed task/bug `assignee` into node_owned_by_person.
9709
+ //
9710
+ // WHY THIS IS A REVERSAL AND NOT A GAP-FILL. 0.12.0 ruled that an assignee
9711
+ // is an owner. Assignment has a time interval and an exclusivity that
9712
+ // ownership does not, and a real board ran 85% unassigned while every item
9713
+ // was owned — a state one edge cannot express.
9714
+ //
9715
+ // WHY EXISTING EDGES ARE NOT REWRITTEN. A node_owned_by_person edge on a
9716
+ // task is indistinguishable by inspection from a genuine ownership edge, so
9717
+ // a blanket rename would silently reclassify real facts. Measured population
9718
+ // across every graph in the originating workspace was ~zero, which makes the
9719
+ // cost of leaving them nil and the cost of guessing a wrong fact.
9720
+ //
9721
+ // user_story is included here and was MISSING from 0.12.0 entirely, though
9722
+ // it has carried `assignee` since 0.20.0.
9723
+ {
9724
+ from_type: "task",
9725
+ scalar_property: "assignee",
9726
+ target_type: "person",
9727
+ edge_type: "node_assigned_to_person",
9728
+ drop_scalar: false,
9729
+ reason: 'P14 actor: the assignee is a first-class person ("every task assigned to this person"). Assignment is not ownership: it has an interval and an exclusivity that ownership lacks, so it links node_assigned_to_person, superseding the 0.12.0 node_owned_by_person rule. Display string kept.'
9730
+ },
9731
+ {
9732
+ from_type: "bug",
9733
+ scalar_property: "assignee",
9734
+ target_type: "person",
9735
+ edge_type: "node_assigned_to_person",
9736
+ drop_scalar: false,
9737
+ reason: "P14 actor: same as task. Supersedes the 0.12.0 node_owned_by_person rule. Display string kept."
9738
+ },
9739
+ {
9740
+ from_type: "user_story",
9741
+ scalar_property: "assignee",
9742
+ target_type: "person",
9743
+ edge_type: "node_assigned_to_person",
9744
+ drop_scalar: false,
9745
+ reason: "P14 actor: user_story has carried `assignee` since 0.20.0 and had no promotion rule at all; an asymmetry with task and bug that this closes. Display string kept."
9746
+ }
9747
+ ],
9582
9748
  "0.12.0": [
9583
9749
  // ── Bucket A1 — orphan scalars (no edge existed): add edge + migrate + drop ──
9584
9750
  {
@@ -9989,22 +10155,12 @@ var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
9989
10155
  drop_scalar: false,
9990
10156
  reason: "P14 actor: the release owner is a first-class person; link node_owned_by_person. Display string kept."
9991
10157
  },
9992
- {
9993
- from_type: "task",
9994
- scalar_property: "assignee",
9995
- target_type: "person",
9996
- edge_type: "node_owned_by_person",
9997
- drop_scalar: false,
9998
- reason: 'P14 actor: the assignee is a first-class person ("every task assigned to this person"); link node_owned_by_person. Display string kept.'
9999
- },
10000
- {
10001
- from_type: "bug",
10002
- scalar_property: "assignee",
10003
- target_type: "person",
10004
- edge_type: "node_owned_by_person",
10005
- drop_scalar: false,
10006
- reason: "P14 actor: the assignee is a first-class person; link node_owned_by_person. Display string kept."
10007
- },
10158
+ // NOTE (0.32.0): the task/bug `assignee` rules that lived here are SUPERSEDED
10159
+ // by the '0.32.0' block below, which routes them to node_assigned_to_person.
10160
+ // They are removed rather than left to double-run: two rules over one scalar
10161
+ // would mint the person once and link it twice, under two verbs that now mean
10162
+ // different things. Graphs promoted before 0.32.0 keep the ownership edges
10163
+ // they were given — see the 0.32.0 block for why those are not rewritten.
10008
10164
  {
10009
10165
  from_type: "roadmap",
10010
10166
  scalar_property: "owner",
@@ -10100,6 +10256,14 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
10100
10256
  return result;
10101
10257
  }
10102
10258
  var UPG_EDGE_MIGRATIONS = {
10259
+ "0.32.0": [
10260
+ // C5 — the cadence scheduling edge widens from user_story to the work-item
10261
+ // set. No requires_target_type: the destination's target_type is the `node`
10262
+ // wildcard, and endpoint guards are checked against the catalog's declared
10263
+ // type rather than the concrete instance (same reasoning as the 0.22.0
10264
+ // workspace_produced_node rename below). Gate only on the unwidened source.
10265
+ { kind: "rename", from: "planning_cycle_schedules_user_story", to: "planning_cycle_schedules_work_item", requires_source_type: "planning_cycle", reason: "0.32.0: a cycle could only schedule a user_story, while tracker imports produce `task` by default; the type a cadence most needed to hold was the one it could not reach. Widened to the polymorphic work-item endpoint; no flip, source unchanged. Measured live population of the old type at release: zero (no .upg in the repo or the dogfood graph carried it), so this rename is a vocabulary change, not a data migration." }
10266
+ ],
10103
10267
  // (target version TBD at release ceremony — pencilled at 0.22.0, the next
10104
10268
  // breaking slot after 0.21.0 per the docket; adjust the key if Wave 4
10105
10269
  // claims it first.) WS3 commit-provenance collapse (2026-07-05, ratified):
@@ -11653,9 +11817,9 @@ var UPG_PROPERTY_SCHEMA = {
11653
11817
  priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Urgency relative to other work. Independent of `bug_severity` (a critical bug can have low priority if rare)." },
11654
11818
  assignee: { type: "string", description: "Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
11655
11819
  due_date: { type: "string", description: "ISO date due. Often tied to a release gate or SLA." },
11656
- labels: { type: "string[]", description: "Free-form classification tags. Applied uniformly across work item types." },
11820
+ labels: { type: "string[]", description: "Free-form classification tags. @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and `classification_axis` + `classification_value` + `node_classified_as_classification_value` when the labels belong to a named group. This field duplicated `tags` and had no consumers; three parallel label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it." },
11657
11821
  workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
11658
- workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning (a source "In Review" and "QA" might both map to a verification phase). Optional companion to `workflow_state`: it lets a graph reason over an imported custom workflow without promoting the raw label to `status`.' }
11822
+ workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
11659
11823
  },
11660
11824
  // BuildArtifactProperties: Build artifact.
11661
11825
  build_artifact: {
@@ -11686,6 +11850,16 @@ var UPG_PROPERTY_SCHEMA = {
11686
11850
  allocated: { type: "number", description: "Capacity already allocated to work" },
11687
11851
  available: { type: "number", description: "Remaining unallocated capacity" }
11688
11852
  },
11853
+ // CaptureProperties: Capture: a dated, hashed rendition of something already in the graph.
11854
+ capture: {
11855
+ capture_uri: { type: "string", description: "Where the bytes are. `https://` for hosted, `file://` or a relative path for local. Required: a capture with no location renders nothing." },
11856
+ content_hash: { type: "string", description: "Content hash of the bytes. The regeneration signal; see the type's `@remarks` for why mtime and size are not." },
11857
+ hash_algorithm: { type: "string", enum: ["sha256", "sha1", "md5"], description: "Hash algorithm. Absent means `sha256`." },
11858
+ captured_at: { type: "string", description: "ISO timestamp the capture was taken." },
11859
+ media_type: { type: "string", description: 'IANA media type of the bytes. @example "image/png"' },
11860
+ fidelity: { type: "string", enum: ["exact", "approximate", "not_applicable"], description: "How faithfully this rendition represents its subject. `approximate` covers a capture taken in a stand-in state or at the wrong viewport." },
11861
+ capture_status: { type: "string", enum: ["captured", "blocked", "skipped"], description: "Whether the capture succeeded.", notes: "A `blocked` capture is a real record rather than a missing one: it says the subject exists and could not be rendered, which is what stops the next run rediscovering the same obstacle. `skipped` is the deliberate exclusion." }
11862
+ },
11689
11863
  // CeremonyProperties: Ceremony entity.
11690
11864
  ceremony: {
11691
11865
  ceremony_type: { type: "string", enum: ["standup", "planning", "review", "retro", "sync", "demo", "other"], description: "Kind of recurring meeting" },
@@ -11889,7 +12063,9 @@ var UPG_PROPERTY_SCHEMA = {
11889
12063
  members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
11890
12064
  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." },
11891
12065
  published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
11892
- published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
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." }
11893
12069
  },
11894
12070
  // ConfigurationAxisProperties: A named dimension along which the product's composition differs.
11895
12071
  configuration_axis: {
@@ -12346,7 +12522,7 @@ var UPG_PROPERTY_SCHEMA = {
12346
12522
  start_date: { type: "string", description: "ISO date work begins" },
12347
12523
  target_date: { type: "string", description: "ISO date work completes" },
12348
12524
  workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
12349
- workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning (a source "In Review" and "QA" might both map to a verification phase). Optional companion to `workflow_state`: it lets a graph reason over an imported custom workflow without promoting the raw label to `status`.' }
12525
+ workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
12350
12526
  },
12351
12527
  // ErrorBudgetProperties: Error budget.
12352
12528
  error_budget: {
@@ -12530,7 +12706,7 @@ var UPG_PROPERTY_SCHEMA = {
12530
12706
  target_date: { type: "string", description: "ISO date work completes" },
12531
12707
  health: { type: "string", enum: ["on_track", "at_risk", "off_track"], description: "Delivery health" },
12532
12708
  workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
12533
- workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning (a source "In Review" and "QA" might both map to a verification phase). Optional companion to `workflow_state`: it lets a graph reason over an imported custom workflow without promoting the raw label to `status`.' }
12709
+ workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
12534
12710
  },
12535
12711
  // FeatureAreaProperties: A structural grouping of related features within a product.
12536
12712
  feature_area: {
@@ -13526,7 +13702,8 @@ var UPG_PROPERTY_SCHEMA = {
13526
13702
  url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
13527
13703
  logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
13528
13704
  launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
13529
- 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." }
13705
+ 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`, \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.' }
13530
13707
  },
13531
13708
  // ProductAreaProperties: ProductArea entity.
13532
13709
  product_area: {
@@ -14126,9 +14303,9 @@ var UPG_PROPERTY_SCHEMA = {
14126
14303
  effort: { type: "string", description: 'Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team.' },
14127
14304
  priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Relative importance against other tasks" },
14128
14305
  due_date: { type: "string", description: "ISO date due. Typically bounded by the containing story's due date." },
14129
- labels: { type: "string[]", description: "Free-form classification tags. Applied uniformly across work item types." },
14306
+ labels: { type: "string[]", description: "Free-form classification tags. @deprecated since 0.32.0. Use base-node `tags` for ungrouped labels, and `classification_axis` + `classification_value` + `node_classified_as_classification_value` when the labels belong to a named group. This field duplicated `tags` and had no consumers; three parallel label surfaces (base `tags`, per-type `tags`, per-type `labels`) were two too many. `UPG_PROPERTY_MIGRATIONS['0.32.0']` drops it." },
14130
14307
  workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
14131
- workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning (a source "In Review" and "QA" might both map to a verification phase). Optional companion to `workflow_state`: it lets a graph reason over an imported custom workflow without promoting the raw label to `status`.' }
14308
+ workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
14132
14309
  },
14133
14310
  // TeamProperties: Team entity.
14134
14311
  team: {
@@ -14350,7 +14527,7 @@ var UPG_PROPERTY_SCHEMA = {
14350
14527
  priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Relative importance against other stories. Lifted onto user_story (0.20.0) so the story is a first-class plannable unit alongside task, matching Jira/Linear where the story/issue is the estimated-and-assigned atom." },
14351
14528
  due_date: { type: "string", description: "ISO date due. Typically bounded by the release or planning_cycle the story is scheduled into." },
14352
14529
  workflow_state: { type: "string", description: 'The source tool\'s raw custom workflow state, verbatim and opaque (e.g. "In Review", "QA", "Needs Triage"). Non-canonical and never reasoned over: it exists to round-trip an import losslessly. Map it onto a canonical bucket with `workflow_state_category`; canonical `status` stays the sole reasoning axis.' },
14353
- workflow_state_category: { type: "string", description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning (a source "In Review" and "QA" might both map to a verification phase). Optional companion to `workflow_state`: it lets a graph reason over an imported custom workflow without promoting the raw label to `status`.' }
14530
+ workflow_state_category: { type: "string", enum: ["triage", "backlog", "unstarted", "started", "completed", "cancelled"], description: 'Canonical bucket the raw `workflow_state` maps onto for reasoning: a source "In Review" and a source "QA" may both map to a verification phase. Optional companion to `workflow_state`; canonical `status` remains the sole reasoning axis.', notes: "It exists so a graph can reason over an imported custom workflow WITHOUT promoting the source's raw label to `status`. The raw label keeps its own field and stays verbatim; this one says what that label means in the six-bucket vocabulary every major tracker converges on. NARROWED FROM `string` AT 0.32.0. The field exists to carry exactly the vocabulary, and typing it as an open string meant nothing enforced the one thing it was for; an importer could write any word here and no consumer would know it had. A graph carrying a free string now fails to type-check rather than failing to be understood." }
14354
14531
  },
14355
14532
  // ValueObjectProperties: DDD value object.
14356
14533
  value_object: {
@@ -14465,8 +14642,8 @@ var UPG_PROPERTY_SCHEMA = {
14465
14642
  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." },
14466
14643
  owner: { type: "string", description: "Workspace owner (handle or email). Display label; canonical owner is `team_owns_workspace` or `persona_owns_workspace`." },
14467
14644
  member_count: { type: "number", description: "Snapshot count. `team_works_in_workspace` edges are the source of truth.", modifier: "derived" },
14468
- archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views." },
14469
- archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
14645
+ archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views. @deprecated since 0.32.0. Use `UPGBaseNode.archived`, which generalises this pair to every entity type. Field data showed the archived/status split is not a workspace peculiarity: a tracker held 559 archived-completed items beside 18 live-completed ones, and one status field has nowhere to put the difference. `UPG_PROPERTY_MIGRATIONS['0.32.0']` lifts this value to the top level." },
14646
+ archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`. @deprecated since 0.32.0. Use `UPGBaseNode.archived_at`. Lifted by `UPG_PROPERTY_MIGRATIONS['0.32.0']`." },
14470
14647
  icon: { type: "string", description: "Display icon (emoji or icon name)" },
14471
14648
  retention: { type: "string", enum: ["transient", "durable"], description: "Retention intent. Absent means `transient`.", notes: "A workspace is a free-form thinking space and is transient by default, so the spec makes both intents EXPRESSIBLE and takes no position on which reaches the file. Whether to write a transient canvas at all is a tool decision, and the recommended posture is not to: a scratch canvas nobody named has no business in a shared, git-tracked graph, where it lands in everyone's diff. Writing only `durable` workspaces is what keeps a canonical graph entity comfortable with a transient-by-default object." },
14472
14649
  canvas: { type: "object", description: "Opaque canvas furniture: the parts of a canvas with no graph referent. Preserved verbatim on round-trip and never interpreted. Tool extension keys are namespaced `<tool>:<key>` with a colon, and no consumer interprets a key it does not own.", notes: "The cut here is UPG principle P14 applied literally. Anything that is a REFERENCE TO A GRAPH NODE is an edge, which is why placed entities ride `workspace_arranges_node` and not this bag: a node id held as a scalar inside a blob is a foreign key in disguise, and a deleted node would leave a stale reference nothing can detect. Anything that is pure UI chrome with no graph referent stays here, because minting `annotation` and `frame` entity types would add catalog surface that is meaningless to every consumer outside the tool that drew it. WHY THE COLON, and why the rule is in the summary rather than buried here. An underscore key is indistinguishable from an ordinary property name, so a migration that targets namespaced keys cannot match it and no validator can detect one that should have been namespaced. That undetectability is why enforcement lives in the type and the documentation instead of in a check: a check would report clean on a bag full of underscore keys, which is worse than no check. Two conventions were already coexisting when this was ruled (`entopo_views` in a live writer, `entopo:view_blocks` in the contract test certifying preservation), which is how a cheap rule becomes a migration. PRESERVATION IS NOT PERMISSION TO RENDER. Preserving every byte says nothing about meaning: a field-measured canvas carried `excluded: true` tombstones, and a consumer that preserved them faithfully while rendering every entry showed the user images they had deleted." }
@@ -21870,6 +22047,7 @@ var ENTITY_EMOJI = {
21870
22047
  competitor_signal: "\u{1F4E1}",
21871
22048
  compliance_framework: "\u{1F3DF}\uFE0F",
21872
22049
  compliance_requirement: "\u26F2",
22050
+ capture: "\u{1F4F8}",
21873
22051
  composition: "\u{1F5BC}\uFE0F",
21874
22052
  constraint: "\u{1F512}",
21875
22053
  contact: "\u{1F4C7}",
@@ -25249,9 +25427,9 @@ var PRODUCT_SPEC_GUIDE = {
25249
25427
  },
25250
25428
  {
25251
25429
  name: "Planning Cadence",
25252
- description: "The cadence axis. Create a planning_cycle (a sprint, iteration, quarter, or PI), nest finer cycles inside a coarse one, schedule the stories that flow through it, and scope the objectives the cycle serves. Scheduling and scoping are deliberate links, not containment: a story keeps its feature/epic parent.",
25253
- entity_types: ["planning_cycle", "user_story", "objective"],
25254
- edge_chain: ["planning_cycle_contains_planning_cycle", "planning_cycle_schedules_user_story", "objective_scoped_to_planning_cycle"]
25430
+ description: "The cadence axis. Create a planning_cycle (a sprint, iteration, quarter, or PI), nest finer cycles inside a coarse one, schedule the work that flows through it (a task, story, epic, feature or bug), and scope the objectives the cycle serves. Scheduling and scoping are deliberate links, not containment: the scheduled item keeps its feature/epic parent.",
25431
+ entity_types: ["planning_cycle", "task", "user_story", "objective"],
25432
+ edge_chain: ["planning_cycle_contains_planning_cycle", "planning_cycle_schedules_work_item", "objective_scoped_to_planning_cycle"]
25255
25433
  }
25256
25434
  ],
25257
25435
  required_bridges: [
@@ -25948,7 +26126,7 @@ var PORTFOLIO_GUIDE = {
25948
26126
  var WORKSPACE_GUIDE = {
25949
26127
  domain_id: "workspace",
25950
26128
  anchor_entity: "workspace",
25951
- creation_sequence: ["workspace", "framework_exercise", "composition"],
26129
+ creation_sequence: ["workspace", "framework_exercise", "composition", "capture"],
25952
26130
  patterns: [
25953
26131
  {
25954
26132
  name: "Canvas to published view",
@@ -28562,7 +28740,7 @@ var UPG_ANTI_PATTERNS = [
28562
28740
  {
28563
28741
  id: "planning-cycle-without-scheduled-work",
28564
28742
  name: "Planning cycle with no scheduled work",
28565
- description: "A planning_cycle exists but neither schedules any user_story nor contains a finer sub-cycle. An empty cadence box is a date range with nothing flowing through it: a sprint or iteration nobody planned work into, or a coarse period that was never broken down.",
28743
+ description: "A planning_cycle exists but neither schedules any work item nor contains a finer sub-cycle. An empty cadence box is a date range with nothing flowing through it: a sprint or iteration nobody planned work into, or a coarse period that was never broken down.",
28566
28744
  structured_condition: {
28567
28745
  operator: "and",
28568
28746
  checks: [
@@ -28571,8 +28749,8 @@ var UPG_ANTI_PATTERNS = [
28571
28749
  check: {
28572
28750
  type: "relationship",
28573
28751
  source_type: "planning_cycle",
28574
- edge_type: "planning_cycle_schedules_user_story",
28575
- target_type: "user_story",
28752
+ edge_type: "planning_cycle_schedules_work_item",
28753
+ target_type: "node",
28576
28754
  comparison: "not_exists"
28577
28755
  }
28578
28756
  },
@@ -28588,10 +28766,16 @@ var UPG_ANTI_PATTERNS = [
28588
28766
  ]
28589
28767
  },
28590
28768
  why_it_matters: "A cadence layer only earns its keep when work is planned through it. An interval with no scheduled stories and no nested cycles adds ceremony without telling anyone what the period is for.",
28591
- remediation: "Schedule the stories the cycle will carry via `planning_cycle_schedules_user_story`, or break a coarse period into finer cycles via `planning_cycle_contains_planning_cycle`. If neither applies, the interval is not yet a real cadence box.",
28769
+ remediation: "Schedule the work the cycle will carry via `planning_cycle_schedules_work_item`, or break a coarse period into finer cycles via `planning_cycle_contains_planning_cycle`. If neither applies, the interval is not yet a real cadence box.",
28592
28770
  stages: ["build", "beta", "launch", "growth", "mature"],
28593
28771
  severity: "low",
28594
28772
  since: "0.20.0"
28773
+ // RETARGETED at 0.32.0 from planning_cycle_schedules_user_story. Left
28774
+ // unchanged it would have become a false-positive generator the moment
28775
+ // cycles could hold tasks: a cycle full of scheduled tasks and no stories
28776
+ // would have reported as empty. Labeled fixtures cover the near-miss this
28777
+ // is most likely to get wrong — a coarse `period` cycle that legitimately
28778
+ // holds only sub-cycles and schedules nothing directly.
28595
28779
  },
28596
28780
  // ── Market intelligence layer ───────────────────────────────────────────
28597
28781
  {
@@ -30717,7 +30901,7 @@ var UPG_REGIONS = [
30717
30901
  "roadmap_item_references_feature",
30718
30902
  "feature_request_voted_on_by_feedback_vote",
30719
30903
  "planning_cycle_contains_planning_cycle",
30720
- "planning_cycle_schedules_user_story"
30904
+ "planning_cycle_schedules_work_item"
30721
30905
  ],
30722
30906
  boundary_edges: [
30723
30907
  {
@@ -32645,9 +32829,12 @@ var NODE_KEY_ORDER = [
32645
32829
  "title",
32646
32830
  "slug",
32647
32831
  "aliases",
32832
+ "key",
32648
32833
  "description",
32649
32834
  "tags",
32650
32835
  "status",
32836
+ "archived",
32837
+ "archived_at",
32651
32838
  "lifecycle_status",
32652
32839
  "source_id",
32653
32840
  "source_type",
@@ -32937,7 +33124,7 @@ function checkHeaderSealText(text) {
32937
33124
  }
32938
33125
 
32939
33126
  // src/index.ts
32940
- var UPG_VERSION = "0.31.0";
33127
+ var UPG_VERSION = "0.32.0";
32941
33128
  var UPG_FORMAT_VERSION = "0.4.0";
32942
33129
  var MARKDOWN_FORMAT_VERSION = "0.1";
32943
33130
  var UPG_TYPES = getTypes();