@unified-product-graph/core 0.32.0 → 0.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.32.2] - 2026-08-21
11
+
12
+ Patch. No schema change: no entity, edge, property, enum, or lifecycle added or removed. The 0.32.0 base fields become reachable through the write path that was supposed to carry them.
13
+
14
+ ### Fixed
15
+ - **`UPGFileStore.updateNode` round-trips every `UPGBaseNode` field.** It merged a hand-maintained list of seven names and returned the unchanged node as a SUCCESS for anything outside it, so the three fields 0.32.0 added to the shape (`key`, `archived`, `archived_at`) were accepted and discarded. A caller could not tell a lost write from a landed one; the key-minting contention loop in the local graph-service adapter hit exactly this and spent its whole retry budget re-minting a key that could never move. The mergeable set is now DERIVED from the shape, so a field added to `UPGBaseNode` round-trips with no further edit.
16
+ - **An undeclared top-level field is now REFUSED, not dropped.** `updateNode` throws the new `UnknownNodeFieldError` naming the offending keys, and refuses atomically so the valid half of a patch cannot land beside a silently discarded half. Changing `id` throws `ImmutableNodeFieldError`; restating the current `id` stays legal, so spreading a whole node into a patch keeps working. Type-level callers cannot reach either path, since `Partial<UPGBaseNode>` rejects an undeclared key at compile time; the guards fire on runtime-shaped input such as a parsed argument bag.
17
+ - **Drift detection no longer reports a minted key as non-spec.** The canonical top-level field sets behind `top_level_drift` (the load-time counter in `@unified-product-graph/sdk` and the per-node breakdown in `validate_graph`) were two more hand-maintained copies of the same list, and 0.32.0 updated neither, so every node carrying `key`, `archived`, or `archived_at` was counted as holding non-spec top-level fields. Both derive from the shape now.
18
+
19
+ ### Added
20
+ - **`UPG_BASE_NODE_FIELDS`, `UPG_BASE_NODE_FIELD_SET`, `UPG_BASE_NODE_SPECIAL_MERGE_FIELDS`** on `@unified-product-graph/core`: the `UPGBaseNode` key set as runtime data, and the subset a generic shallow merge must not touch. A TypeScript interface is erased at runtime, which is why four consumers each kept their own copy behind a comment asking the next editor to remember. The list is locked to the interface by a `Record<keyof UPGBaseNode, true>` that is exhaustive in both directions, so adding a field to the shape without extending the list is now a COMPILE error rather than a silent divergence discovered three releases later.
21
+ - `UnknownNodeFieldError` and `ImmutableNodeFieldError` are exported from `@unified-product-graph/sdk`.
22
+
23
+ ### Notes
24
+ - Truth unchanged: **324 entities · 1082 edges · 69 cross-edges · 25 anti-patterns · 98 tools · 334 phases**. Zero new tools, zero new checks.
25
+ - Release process: `scripts/upg-release.sh` gains a step that regenerates the five `apps/entopo` spec mirrors from the freshly built train and then re-runs their `--check` forms. Those mirrors sit outside the release train's build filters, so no release ever ran their generators or their gates, and they have gone stale three times; 0.32.1 shipped with the snapshot still reading 0.32.0. The failure resists inspection because the drift test reads a checkout's `dist/`, and against a stale `dist` the comparison is stale-against-stale and reports green.
26
+ - The MCP `update_node` and `batch_update_nodes` tools still do not accept `key`, `archived`, or `archived_at` as arguments, so those fields remain unreachable from the agent surface. Widening a tool's argument schema is additive surface rather than a fix and is deliberately NOT in this patch.
27
+
28
+ ---
29
+
30
+ ## [0.32.1] - 2026-08-21
31
+
32
+ Patch. No schema change: no entity, edge, property, enum, or lifecycle added or removed.
33
+
34
+ ### Fixed
35
+ - `capture` and `composition` now carry authored one-line descriptions in the published catalog data. The 0.32.0 site rendered `capture` as "A capture entity": a generator fallback, not a description. The generator now refuses to emit a fallback and fails the build naming the undescribed type.
36
+ - `@unified-product-graph/adapters` exports `mapLinearPriority` and `normalizeLinearStatus` from the barrel, and `./adapters/*` is in the package `exports` map, matching every other adapter's normalisers.
37
+ - Documentation: the `planning_cycle` active-cycle and roll-forward rules are stated in `PlanningCycleProperties`; the two different sets the word "work item" names (the scheduling family and the WORK_ITEM lifecycle set) are spelled out in the lifecycle template; `classification_axis` no longer claims to be hosted only by a competitive analysis; the `workspace.canvas` namespaced-key rule carries one dated statement; `specification` states plainly that no typed conformance edge from product or feature exists yet; the `bug.labels` deprecation names both replacement halves.
38
+
39
+ ### Added
40
+ - Release gate `check:editorial`: a release is red until every entity type has an authored description, a published editorial doc with guidance and body, a review stamp at or above the release version for every new or shape-changed type, and no edge name cited in doc text that the catalog does not define. Ships with labeled fixtures; the live train refuses `--skip-editorial`.
41
+
42
+ ---
43
+
10
44
  ## [0.32.0] - 2026-08-21
11
45
 
12
46
  **A graph can now hold a real issue tracker's corpus without losing anything, and a query-driven view can say what it is a view OF.** Nine items in one design cycle: the two shapes the D-track held for "the next minor" (F1's query declaration, Q4b's artifact record) plus the six asks from a measured 1,032-issue Linear-parity audit. Additive throughout; three migrations registered, every one with a live population of zero.
