@unified-product-graph/core 0.6.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/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@unified-product-graph/core",
3
+ "version": "0.6.0",
4
+ "description": "Unified Product Graph — open specification and TypeScript types for product knowledge",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "The Product Creator <hello@theproductcreator.com>",
8
+ "homepage": "https://unifiedproductgraph.org",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/unified-product-graph/core.git"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "!dist/__tests__",
25
+ "spec/UPG-MARKDOWN-v0.1.md",
26
+ "spec/examples",
27
+ "README.md",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "type-check": "tsc --noEmit",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "lint": "eslint src/",
36
+ "audit:slot-connectivity": "tsx scripts/audit-framework-slot-connectivity.ts",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "devDependencies": {
40
+ "tsup": "^8.5.1"
41
+ },
42
+ "keywords": [
43
+ "product-management",
44
+ "product-graph",
45
+ "upg",
46
+ "unified-product-graph",
47
+ "product-knowledge",
48
+ "open-standard",
49
+ "ontology"
50
+ ],
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }
@@ -0,0 +1,437 @@
1
+ # UPG Markdown — Format Specification (v0.1)
2
+
3
+ > The literate variant of the `.upg` JSON format.
4
+ > Human-authored Markdown with typed entity references that resolve against a product graph.
5
+
6
+ **Format version:** 0.1
7
+ **Status:** Pragmatic spec (not formal grammar — written the way good web specs are written).
8
+ **Reference implementation:** `@unified-product-graph/markdown` on npm.
9
+ **Third-party parser conformance:** §3 below is the normative checklist for conformant parsers.
10
+ **License:** MIT.
11
+
12
+ ---
13
+
14
+ ## 1. What `.upg.md` is
15
+
16
+ A `.upg.md` file is a single CommonMark Markdown document with two additions:
17
+
18
+ 1. **YAML frontmatter** declaring the document's identity and graph-membership.
19
+ 2. **Inline typed references** in the body — `[[type:id]]` chips for entities, `{{type:id → type:id|verb}}` connectors for edges — and an optional fenced JSON escape hatch (` ```upg `) for declarations a renderer needs to resolve up-front.
20
+
21
+ It is **the same product graph as `.upg`**, expressed prose-first. A `.upg.md` document is itself an entity of type `document`; the references it contains are the graph the document narrates.
22
+
23
+ The format is designed for:
24
+
25
+ - **Human authoring** — write Markdown the way you already do; entities and edges sit inline.
26
+ - **Round-trip with `.upg` JSON** — a `.upg.md` and an equivalent `.upg` file describe the same graph.
27
+ - **Portability** — any Markdown-aware tool can render the document; UPG-aware tools resolve the chips.
28
+ - **Diffability** — line-oriented source, friendly to git, code review, and pair authoring.
29
+
30
+ Non-goals: HTML rendering semantics, live link resolution, embedding/search, MDX-style component syntax, multi-document workspace indexing. All are out of scope here. They MAY ship in tooling layers; they are not part of the format.
31
+
32
+ ---
33
+
34
+ ## 2. File structure
35
+
36
+ A `.upg.md` document has exactly two parts in this order:
37
+
38
+ 1. **Frontmatter** — a YAML block delimited by `---` on its own lines (§3).
39
+ 2. **Body** — CommonMark Markdown with inline references (§4–§6) and optional fenced `upg` blocks (§7).
40
+
41
+ ```
42
+ ---
43
+ title: "Document Title"
44
+ upg_product: <product-slug>
45
+ upg_version: "0.2"
46
+ entity_type: document
47
+ entity_id: <stable-id>
48
+ ---
49
+
50
+ # Body starts here.
51
+
52
+ The persona [[persona:alex-senior-pm]] pursues
53
+ {{persona:alex-senior-pm → job:keep-team-aligned|pursues}}.
54
+ ```
55
+
56
+ **Encoding:** UTF-8, no BOM. Line endings: LF preferred, CRLF tolerated (parsers SHOULD normalise to LF on emit).
57
+ **Extension:** `.upg.md`. MIME type: `text/markdown; profile="upg"`.
58
+
59
+ ---
60
+
61
+ ## 3. Frontmatter
62
+
63
+ The frontmatter MUST be the first construct in the file. If the opening `---` is absent, the document is not a `.upg.md` — UPG-aware tools SHOULD treat it as plain Markdown and emit a `MISSING_FRONTMATTER` error if UPG semantics are required.
64
+
65
+ ### 3.1 Required fields
66
+
67
+ | Field | Type | Meaning |
68
+ |---|---|---|
69
+ | `title` | string | Human-readable document title. Used as fallback display when chips reference this document by id. |
70
+ | `upg_product` | string | Slug of the product graph this document belongs to. Scopes every chip ID unless an explicit `@product` suffix overrides (§5). |
71
+ | `upg_version` | string | UPG spec version this document targets (e.g. `"0.2"`). Parsers MUST NOT reject unknown versions; they MAY warn. |
72
+ | `entity_type` | string literal `document` | The document is an entity. v0.1 fixes this to `document`. |
73
+ | `entity_id` | string | Stable ID of the document entity itself. MUST match `^[a-z][a-z0-9_-]*$`. |
74
+
75
+ A document missing any required field is **invalid** — a conforming parser emits `MISSING_REQUIRED_FIELD` and halts structural extraction. Chips and edges MAY still be reported as warnings for diagnostics.
76
+
77
+ ### 3.2 Optional fields
78
+
79
+ Reserved keys recognised by the spec:
80
+
81
+ | Field | Type | Meaning |
82
+ |---|---|---|
83
+ | `author` | string | Free-form author attribution. |
84
+ | `created_at` | ISO 8601 date | Creation timestamp. |
85
+ | `updated_at` | ISO 8601 date | Last-update timestamp. |
86
+ | `tags` | string[] | Free-form categorisation. Flow (`[a, b, c]`) and block (`- a\n- b`) styles both valid. |
87
+ | `status` | `'draft'` \| `'review'` \| `'published'` \| `'archived'` | Document lifecycle state. |
88
+ | `composition_pattern` | string | Named structural pattern (e.g. `experiment_report`, `decision_record`). |
89
+ | `graph_source` | path | Relative path to the `.upg` file this document resolves against. |
90
+ | `aliases` | string[] | Alternate IDs for the document entity. |
91
+ | `region` | string | Spec region this document belongs to. |
92
+ | `parent` | string | Parent entity (`type:id`) under which this document nests. |
93
+
94
+ ### 3.3 Extension fields
95
+
96
+ Any frontmatter key not listed in §3.1 or §3.2 is an **extension**. Conforming parsers MUST preserve unknown keys verbatim, including key order, on round-trip. A parser that drops an unknown key is non-conforming.
97
+
98
+ This is the forward-compatibility commitment: tools that ship today MUST round-trip documents that target future versions of the spec without data loss for constructs they do not understand.
99
+
100
+ ---
101
+
102
+ ## 4. Body conventions
103
+
104
+ The body is **prose-first**: standard CommonMark plus GitHub-Flavored Markdown (tables, fenced code blocks, strikethrough, task lists). The two Markdown extensions UPG MD adds — typed chips and edge connectors — are **inline syntax inside ordinary prose**, not block-level constructs.
105
+
106
+ > **Rationale for prose-first.** See `08-decisions/2026-04-25-upg-md-prose-first.md`. Earlier drafts treated entity declarations as block constructs (Markdown headings + property tables). Authoring practice converged on inline references. The fenced `upg` block (§7) remains the structural escape hatch when prose alone cannot express what's needed.
107
+
108
+ A document MAY contain zero references. A `.upg.md` with no chips is a valid document — it just narrates without graph-augmentation.
109
+
110
+ ### 4.1 What Markdown extensions are in scope
111
+
112
+ | Feature | In scope | Notes |
113
+ |---|---|---|
114
+ | CommonMark | YES | Full CommonMark is the substrate. |
115
+ | GFM tables | YES | Tables are common in property-heavy documents (§9 examples). |
116
+ | GFM fenced code blocks | YES | The fenced `upg` block (§7) is a special case. |
117
+ | GFM strikethrough, task lists, autolinks | YES | Permitted but not load-bearing. |
118
+ | YAML frontmatter | YES | §3. |
119
+ | MDX / JSX components | NO | Out of scope. Prose-first means CommonMark prose. |
120
+ | Inline HTML | TOLERATED | Permitted but not extracted. Renderers MAY pass through. |
121
+ | Markdown comments (`<!-- ... -->`) | TOLERATED | Pass-through; a parser MAY drop them outside chip/edge ranges (§8.4). |
122
+
123
+ ---
124
+
125
+ ## 5. Entity references (chips)
126
+
127
+ Form: `[[type:id]]`, with optional creation marker, product scope, and pipe-delimited modifiers.
128
+
129
+ ### 5.1 Canonical grammar
130
+
131
+ ```
132
+ /\[\[(\+?)([a-z][\w]*?):([a-z][\w-]*?)(?:@([\w-]+))?(?:\|([^\]]*))?\]\]/g
133
+ ```
134
+
135
+ Capture groups:
136
+
137
+ 1. **Creation flag** — `+` if present (§5.3), empty otherwise.
138
+ 2. **Type** — `^[a-z][a-z_]*$`. Must match an entry in `UPG_ENTITY_META` to be **resolved**; unknown types remain valid syntax (§8.5).
139
+ 3. **ID** — `^[a-z][a-z0-9_-]*$`. May be the entity's canonical UUID, slug, or any value in its `aliases[]` (§5.5).
140
+ 4. **Product slug** — optional `@product` suffix (§5.4). Absent means inherit `upg_product` from frontmatter.
141
+ 5. **Modifiers** — pipe-delimited segments parsed left-to-right (§5.6).
142
+
143
+ ### 5.2 The basic forms
144
+
145
+ | Form | Meaning |
146
+ |---|---|
147
+ | `[[type:id]]` | Reference an existing entity. |
148
+ | `[[+type:id]]` | **Creation marker.** Declares the entity (§5.3). |
149
+ | `[[type:id@product]]` | Cross-product reference (§5.4). |
150
+ | `[[type:id\|"label"]]` | Display label override (§5.6.1). |
151
+ | `[[type:id\|key:value]]` | Inline property assignment (§5.6.2). |
152
+ | `[[type:id\|key:value\|"label"]]` | Combined — modifiers and display text in any order. |
153
+
154
+ ### 5.3 Creation references
155
+
156
+ A `[[+type:id]]` marks the **first occurrence** in document order as the entity declaration. Subsequent `[[type:id]]` references resolve to it. The creation form is equivalent to a fenced-block declaration (§7.2) for the same `type:id`.
157
+
158
+ If both an inline `[[+type:id]]` and a fenced declaration name the same `type:id`, the **first occurrence in document order wins**; later ones are references. Conforming parsers emit `DUPLICATE_DECLARATION` as a **warning**, never an error.
159
+
160
+ ### 5.4 Cross-product references
161
+
162
+ `[[persona:alex@analytics]]` references entity `persona:alex` in product `analytics`, regardless of the current document's `upg_product`. The product slug suffix is `@<product>`; valid characters are `[\w-]+`.
163
+
164
+ The grammar collapses `@product` and `@variant` (a hypothetical future construct) into a single `@([\w-]+)` slot. Resolution is the consumer's concern: parsers that don't implement variants treat the suffix as a product slug.
165
+
166
+ ### 5.5 Layered ID resolution (UPG ≥ v0.2.2)
167
+
168
+ The `id` capture is a generic identifier. A conforming **resolver** MUST try each layer in order and stop at the first hit:
169
+
170
+ 1. UUID match against the node's `id` field.
171
+ 2. Slug match against the node's `slug` field, scoped to the chip's `type`.
172
+ 3. Alias match against any value in the node's `aliases[]`, scoped to the chip's `type`.
173
+ 4. None matched → unresolved (warning, not a parse error).
174
+
175
+ Pure syntax recognisers (parsers that don't perform resolution) SHOULD surface `id` verbatim and let downstream tools resolve. UUID-form chips remain valid forever; machine-generated content is not required to know about slugs.
176
+
177
+ ### 5.6 Modifiers
178
+
179
+ Everything after the first `|` and before the closing `]]` is the modifier string, split on un-quoted `|`. Each segment is one of:
180
+
181
+ #### 5.6.1 Display label
182
+ A double-quoted string: `[[persona:alex|"Alex (CEO)"]]`. The label is the rendered text; it does NOT mutate the entity's title in the graph.
183
+
184
+ #### 5.6.2 Inline property
185
+ An unquoted `key:value` pair matching `^([\w]+):(.+)$`. Example: `[[need:context-loss|valence:pain|severity:3]]`.
186
+
187
+ Inline properties are **assignments scoped to this reference**. Their interpretation depends on the consumer:
188
+
189
+ - **Renderers** display them as additional context next to the chip.
190
+ - **Loaders** treat the first such assignment for a given `(entity, property)` pair as authoritative; subsequent occurrences are SHOULD-warn (`DUPLICATE_INLINE_PROPERTY`).
191
+ - **Round-trippers** preserve every modifier byte-for-byte.
192
+
193
+ Conforming parsers MUST surface modifiers as an **ordered list** of `{kind: 'label', value}` or `{kind: 'property', key, value}` entries — order is normative for round-trip (§8).
194
+
195
+ ### 5.7 What chips do NOT do
196
+
197
+ Chips do not declare relationships. The relationship between two entities is an **edge** (§6) or a property assignment, not a chip. Two chips on the same line are two references; they do not form an implicit edge.
198
+
199
+ ---
200
+
201
+ ## 6. Edge references
202
+
203
+ Form: `{{src_type:src_id → tgt_type:tgt_id|verb}}`.
204
+
205
+ ### 6.1 Canonical grammar
206
+
207
+ ```
208
+ /\{\{([\w]+):([\w-]+)(?:@([\w-]+))?\s*(?:→|->)\s*([\w]+):([\w-]+)(?:@([\w-]+))?\|([\w_]+)\}\}/g
209
+ ```
210
+
211
+ ### 6.2 Endpoints
212
+
213
+ Both endpoints follow chip rules: type matches `^[a-z][a-z_]*$`, id matches `^[a-z][a-z0-9_-]*$`, optional `@product` suffix per §5.4. Endpoints inside an edge reference are **references**, not declarations — to declare an entity inside an edge, use a creation chip elsewhere in the document.
214
+
215
+ ### 6.3 The arrow
216
+
217
+ The arrow MUST be either `→` (U+2192) or the ASCII fallback `->`. Conforming parsers MUST accept both and SHOULD normalise to `→` on emit unless the consumer explicitly requests otherwise. The arrow's whitespace is permissive: `{{src→tgt|verb}}` and `{{src → tgt|verb}}` are equivalent.
218
+
219
+ ### 6.4 Verbs
220
+
221
+ The verb segment matches `[\w_]+` (lowercase identifiers with underscores). To be **resolved**, the verb MUST be a key in `UPG_EDGE_CATALOG`. Unknown verbs are warnings (§8.5), not errors — the format is forward-compatible with edge types added in later spec versions.
222
+
223
+ A verb is **canonical** when paired with the correct `(src_type, tgt_type)` — the spec catalogue keys edges by full triple. A verb that exists in the catalogue but is paired with the wrong endpoint types is `UNKNOWN_VERB_FOR_PAIR` (warning).
224
+
225
+ ### 6.5 Inline edges and fenced edges are equivalent
226
+
227
+ An inline `{{a → b|verb}}` and a fenced declaration `{ "src": "a", "tgt": "b", "verb": "verb" }` (§7) are equivalent. Conforming parsers MUST merge both into a single edge set, deduplicated by `(src, tgt, verb)`. Duplicate edges are silently de-duped, not warned.
228
+
229
+ ---
230
+
231
+ ## 7. Fenced `upg` blocks (the JSON escape hatch)
232
+
233
+ Inline chips (§5) and inline edges (§6) are the primary authoring surface. **Fenced `upg` blocks remain part of v0.1 and MUST be supported by every conforming parser** — exporters, structural import, and round-trip with `.upg` JSON depend on them.
234
+
235
+ ### 7.1 Block shape
236
+
237
+ A fenced block opens with `^```upg\b` and closes with `^```$`. The body is JSON. The JSON body MUST parse as an object with optional `declarations` and `edges` arrays:
238
+
239
+ ````
240
+ ```upg
241
+ {
242
+ "declarations": [
243
+ { "type": "feature", "id": "smart-search", "title": "Smart Search", "properties": { "status": "shipped" } }
244
+ ],
245
+ "edges": [
246
+ { "src": "feature:smart-search", "tgt": "metric:weekly-actives", "verb": "drives" }
247
+ ]
248
+ }
249
+ ```
250
+ ````
251
+
252
+ ### 7.2 Declarations
253
+
254
+ Each entry in `declarations[]` is equivalent to a creation chip `[[+type:id]]` plus inline property modifiers. Required fields: `type`, `id`. Optional: `title`, `properties` (object), `aliases` (string[]). Any additional keys MUST round-trip.
255
+
256
+ ### 7.3 Edges
257
+
258
+ Each entry in `edges[]` is equivalent to an inline `{{src → tgt|verb}}`. Required fields: `src` (string `type:id[@product]`), `tgt` (same), `verb` (string). Additional keys MUST round-trip.
259
+
260
+ ### 7.4 Malformed JSON
261
+
262
+ A malformed block is a **warning** (`INVALID_FENCED_JSON`) that affects only that block — the rest of the document MUST still parse. Conforming parsers SHOULD continue to extract inline chips and edges around the malformed block.
263
+
264
+ ### 7.5 When to use a fenced block
265
+
266
+ Use the fenced block when:
267
+
268
+ - A renderer needs property assignments that don't fit cleanly inline (e.g. a 12-field `metric_quality_assessment`).
269
+ - The same set of declarations is reused across multiple sections — declare once, reference inline thereafter.
270
+ - The document is generated by tooling that emits structural data and prose separately.
271
+
272
+ For most human authoring, inline chips suffice. Fenced blocks are the escape hatch, not the default.
273
+
274
+ ---
275
+
276
+ ## 8. Parser behaviour
277
+
278
+ ### 8.1 Required behaviour (MUST)
279
+
280
+ 1. **Parse frontmatter first.** A document with malformed frontmatter is invalid; halt structural extraction (§3).
281
+ 2. **Mask code regions.** Chips and edges inside fenced code blocks (` ``` ... ``` ` excluding `upg` blocks) and inline code spans (`` ` ... ` `` and `` `` ... `` ``) MUST NOT be extracted as references.
282
+ 3. **Honour escapes.** `\[\[`, `\]\]`, `\{\{`, `\}\}` render literal brackets and MUST NOT be extracted (§8.3).
283
+ 4. **Surface byte ranges.** For every chip and edge, expose the source byte range. Byte ranges are normative — they enable round-trip edits (§9).
284
+ 5. **Preserve unknown frontmatter keys.** Verbatim, including order (§3.3).
285
+ 6. **Preserve modifier order.** Modifier segments inside a chip MUST round-trip in source order (§5.6).
286
+
287
+ ### 8.2 Recommended behaviour (SHOULD)
288
+
289
+ 1. **Normalise the arrow** to `→` on emit, preserving the source style on round-trip (§6.3).
290
+ 2. **Warn, don't drop.** Unknown types, unknown verbs, unknown frontmatter keys, malformed individual chips — warn and continue (§8.5).
291
+ 3. **Layered ID resolution** per §5.5 if the parser also resolves.
292
+ 4. **Multiline limit.** A single reference SHOULD NOT span more than 3 lines; longer spans warn (`MULTILINE_EXCEEDS_LIMIT`) and are not extracted.
293
+
294
+ ### 8.3 Escape sequences
295
+
296
+ | Escape | Renders |
297
+ |---|---|
298
+ | `\[\[` | `[[` |
299
+ | `\]\]` | `]]` |
300
+ | `\{\{` | `{{` |
301
+ | `\}\}` | `}}` |
302
+
303
+ Escapes survive parse-and-emit round-trips byte-for-byte. A parser that "unescapes" on emit is non-conforming.
304
+
305
+ ### 8.4 Comments and incidental whitespace
306
+
307
+ A conforming parser MAY drop:
308
+
309
+ - Trailing whitespace on lines outside chip/edge ranges.
310
+ - HTML/Markdown comments (`<!-- ... -->`) outside fenced `upg` blocks.
311
+ - Duplicate blank lines outside chip/edge ranges.
312
+
313
+ A conforming parser MUST NOT drop:
314
+
315
+ - Frontmatter keys (§3.3).
316
+ - Chip or edge byte content.
317
+ - Whitespace inside chip/edge ranges.
318
+
319
+ ### 8.5 Out-of-spec, gracefully
320
+
321
+ A conforming parser **never silently drops structural content**.
322
+
323
+ | Situation | Behaviour |
324
+ |---|---|
325
+ | Chip type not in spec catalog | Emit chip with `resolved: false`. **Warning** (`UNKNOWN_TYPE`). Preserve in source. |
326
+ | Edge verb not in `UPG_EDGE_CATALOG` | Emit edge with `resolved: false`. **Warning** (`UNKNOWN_VERB`). Preserve. |
327
+ | Edge verb in catalog but wrong endpoint pair | **Warning** (`UNKNOWN_VERB_FOR_PAIR`). Preserve. |
328
+ | Modifier value invalid for a reserved key | Keep the chip. **Warning** (`INVALID_PROPERTY_VALUE`). |
329
+ | `@product` references an unknown product | Keep the reference unresolved. **Warning** (`UNKNOWN_PRODUCT`). |
330
+ | Malformed chip syntax (`[[type:]]`, missing closing `]]`) | Skip. **Warning** (`MALFORMED_REFERENCE`). Continue parsing. |
331
+ | Frontmatter invalid | **Error** (`MISSING_REQUIRED_FIELD` or `INVALID_FRONTMATTER_YAML`). Halt structural extraction. |
332
+ | Future syntax (e.g. unknown sigils inside `[[…]]`) | Treat as opaque. Preserve byte range. **Warning** only. |
333
+ | Malformed fenced `upg` JSON | **Warning** (`INVALID_FENCED_JSON`). Affects that block only. |
334
+
335
+ ### 8.6 Forward compatibility
336
+
337
+ An older conforming parser reading a newer document MUST round-trip without data loss for any construct it does not understand. This is non-negotiable. A future spec version MAY add new sigils, new modifier kinds, new fenced block types — older parsers preserve them as opaque text and warn.
338
+
339
+ ---
340
+
341
+ ## 9. Round-trip with `.upg` JSON
342
+
343
+ `.upg.md` and `.upg` describe the same product graph. Conversion between them is **lossless in one direction** and **lossy in the other**, with a clear contract.
344
+
345
+ ### 9.1 `.upg.md → .upg` (lossy)
346
+
347
+ Drops:
348
+ - Prose surrounding the chips (the document is preserved as a `document` entity with its body in `properties.body_md`, but Markdown structure — headings, paragraph layout, formatting — is intentionally not modelled in the graph).
349
+ - Modifier display labels (`"Alex (CEO)"`) — these are presentation, not graph data.
350
+ - Comments and incidental whitespace (§8.4).
351
+
352
+ Preserves:
353
+ - Every chip → entity (creation refs declare new entities; references link existing ones).
354
+ - Every edge → graph edge.
355
+ - Every inline property modifier → property assignment on the target entity.
356
+ - All frontmatter (mapped to the document entity's properties + extension fields).
357
+ - All fenced `upg` declarations and edges.
358
+
359
+ ### 9.2 `.upg → .upg.md` (lossy)
360
+
361
+ Drops:
362
+ - Edge metadata that has no inline syntax (e.g. edge weights, custom edge properties beyond verb).
363
+ - Per-entity properties that aren't surfaceable as inline modifiers (large bodies of structured data — these stay in the graph; the document references them).
364
+
365
+ Preserves:
366
+ - Every entity → creation chip or fenced declaration.
367
+ - Every edge → inline `{{src → tgt|verb}}` or fenced edge.
368
+ - Document-level metadata → frontmatter.
369
+
370
+ ### 9.3 Round-trip identity
371
+
372
+ `.upg.md → in-memory graph → .upg.md'` is **byte-stable** for any document where every chip and edge is in the spec catalogue, every frontmatter key is recognised, and no malformed-but-tolerated constructs are present. Documents using extension fields or unknown types round-trip with **content stability** (semantic equivalence) but MAY differ in byte form on the unrecognised regions.
373
+
374
+ `.upg → .upg.md → .upg'` is **content-stable** for the entity and edge sets but does not preserve prose narration (because there was none to begin with).
375
+
376
+ ---
377
+
378
+ ## 10. Worked examples
379
+
380
+ Each example below is a complete `.upg.md` document. Each is validated by the round-trip test suite in `@unified-product-graph/markdown` (`packages/upg-markdown/src/__tests__/spec-examples.test.ts`).
381
+
382
+ See `packages/upg-spec/spec/examples/`:
383
+
384
+ - `enterprise-pilot-results.upg.md` — full experiment-report shape (existing reference document).
385
+ - `persona-minimal.upg.md` — minimal viable doc; frontmatter + a single chip (§10.1).
386
+ - `feature-decision.upg.md` — feature decision doc with chips + edges + a fenced `upg` block (§10.2).
387
+ - `metric-tracking.upg.md` — metric doc using property modifiers and cross-product references (§10.3).
388
+
389
+ ### 10.1 `persona-minimal.upg.md` — minimum viable document
390
+
391
+ The smallest valid `.upg.md`. Demonstrates required frontmatter and a single inline chip.
392
+
393
+ ### 10.2 `feature-decision.upg.md` — chips + edges + fenced block
394
+
395
+ Demonstrates the full prose-first authoring shape: typed chips for the persona/feature/decision; an edge expressing the rationale relationship; a fenced `upg` block for the structural decision-record properties (the prose alone wouldn't fit them cleanly).
396
+
397
+ ### 10.3 `metric-tracking.upg.md` — modifiers + cross-product
398
+
399
+ Demonstrates inline property modifiers on metrics (`current:22%|target:30%`), the creation-marker form for newly-discovered entities, and a cross-product reference to a sibling product graph.
400
+
401
+ ---
402
+
403
+ ## 11. Versioning
404
+
405
+ The format spec is versioned independently from `@unified-product-graph/core`'s spec version (entity catalogue, edge catalogue, lifecycles) — those evolve more frequently and the format remains stable across them.
406
+
407
+ - **Format version (this document):** `0.1`. Exposed at runtime as `MARKDOWN_FORMAT_VERSION` from `@unified-product-graph/core`.
408
+ - **Spec version (`upg_version` frontmatter):** the entity/edge catalogue version the document targets (e.g. `0.2`). Independent of format version.
409
+
410
+ A document declaring `upg_version: "0.2"` and consumed by a tool that bundles `@unified-product-graph/core@0.2.x` is in-spec. Forward compatibility (§8.6) handles version drift.
411
+
412
+ A future format `v0.2` would ship as `UPG-MARKDOWN-v0.2.md` alongside this document. The format is intentionally conservative about breaking changes — most evolution happens in catalogue versions, not format versions.
413
+
414
+ ---
415
+
416
+ ## 12. Out of scope for v0.1
417
+
418
+ Explicitly **not** part of this spec:
419
+
420
+ - HTML rendering semantics, hover cards, syntax highlighting (consumer concerns).
421
+ - Live link resolution against a graph store (resolver tooling).
422
+ - Embedding semantics — search, similarity, vector indexing.
423
+ - Workspace-level multi-document indexing and cross-document linking semantics.
424
+ - Speculative future syntax — templated chips, computed properties, inline queries, conditional rendering.
425
+ - MDX / JSX components — out of scope per §4.1; prose-first means CommonMark prose.
426
+ - Block-level entity declaration syntax (e.g. heading-keyed entity blocks). The format converged prose-first; block-level structure is the fenced `upg` block (§7).
427
+
428
+ Each of the above MAY be specified in a later format version or in tooling layers above the format. None affect parser conformance for v0.1.
429
+
430
+ ---
431
+
432
+ ## 13. Cross-references
433
+
434
+ - **Reference implementation:** `@unified-product-graph/markdown` on npm — `parse()`, `validate()`, `buildIndex()`, `toPlainMarkdown()`, `updateRefs()`, TipTap converters.
435
+ - **Prose-first rationale:** UPG MD is authored for humans first; entity references are CommonMark-compatible inline chips so files render cleanly in any Markdown viewer.
436
+ - **`.upg` JSON format:** UPG MD is the literate variant of the `.upg` JSON format. See the UPG core spec for the JSON schema.
437
+ - **Site `/format` page:** [unifiedproductgraph.org/format](https://unifiedproductgraph.org/format) links to this document at a stable URL.