@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.js CHANGED
@@ -491,7 +491,7 @@ var UPG_DOMAINS = [
491
491
  id: "workspace",
492
492
  label: "Workspace",
493
493
  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.",
494
- types: ["workspace", "framework_exercise", "composition"]
494
+ types: ["workspace", "framework_exercise", "composition", "capture"]
495
495
  },
496
496
  {
497
497
  id: "foundations",
@@ -979,7 +979,10 @@ var UPG_ENTITY_META = [
979
979
  // ── Workspace ──
980
980
  { name: "workspace", type_id: "ent_328", maturity: "proposed", since: "0.2.0" },
981
981
  { name: "framework_exercise", type_id: "ent_350", maturity: "proposed", since: "0.8.4" },
982
- { name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" }
982
+ { name: "composition", type_id: "ent_361", maturity: "proposed", since: "0.31.0" },
983
+ // A dated, hashed rendition of a node that is already in the graph. Enters
984
+ // `proposed` per the maturity rubric (precedent: composition 0.31.0).
985
+ { name: "capture", type_id: "ent_362", maturity: "proposed", since: "0.32.0" }
983
986
  ];
984
987
  var DEFERRED_PROPOSED_BY_CONTRACT = {
985
988
  // user_story (UPG-571) and experiment_run (UPG-664) graduated to stable in
@@ -1267,6 +1270,24 @@ var UPG_EDGE_CATALOG = {
1267
1270
  // the catalog's singular-source/target grammar (polymorphic targets
1268
1271
  // would be a separate catalog-mechanic change).
1269
1272
  competitive_analysis_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "competitive_analysis", target_type: "classification_axis" },
1273
+ // A product's own classification axes (0.32.0). Before this the only parent a
1274
+ // classification_axis could have was a competitive_analysis, so a taxonomy the
1275
+ // product uses on its OWN entities — the grouped-label case, one named group
1276
+ // over a set of values — had to hang off a competitive analysis it has nothing
1277
+ // to do with, or float parentless.
1278
+ //
1279
+ // Hierarchy, so the axis has a home in get_tree, on the same honest verb the
1280
+ // competitive_analysis edge already uses; and it earns its
1281
+ // UPG_VALID_CHILDREN.product pair the same way product_defines_configuration_axis
1282
+ // did at 0.30.0 — axes are few per product, and visibility of a mechanism
1283
+ // matters more than the tree cost.
1284
+ //
1285
+ // This is what makes one level of label grouping expressible with NO new label
1286
+ // surface: the group is the axis, the labels are its values, and carrying one
1287
+ // is node_classified_as_classification_value, all of it stable since 0.4.0.
1288
+ // Ungrouped freeform labels stay in `tags`. The line between them: if the
1289
+ // group has a name a person would filter by, it is an axis; otherwise a tag.
1290
+ product_dimensioned_by_classification_axis: { forward_verb: "dimensioned_by", reverse_verb: "dimensions", classification: "hierarchy", source_type: "product", target_type: "classification_axis" },
1270
1291
  classification_axis_includes_classification_value: { forward_verb: "includes", reverse_verb: "value_of", classification: "hierarchy", source_type: "classification_axis", target_type: "classification_value" },
1271
1292
  // 0.10.2: also dual-registered as a cross-edge (UPG_CROSS_EDGE_TYPES) so a
1272
1293
  // competitor can be classified directly against a `registry/{classification_value}`
@@ -1506,10 +1527,20 @@ var UPG_EDGE_CATALOG = {
1506
1527
  // iterations; a cycle contains its cooldown), mirroring team_contains_team.
1507
1528
  planning_cycle_contains_planning_cycle: { forward_verb: "contains", reverse_verb: "belongs_to", classification: "hierarchy", source_type: "planning_cycle", target_type: "planning_cycle" },
1508
1529
  // E3: scheduling work into a cycle is a deliberate authoring act, not a
1509
- // containment nesting — user_story keeps its feature/epic parent and is merely
1510
- // referenced by the cycle. deliberate_only so the generic-inference chokepoints
1511
- // never auto-materialise it.
1512
- 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 },
1530
+ // containment nesting — the work item keeps its feature/epic parent and is
1531
+ // merely referenced by the cycle. deliberate_only so the generic-inference
1532
+ // chokepoints never auto-materialise it.
1533
+ //
1534
+ // WIDENED AT 0.32.0, renamed from planning_cycle_schedules_user_story. The
1535
+ // story-only endpoint could not hold a real tracker import: the adapters
1536
+ // default an unrecognised issue to `task`, so the type a cycle most needed to
1537
+ // schedule was the one type it could not reach. Endpoint-polymorphic over the
1538
+ // work-item set {feature, epic, user_story, task, bug} by the same
1539
+ // construction as work_item_blocks_work_item in the same 0.20.0 batch — the
1540
+ // `work_item` token names the intended semantic domain, the endpoint is the
1541
+ // `node` wildcard. Three typed edges were the alternative and would have
1542
+ // re-opened a decision this vocabulary already took for the same entity set.
1543
+ planning_cycle_schedules_work_item: { forward_verb: "schedules", reverse_verb: "scheduled_in", classification: "semantic", source_type: "planning_cycle", target_type: "node", deliberate_only: true },
1513
1544
  // E2: the OKR cycle-scoping anchor. objective -> planning_cycle. planning_cycle
1514
1545
  // is portfolio_shared, so this passes the cross-scope gate and ships as
1515
1546
  // PROVISIONAL (allowed cross-product with a write-time warning, self-maintaining,
@@ -2634,6 +2665,16 @@ var UPG_EDGE_CATALOG = {
2634
2665
  // NOT cross_product_eligible: a portfolio-level composition spanning several
2635
2666
  // product graphs is conceivable but unevidenced. Same YAGNI call as above.
2636
2667
  composition_focuses_node: { forward_verb: "focuses", reverse_verb: "focused_in", classification: "cross-domain", source_type: "composition", target_type: "node" },
2668
+ // What a capture is a picture OF (0.32.0). Polymorphic because anything in the
2669
+ // graph can be rendered — a surface, a screen, a report, a canvas — and being
2670
+ // captured carries no structural role, so the endpoint collapses to the
2671
+ // wildcard rather than enumerating a list that would never finish.
2672
+ //
2673
+ // This edge is also where the preservation-is-not-permission rule lands for
2674
+ // renditions: a consumer renders the captures the GRAPH says exist, never the
2675
+ // ones some tool's opaque bag still remembers. A capture that was deleted is a
2676
+ // node that was deleted, and the spec needs no second deletion vocabulary.
2677
+ capture_renders_node: { forward_verb: "renders", reverse_verb: "rendered_by", classification: "cross-domain", source_type: "capture", target_type: "node" },
2637
2678
  // ── P14 edges (replacing foreign-key properties) ─────────────────────────
2638
2679
  feature_addresses_job: { forward_verb: "addresses", reverse_verb: "addressed_by", classification: "cross-domain", source_type: "feature", target_type: "job" },
2639
2680
  feature_drives_key_result: { forward_verb: "drives", reverse_verb: "driven_by", classification: "cross-domain", source_type: "feature", target_type: "key_result" },
@@ -2750,6 +2791,23 @@ var UPG_EDGE_CATALOG = {
2750
2791
  node_owned_by_stakeholder: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "stakeholder" },
2751
2792
  node_owned_by_department: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "department", cross_product_eligible: true },
2752
2793
  node_owned_by_person: { forward_verb: "owned_by", reverse_verb: "owns", classification: "cross-domain", source_type: "node", target_type: "person" },
2794
+ // ── Cross-domain: Assignment (0.32.0) ───────────────────────────────────────
2795
+ // Assignment is NOT ownership, and 0.12.0 ruled otherwise. That ruling routed
2796
+ // task.assignee and bug.assignee into node_owned_by_person, and it is
2797
+ // superseded here rather than quietly widened, because the two differ in a way
2798
+ // the field measures.
2799
+ //
2800
+ // ASSIGNMENT HAS A TIME INTERVAL AND OWNERSHIP DOES NOT. A shipped ops plane
2801
+ // models assignment as a claim carrying claimed-at, released-at and a partial
2802
+ // unique index enforcing one live claim; nothing in the spec gives ownership
2803
+ // that shape. And a real 184-item board ran with 157 items (85%) unassigned
2804
+ // while every one of them was owned in the ordinary sense. One edge meaning
2805
+ // both cannot express "owned by the team, assigned to nobody", which is the
2806
+ // modal state of a working board.
2807
+ //
2808
+ // Not cross_product_eligible, matching node_owned_by_person: an assignee is a
2809
+ // person in this graph's own roster.
2810
+ node_assigned_to_person: { forward_verb: "assigned_to", reverse_verb: "assigned", classification: "cross-domain", source_type: "node", target_type: "person" },
2753
2811
  // ── Cross-domain: Architecture (DDD) ─────────────────────────────────────────
2754
2812
  // Service, domain_event, domain_entity, aggregate, read_model, api_contract,
2755
2813
  // value_object, command, and data_model are DDD building blocks that all
@@ -3694,7 +3752,18 @@ var UPG_POLYMORPHIC_EDGE_KEYS = [
3694
3752
  // onto a canvas or shown in a published view, and neither carries a
3695
3753
  // structural role, so both collapse polymorphic rather than enumerating.
3696
3754
  "workspace_arranges_node",
3697
- "composition_focuses_node"
3755
+ "composition_focuses_node",
3756
+ // Universal assignment (0.32.0): who is working on this, as distinct from who
3757
+ // owns it. Wildcard source for the same reason node_owned_by_person has one.
3758
+ "node_assigned_to_person",
3759
+ // Capture subject (0.32.0): anything in the graph can be rendered, and being
3760
+ // rendered carries no structural role.
3761
+ "capture_renders_node",
3762
+ // Cadence scheduling (0.32.0, widened from planning_cycle_schedules_user_story):
3763
+ // endpoint-polymorphic over the work-item set {feature, epic, user_story, task,
3764
+ // bug}. The `work_item` key token names the intended semantic domain; the
3765
+ // endpoint is the `node` wildcard.
3766
+ "planning_cycle_schedules_work_item"
3698
3767
  ];
3699
3768
  var _POLY_KEY_SET = new Set(UPG_POLYMORPHIC_EDGE_KEYS);
3700
3769
  function isPolymorphicEdge(key) {
@@ -3889,6 +3958,11 @@ var UPG_VALID_CHILDREN = {
3889
3958
  // the product renders. Few per product, and top-level because an axis is a
3890
3959
  // product-wide fact rather than a child of any one feature or surface.
3891
3960
  "configuration_axis",
3961
+ // Classification (0.32.0): the product's own taxonomies — the grouped-label
3962
+ // case, one named group over a set of values. Same reasoning as the
3963
+ // configuration axis above, and until now the only parent a
3964
+ // classification_axis could have was a competitive_analysis.
3965
+ "classification_axis",
3892
3966
  // Engineering
3893
3967
  "bounded_context",
3894
3968
  "code_repository",
@@ -4173,7 +4247,7 @@ var UPG_VALID_CHILDREN = {
4173
4247
  // contains iterations; a cycle contains its cooldown) via
4174
4248
  // planning_cycle_contains_planning_cycle. It does NOT contain user_story as a
4175
4249
  // child: scheduling work into a cycle is the semantic, deliberate-only
4176
- // planning_cycle_schedules_user_story edge, so a story keeps its feature/epic
4250
+ // planning_cycle_schedules_work_item edge, so the item keeps its feature/epic
4177
4251
  // containment parent and is merely referenced by the cycle.
4178
4252
  planning_cycle: ["planning_cycle"],
4179
4253
  // ── Engineering hierarchy ───────────────────────────────────────────────────
@@ -4519,7 +4593,12 @@ var UPG_CONTAINMENT_FREE_TYPES = /* @__PURE__ */ new Set([
4519
4593
  // part of the product, it is a lens onto one. This is also what keeps the
4520
4594
  // type to two edges rather than three: no provenance edge is minted (that is
4521
4595
  // `workspace_produced_node`, reused) and no parent edge is needed.
4522
- "composition"
4596
+ "composition",
4597
+ // A capture has no structural parent for the same reason: it anchors to what
4598
+ // it RENDERS, via `capture_renders_node`. Giving it a product parent would
4599
+ // claim a capture is part of the product rather than a picture of one, and
4600
+ // would mint a containment edge whose only job is to satisfy the hierarchy.
4601
+ "capture"
4523
4602
  ]);
4524
4603
  function isContainmentFreeType(entityType) {
4525
4604
  return UPG_CONTAINMENT_FREE_TYPES.has(entityType);
@@ -6625,7 +6704,8 @@ var WORK_ITEM_TEMPLATE = {
6625
6704
  initial_phase: "todo",
6626
6705
  terminal_phases: ["done", "cancelled"],
6627
6706
  phases: [
6628
- { id: "todo", status_category: "unstarted", label: "To Do", description: "Identified but not yet started.", transitions_to: ["in_progress", "cancelled"] },
6707
+ { 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"] },
6708
+ { id: "todo", status_category: "unstarted", label: "To Do", description: "Identified but not yet started.", transitions_to: ["in_progress", "backlog", "cancelled"] },
6629
6709
  { id: "in_progress", status_category: "started", label: "In Progress", description: "Actively being worked on.", transitions_to: ["in_review", "todo", "cancelled"] },
6630
6710
  { id: "in_review", status_category: "started", label: "In Review", description: "Work completed, awaiting review or acceptance.", transitions_to: ["done", "in_progress", "cancelled"] },
6631
6711
  { id: "done", status_category: "completed", label: "Done", description: "Completed and accepted.", transitions_to: [] },
@@ -7256,7 +7336,11 @@ var UPG_LIFECYCLE_FREE_TYPES = /* @__PURE__ */ new Set([
7256
7336
  // from experiment_runs (which have lifecycles); hypothesis_evidence was
7257
7337
  // lifecycle-free here but is deprecated at v0.4.0 ──────────────────────────────────────
7258
7338
  "evidence",
7259
- "learning"
7339
+ "learning",
7340
+ // ── Workspace (1 of 4): a capture is a fact about a moment, not a workflow.
7341
+ // It is taken, and from then on it either still matches its subject or it
7342
+ // does not — which is what `content_hash` answers, not a phase. ─────────
7343
+ "capture"
7260
7344
  ]);
7261
7345
  function isLifecycleFreeType(entityType) {
7262
7346
  return UPG_LIFECYCLE_FREE_TYPES.has(entityType);
@@ -8160,6 +8244,46 @@ function getDeprecatedTypes() {
8160
8244
  return deprecated;
8161
8245
  }
8162
8246
  var UPG_PROPERTY_MIGRATIONS = {
8247
+ "0.32.0": [
8248
+ // C3 — archive becomes a base-node axis. WorkspaceProperties.archived /
8249
+ // archived_at shipped the orthogonal-archive idea for exactly one type;
8250
+ // field data showed it is general (a tracker held 559 archived-completed
8251
+ // items beside 18 live-completed ones), so the pair moves to UPGBaseNode
8252
+ // and the workspace properties are @deprecated. Lift, not drop: the value
8253
+ // is real and its new home means the same thing.
8254
+ {
8255
+ kind: "lift_property_to_top_level",
8256
+ type: "workspace",
8257
+ from_property: "archived",
8258
+ to: "archived",
8259
+ reason: "0.32.0: archive generalised from WorkspaceProperties to UPGBaseNode.archived. Same fact, wider home; the workspace property is @deprecated."
8260
+ },
8261
+ {
8262
+ kind: "lift_property_to_top_level",
8263
+ type: "workspace",
8264
+ from_property: "archived_at",
8265
+ to: "archived_at",
8266
+ reason: "0.32.0: pairs with the archived lift above. UPGBaseNode.archived_at is the canonical home."
8267
+ },
8268
+ // C4 — the duplicate label surfaces. TaskProperties.labels and
8269
+ // BugProperties.labels were declared "applied uniformly across work item
8270
+ // types" and duplicated base-node `tags` with no consumer anywhere. Three
8271
+ // label surfaces (base tags, per-type tags, per-type labels) is two too
8272
+ // many; grouped labels are classification_axis + classification_value, and
8273
+ // ungrouped ones are `tags`.
8274
+ {
8275
+ kind: "drop_props",
8276
+ type: "task",
8277
+ drop_props: ["labels"],
8278
+ 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."
8279
+ },
8280
+ {
8281
+ kind: "drop_props",
8282
+ type: "bug",
8283
+ drop_props: ["labels"],
8284
+ reason: "0.32.0: BugProperties.labels duplicated base-node `tags` and had no consumers. Same resolution as task."
8285
+ }
8286
+ ],
8163
8287
  // ── v0.21.0: UPG-690 D.1 — collapse residual *_status shadows into status ────
8164
8288
  //
8165
8289
  // Six `*_status` enum properties that re-encode their entity's own lifecycle
@@ -9295,6 +9419,48 @@ function getSplitMigrations(fromVersion, toVersion) {
9295
9419
  return result;
9296
9420
  }
9297
9421
  var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
9422
+ "0.32.0": [
9423
+ // C2 — assignment gets its own edge, superseding the 0.12.0 rules that
9424
+ // routed task/bug `assignee` into node_owned_by_person.
9425
+ //
9426
+ // WHY THIS IS A REVERSAL AND NOT A GAP-FILL. 0.12.0 ruled that an assignee
9427
+ // is an owner. Assignment has a time interval and an exclusivity that
9428
+ // ownership does not, and a real board ran 85% unassigned while every item
9429
+ // was owned — a state one edge cannot express.
9430
+ //
9431
+ // WHY EXISTING EDGES ARE NOT REWRITTEN. A node_owned_by_person edge on a
9432
+ // task is indistinguishable by inspection from a genuine ownership edge, so
9433
+ // a blanket rename would silently reclassify real facts. Measured population
9434
+ // across every graph in the originating workspace was ~zero, which makes the
9435
+ // cost of leaving them nil and the cost of guessing a wrong fact.
9436
+ //
9437
+ // user_story is included here and was MISSING from 0.12.0 entirely, though
9438
+ // it has carried `assignee` since 0.20.0.
9439
+ {
9440
+ from_type: "task",
9441
+ scalar_property: "assignee",
9442
+ target_type: "person",
9443
+ edge_type: "node_assigned_to_person",
9444
+ drop_scalar: false,
9445
+ 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.'
9446
+ },
9447
+ {
9448
+ from_type: "bug",
9449
+ scalar_property: "assignee",
9450
+ target_type: "person",
9451
+ edge_type: "node_assigned_to_person",
9452
+ drop_scalar: false,
9453
+ reason: "P14 actor: same as task. Supersedes the 0.12.0 node_owned_by_person rule. Display string kept."
9454
+ },
9455
+ {
9456
+ from_type: "user_story",
9457
+ scalar_property: "assignee",
9458
+ target_type: "person",
9459
+ edge_type: "node_assigned_to_person",
9460
+ drop_scalar: false,
9461
+ 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."
9462
+ }
9463
+ ],
9298
9464
  "0.12.0": [
9299
9465
  // ── Bucket A1 — orphan scalars (no edge existed): add edge + migrate + drop ──
9300
9466
  {
@@ -9705,22 +9871,12 @@ var UPG_SCALAR_TO_EDGE_MIGRATIONS = {
9705
9871
  drop_scalar: false,
9706
9872
  reason: "P14 actor: the release owner is a first-class person; link node_owned_by_person. Display string kept."
9707
9873
  },
9708
- {
9709
- from_type: "task",
9710
- scalar_property: "assignee",
9711
- target_type: "person",
9712
- edge_type: "node_owned_by_person",
9713
- drop_scalar: false,
9714
- reason: 'P14 actor: the assignee is a first-class person ("every task assigned to this person"); link node_owned_by_person. Display string kept.'
9715
- },
9716
- {
9717
- from_type: "bug",
9718
- scalar_property: "assignee",
9719
- target_type: "person",
9720
- edge_type: "node_owned_by_person",
9721
- drop_scalar: false,
9722
- reason: "P14 actor: the assignee is a first-class person; link node_owned_by_person. Display string kept."
9723
- },
9874
+ // NOTE (0.32.0): the task/bug `assignee` rules that lived here are SUPERSEDED
9875
+ // by the '0.32.0' block below, which routes them to node_assigned_to_person.
9876
+ // They are removed rather than left to double-run: two rules over one scalar
9877
+ // would mint the person once and link it twice, under two verbs that now mean
9878
+ // different things. Graphs promoted before 0.32.0 keep the ownership edges
9879
+ // they were given — see the 0.32.0 block for why those are not rewritten.
9724
9880
  {
9725
9881
  from_type: "roadmap",
9726
9882
  scalar_property: "owner",
@@ -9816,6 +9972,14 @@ function getScalarToEdgeMigrations(fromVersion, toVersion) {
9816
9972
  return result;
9817
9973
  }
9818
9974
  var UPG_EDGE_MIGRATIONS = {
9975
+ "0.32.0": [
9976
+ // C5 — the cadence scheduling edge widens from user_story to the work-item
9977
+ // set. No requires_target_type: the destination's target_type is the `node`
9978
+ // wildcard, and endpoint guards are checked against the catalog's declared
9979
+ // type rather than the concrete instance (same reasoning as the 0.22.0
9980
+ // workspace_produced_node rename below). Gate only on the unwidened source.
9981
+ { 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." }
9982
+ ],
9819
9983
  // (target version TBD at release ceremony — pencilled at 0.22.0, the next
9820
9984
  // breaking slot after 0.21.0 per the docket; adjust the key if Wave 4
9821
9985
  // claims it first.) WS3 commit-provenance collapse (2026-07-05, ratified):
@@ -11369,9 +11533,9 @@ var UPG_PROPERTY_SCHEMA = {
11369
11533
  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)." },
11370
11534
  assignee: { type: "string", description: "Assigned person. Promote to a `node_owned_by_person` edge if ownership must be queryable." },
11371
11535
  due_date: { type: "string", description: "ISO date due. Often tied to a release gate or SLA." },
11372
- labels: { type: "string[]", description: "Free-form classification tags. Applied uniformly across work item types." },
11536
+ 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." },
11373
11537
  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.' },
11374
- 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`.' }
11538
+ 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." }
11375
11539
  },
11376
11540
  // BuildArtifactProperties: Build artifact.
11377
11541
  build_artifact: {
@@ -11402,6 +11566,16 @@ var UPG_PROPERTY_SCHEMA = {
11402
11566
  allocated: { type: "number", description: "Capacity already allocated to work" },
11403
11567
  available: { type: "number", description: "Remaining unallocated capacity" }
11404
11568
  },
11569
+ // CaptureProperties: Capture: a dated, hashed rendition of something already in the graph.
11570
+ capture: {
11571
+ 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." },
11572
+ 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." },
11573
+ hash_algorithm: { type: "string", enum: ["sha256", "sha1", "md5"], description: "Hash algorithm. Absent means `sha256`." },
11574
+ captured_at: { type: "string", description: "ISO timestamp the capture was taken." },
11575
+ media_type: { type: "string", description: 'IANA media type of the bytes. @example "image/png"' },
11576
+ 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." },
11577
+ 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." }
11578
+ },
11405
11579
  // CeremonyProperties: Ceremony entity.
11406
11580
  ceremony: {
11407
11581
  ceremony_type: { type: "string", enum: ["standup", "planning", "review", "retro", "sync", "demo", "other"], description: "Kind of recurring meeting" },
@@ -11605,7 +11779,9 @@ var UPG_PROPERTY_SCHEMA = {
11605
11779
  members: { type: "object[]", description: "The frozen member arrangement, captured at publish. Layout and pointers only, never resolved content." },
11606
11780
  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." },
11607
11781
  published_at: { type: "string", description: "ISO timestamp of the most recent publish or republish." },
11608
- published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." }
11782
+ published_by: { type: "string", description: "Publisher handle or email. Display scalar, same posture as `WorkspaceProperties.owner`." },
11783
+ 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." },
11784
+ presentation: { type: "object", description: "Advisory rendering intent for the composition as a whole. A consumer may ignore it entirely and still be conformant." }
11609
11785
  },
11610
11786
  // ConfigurationAxisProperties: A named dimension along which the product's composition differs.
11611
11787
  configuration_axis: {
@@ -12062,7 +12238,7 @@ var UPG_PROPERTY_SCHEMA = {
12062
12238
  start_date: { type: "string", description: "ISO date work begins" },
12063
12239
  target_date: { type: "string", description: "ISO date work completes" },
12064
12240
  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.' },
12065
- 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`.' }
12241
+ 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." }
12066
12242
  },
12067
12243
  // ErrorBudgetProperties: Error budget.
12068
12244
  error_budget: {
@@ -12246,7 +12422,7 @@ var UPG_PROPERTY_SCHEMA = {
12246
12422
  target_date: { type: "string", description: "ISO date work completes" },
12247
12423
  health: { type: "string", enum: ["on_track", "at_risk", "off_track"], description: "Delivery health" },
12248
12424
  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.' },
12249
- 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`.' }
12425
+ 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." }
12250
12426
  },
12251
12427
  // FeatureAreaProperties: A structural grouping of related features within a product.
12252
12428
  feature_area: {
@@ -13242,7 +13418,8 @@ var UPG_PROPERTY_SCHEMA = {
13242
13418
  url: { type: "string", description: "Where the product lives. Marketing site, app store URL, etc.", modifier: "volatile" },
13243
13419
  logo_url: { type: "string", description: "Logo or icon URL. Used to render product cards and lists.", modifier: "volatile" },
13244
13420
  launched_at: { type: "string", description: "When the product became generally available (ISO 8601)" },
13245
- 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." }
13421
+ 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." },
13422
+ 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.' }
13246
13423
  },
13247
13424
  // ProductAreaProperties: ProductArea entity.
13248
13425
  product_area: {
@@ -13842,9 +14019,9 @@ var UPG_PROPERTY_SCHEMA = {
13842
14019
  effort: { type: "string", description: 'Effort estimate (e.g. "2h", "1d", "3 points"). Use a consistent unit within your team.' },
13843
14020
  priority: { type: "string", enum: ["urgent", "high", "medium", "low", "none"], description: "Relative importance against other tasks" },
13844
14021
  due_date: { type: "string", description: "ISO date due. Typically bounded by the containing story's due date." },
13845
- labels: { type: "string[]", description: "Free-form classification tags. Applied uniformly across work item types." },
14022
+ 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." },
13846
14023
  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.' },
13847
- 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`.' }
14024
+ 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." }
13848
14025
  },
13849
14026
  // TeamProperties: Team entity.
13850
14027
  team: {
@@ -14066,7 +14243,7 @@ var UPG_PROPERTY_SCHEMA = {
14066
14243
  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." },
14067
14244
  due_date: { type: "string", description: "ISO date due. Typically bounded by the release or planning_cycle the story is scheduled into." },
14068
14245
  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.' },
14069
- 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`.' }
14246
+ 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." }
14070
14247
  },
14071
14248
  // ValueObjectProperties: DDD value object.
14072
14249
  value_object: {
@@ -14181,8 +14358,8 @@ var UPG_PROPERTY_SCHEMA = {
14181
14358
  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." },
14182
14359
  owner: { type: "string", description: "Workspace owner (handle or email). Display label; canonical owner is `team_owns_workspace` or `persona_owns_workspace`." },
14183
14360
  member_count: { type: "number", description: "Snapshot count. `team_works_in_workspace` edges are the source of truth.", modifier: "derived" },
14184
- archived: { type: "boolean", description: "Archived. Archived workspaces remain queryable but hidden from default views." },
14185
- archived_at: { type: "string", description: "ISO timestamp archived. Pairs with `archived === true`." },
14361
+ 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." },
14362
+ 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']`." },
14186
14363
  icon: { type: "string", description: "Display icon (emoji or icon name)" },
14187
14364
  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." },
14188
14365
  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." }
@@ -21586,6 +21763,7 @@ var ENTITY_EMOJI = {
21586
21763
  competitor_signal: "\u{1F4E1}",
21587
21764
  compliance_framework: "\u{1F3DF}\uFE0F",
21588
21765
  compliance_requirement: "\u26F2",
21766
+ capture: "\u{1F4F8}",
21589
21767
  composition: "\u{1F5BC}\uFE0F",
21590
21768
  constraint: "\u{1F512}",
21591
21769
  contact: "\u{1F4C7}",
@@ -24965,9 +25143,9 @@ var PRODUCT_SPEC_GUIDE = {
24965
25143
  },
24966
25144
  {
24967
25145
  name: "Planning Cadence",
24968
- 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.",
24969
- entity_types: ["planning_cycle", "user_story", "objective"],
24970
- edge_chain: ["planning_cycle_contains_planning_cycle", "planning_cycle_schedules_user_story", "objective_scoped_to_planning_cycle"]
25146
+ 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.",
25147
+ entity_types: ["planning_cycle", "task", "user_story", "objective"],
25148
+ edge_chain: ["planning_cycle_contains_planning_cycle", "planning_cycle_schedules_work_item", "objective_scoped_to_planning_cycle"]
24971
25149
  }
24972
25150
  ],
24973
25151
  required_bridges: [
@@ -25664,7 +25842,7 @@ var PORTFOLIO_GUIDE = {
25664
25842
  var WORKSPACE_GUIDE = {
25665
25843
  domain_id: "workspace",
25666
25844
  anchor_entity: "workspace",
25667
- creation_sequence: ["workspace", "framework_exercise", "composition"],
25845
+ creation_sequence: ["workspace", "framework_exercise", "composition", "capture"],
25668
25846
  patterns: [
25669
25847
  {
25670
25848
  name: "Canvas to published view",
@@ -28278,7 +28456,7 @@ var UPG_ANTI_PATTERNS = [
28278
28456
  {
28279
28457
  id: "planning-cycle-without-scheduled-work",
28280
28458
  name: "Planning cycle with no scheduled work",
28281
- 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.",
28459
+ 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.",
28282
28460
  structured_condition: {
28283
28461
  operator: "and",
28284
28462
  checks: [
@@ -28287,8 +28465,8 @@ var UPG_ANTI_PATTERNS = [
28287
28465
  check: {
28288
28466
  type: "relationship",
28289
28467
  source_type: "planning_cycle",
28290
- edge_type: "planning_cycle_schedules_user_story",
28291
- target_type: "user_story",
28468
+ edge_type: "planning_cycle_schedules_work_item",
28469
+ target_type: "node",
28292
28470
  comparison: "not_exists"
28293
28471
  }
28294
28472
  },
@@ -28304,10 +28482,16 @@ var UPG_ANTI_PATTERNS = [
28304
28482
  ]
28305
28483
  },
28306
28484
  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.",
28307
- 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.",
28485
+ 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.",
28308
28486
  stages: ["build", "beta", "launch", "growth", "mature"],
28309
28487
  severity: "low",
28310
28488
  since: "0.20.0"
28489
+ // RETARGETED at 0.32.0 from planning_cycle_schedules_user_story. Left
28490
+ // unchanged it would have become a false-positive generator the moment
28491
+ // cycles could hold tasks: a cycle full of scheduled tasks and no stories
28492
+ // would have reported as empty. Labeled fixtures cover the near-miss this
28493
+ // is most likely to get wrong — a coarse `period` cycle that legitimately
28494
+ // holds only sub-cycles and schedules nothing directly.
28311
28495
  },
28312
28496
  // ── Market intelligence layer ───────────────────────────────────────────
28313
28497
  {
@@ -30433,7 +30617,7 @@ var UPG_REGIONS = [
30433
30617
  "roadmap_item_references_feature",
30434
30618
  "feature_request_voted_on_by_feedback_vote",
30435
30619
  "planning_cycle_contains_planning_cycle",
30436
- "planning_cycle_schedules_user_story"
30620
+ "planning_cycle_schedules_work_item"
30437
30621
  ],
30438
30622
  boundary_edges: [
30439
30623
  {
@@ -32361,9 +32545,12 @@ var NODE_KEY_ORDER = [
32361
32545
  "title",
32362
32546
  "slug",
32363
32547
  "aliases",
32548
+ "key",
32364
32549
  "description",
32365
32550
  "tags",
32366
32551
  "status",
32552
+ "archived",
32553
+ "archived_at",
32367
32554
  "lifecycle_status",
32368
32555
  "source_id",
32369
32556
  "source_type",
@@ -32653,7 +32840,7 @@ function checkHeaderSealText(text) {
32653
32840
  }
32654
32841
 
32655
32842
  // src/index.ts
32656
- var UPG_VERSION = "0.31.0";
32843
+ var UPG_VERSION = "0.32.0";
32657
32844
  var UPG_FORMAT_VERSION = "0.4.0";
32658
32845
  var MARKDOWN_FORMAT_VERSION = "0.1";
32659
32846
  var UPG_TYPES = getTypes();