package/dist/index.cjs CHANGED
@@ -70,6 +70,9 @@ __export(index_exports, {
70
70
  UPG_APPROACHES_BY_ID: () => UPG_APPROACHES_BY_ID,
71
71
  UPG_AREA_TAXONOMY: () => UPG_AREA_TAXONOMY,
72
72
  UPG_AREA_TAXONOMY_BY_COVERAGE_KEY: () => UPG_AREA_TAXONOMY_BY_COVERAGE_KEY,
73
+ UPG_BASE_NODE_FIELDS: () => UPG_BASE_NODE_FIELDS,
74
+ UPG_BASE_NODE_FIELD_SET: () => UPG_BASE_NODE_FIELD_SET,
75
+ UPG_BASE_NODE_SPECIAL_MERGE_FIELDS: () => UPG_BASE_NODE_SPECIAL_MERGE_FIELDS,
73
76
  UPG_CANONICAL_FORMAT_VERSION: () => UPG_CANONICAL_FORMAT_VERSION,
74
77
  UPG_CONTAINMENT_FREE_TYPES: () => UPG_CONTAINMENT_FREE_TYPES,
75
78
  UPG_COUNT_BENCHMARKS: () => UPG_COUNT_BENCHMARKS,
@@ -4085,6 +4088,39 @@ function isLegacyProductStage(stage) {
4085
4088
  return typeof stage === "string" && stage.toLowerCase() in LEGACY_PRODUCT_STAGES;
4086
4089
  }
4087
4090
 
4091
+ // src/shapes/base-node.ts
4092
+ var BASE_NODE_FIELD_PRESENCE = {
4093
+ id: true,
4094
+ type: true,
4095
+ title: true,
4096
+ slug: true,
4097
+ aliases: true,
4098
+ key: true,
4099
+ description: true,
4100
+ tags: true,
4101
+ status: true,
4102
+ archived: true,
4103
+ archived_at: true,
4104
+ source_id: true,
4105
+ source_type: true,
4106
+ mapping_confidence: true,
4107
+ external_tool: true,
4108
+ external_ref: true,
4109
+ external_id: true,
4110
+ properties: true
4111
+ };
4112
+ var UPG_BASE_NODE_FIELDS = Object.freeze(
4113
+ Object.keys(BASE_NODE_FIELD_PRESENCE)
4114
+ );
4115
+ var UPG_BASE_NODE_FIELD_SET = new Set(UPG_BASE_NODE_FIELDS);
4116
+ var UPG_BASE_NODE_SPECIAL_MERGE_FIELDS = /* @__PURE__ */ new Set([
4117
+ "id",
4118
+ "type",
4119
+ "slug",
4120
+ "aliases",
4121
+ "properties"
4122
+ ]);
4123
+
4088
4124
  // src/shapes/document.ts
4089
4125
  var UPG_CROSS_ONLY_EDGE_TYPES = [
4090
4126
  // Peer overlap: "these two products share / overlap on this thing." Symmetric,
@@ -8565,7 +8601,7 @@ var UPG_PROPERTY_MIGRATIONS = {
8565
8601
  kind: "drop_props",
8566
8602
  type: "bug",
8567
8603
  drop_props: ["labels"],
8568
- reason: "0.32.0: BugProperties.labels duplicated base-node `tags` and had no consumers. Same resolution as task."
8604
+ reason: "0.32.0: BugProperties.labels duplicated base-node `tags` and had no consumers. Freeform labels live in `tags`; grouped labels are classification_axis + node_classified_as_classification_value."
8569
8605
  }
8570
8606
  ],
8571
8607
  // ── v0.21.0: UPG-690 D.1 — collapse residual *_status shadows into status ────
@@ -14646,7 +14682,7 @@ var UPG_PROPERTY_SCHEMA = {
14646
14682
  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']`." },
14647
14683
  icon: { type: "string", description: "Display icon (emoji or icon name)" },
14648
14684
  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." },
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." }
14685
+ 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 (the rule since 0.31.0), 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." }
14650
14686
  }
14651
14687
  };
14652
14688
  function getPropertySchema(entityType) {
@@ -33124,7 +33160,7 @@ function checkHeaderSealText(text) {
33124
33160
  }
33125
33161
 
33126
33162
  // src/index.ts
33127
- var UPG_VERSION = "0.32.0";
33163
+ var UPG_VERSION = "0.32.2";
33128
33164
  var UPG_FORMAT_VERSION = "0.4.0";
33129
33165
  var MARKDOWN_FORMAT_VERSION = "0.1";
33130
33166
  var UPG_TYPES = getTypes();
@@ -33229,6 +33265,9 @@ var UPG_META_COUNT = UPG_ENTITY_META.length;
33229
33265
  UPG_APPROACHES_BY_ID,
33230
33266
  UPG_AREA_TAXONOMY,
33231
33267
  UPG_AREA_TAXONOMY_BY_COVERAGE_KEY,
33268
+ UPG_BASE_NODE_FIELDS,
33269
+ UPG_BASE_NODE_FIELD_SET,
33270
+ UPG_BASE_NODE_SPECIAL_MERGE_FIELDS,
33232
33271
  UPG_CANONICAL_FORMAT_VERSION,
33233
33272
  UPG_CONTAINMENT_FREE_TYPES,
33234
33273
  UPG_COUNT_BENCHMARKS,