Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

livt - Living Text

Collaborate on board. Make it living in text.

Outcome

Collaborative outcomes evolve alongside the product as living text.

Opportunities

  • Stale Discovery: Discovery-phase artifacts are not persisted after synchronous collaboration sessions
  • Discovery-Development Gap: Persisted discovery artifacts are not leveraged in the development process

Solutions

  • Provide a CLI tool that captures collaborative outcomes as text files (YAML, Markdown)
  • Track consistency across artifacts via ID-based references with automated checks
  • Treat Formulation artifacts (Gherkin scenarios) as generated output, not the source of truth — that lives in the Discovery artifacts

Live Demo

livt dogfoods itself: its own discovery artifacts — stories, story maps, example mappings, and ubiquitous language — are published with livt build as a live demo. It shows exactly what the guides below describe.

Installation

livt ships as a single static binary. Every release is built by the release workflow on GitHub Actions, and every artifact is:

  • listed in checksums.txt (SHA-256)
  • signed with a GitHub artifact attestation (Sigstore build provenance) tying it to the exact source commit and workflow run
  • built from the module as served by the Go module proxy and verified against the Go checksum database, so the binaries can be reproduced from the public source

Pick the method that matches how much of that verification you want automated.

The mise github backend downloads the release binary and verifies its build provenance attestation by default — a tampered or foreign-built asset fails to install.

mise use "github:boykush/livt@<version>"

Or in mise.toml:

[settings]
lockfile = true # pin checksum + provenance in mise.lock

[tools]
"github:boykush/livt" = "<version>"

With lockfile = true, the asset checksum and attestation provenance are recorded in mise.lock, so later installs — on CI or a teammate’s machine — must match bit-for-bit.

Manual download

Download the archive for your platform and checksums.txt from GitHub Releases, then verify before running:

# Provenance: proves the artifact was built by this repository's release
# workflow on GitHub Actions (requires the GitHub CLI)
gh attestation verify livt_<version>_<os>_<arch>.tar.gz --repo boykush/livt

# Integrity: check the SHA-256 checksum (macOS: shasum -a 256 -c)
sha256sum --check --ignore-missing checksums.txt

tar -xzf livt_<version>_<os>_<arch>.tar.gz
install livt ~/.local/bin/ # or anywhere on your PATH

Build from source

Source installs are verified by the Go checksum database: everyone gets byte-identical source for a given version, and a silently re-pushed tag is rejected. Pin a release version rather than latest so installs stay reproducible.

go install github.com/boykush/livt@<version>

Or with the mise go backend (requires Go on your PATH):

mise use "go:github.com/boykush/livt@<version>"

Getting Started

Installation

Install with mise, which verifies the release binary’s build provenance attestation by default:

mise use "github:boykush/livt@<version>"

See Installation for all methods and how release artifacts are verified.

Quick Start

  1. Create the required directories:
mkdir -p stories discoveries/usm discoveries/example-mappings ubiquitous
  1. Create your first story in stories/my-first-story.md:
---
name: My first story
---

As a user
I want to do something
So that I get value
  1. Build and serve:
livt serve
  1. Open http://localhost:3000 in your browser. Every page has a sidebar to switch between Example Mappings, Story Maps, Stories, and Ubiquitous Language. Open Stories to find the story you just created:

Stories

Stories are Markdown files with YAML frontmatter, stored in the stories/ directory. The stories/ directory is the committed story registry: once a story candidate is committed for detailed discovery, its key is owned by stories/{story-key}.md.

Format

---
name: Story display name
---

Story body in Markdown.

The name field in frontmatter is required. The story key is derived from the filename (without .md). Story keys must be kebab-case, using lowercase letters, numbers, and hyphens. Key uniqueness is enforced by the filesystem: two committed stories cannot use the same stories/{story-key}.md path.

Example

stories/confirm-story-map.md:

---
name: Confirm story map
---

As a team member
I want to view the story map as a board
So that I can visually confirm the discovery outcomes maintained in text

This story has key confirm-story-map, which is used to reference it from story maps and example mappings.

Commit from a story map

Committing a story candidate from a story map into the registry is handled by the /story-commit skill (from the discovery-facilitator plugin), rather than a CLI command. Given the story map and the candidate, it creates stories/{story-key}.md first and then writes the same key back to the matching candidate in the story map.

Story Maps

Story maps are YAML files stored in discoveries/usm/. They define the structure of a User Story Map with activities, steps, stories, and release slices.

Format

name: Map Name

activities:
  - key: activity-key
    name: Activity Name
    steps:
      - key: step-key
        name: Step Name
        stories:
          - key: story-key
            name: Story Card Name
            release: release-id
          - name: Lightweight Story Card

releases:
  - id: release-id
    name: Release Name

ubiquitous:
  - term-key

Ubiquitous Language

  • ubiquitous is optional: each entry is a ubiquitous language term key
  • Referenced terms render as pink stickies below the board, linking to ubiquitous.html#{term-key}
  • A key with no matching term file renders as a plain pink card

Releases

  • Each release defines a horizontal divider on the board
  • Each release has an id that story cards reference with release
  • Stories with a release appear above that release’s divider
  • A release without name defaults to “Release N” based on position
  • Stories not in any release appear below all dividers
  • Story cards without key appear as plain cards and can still belong to a release
  • A story can reference only one release

Example

discoveries/usm/collaborative-discovery.yaml:

name: Collaborative Discovery

activities:
  - key: story-mapping
    name: Story Mapping
    steps:
      - key: discover-stories
        name: Discover stories
        stories:
          - key: confirm-story-context
            name: Confirm story context
            release: walking-skeleton
          - key: confirm-story-map
            name: Confirm story map
            release: walking-skeleton
          - name: Draft session outcomes
      - key: slice-releases
        name: Slice into releases
        stories:
          - key: split-release-scope
            name: Split release scope
            release: release-2

  - key: discovery
    name: Discovery
    steps:
      - key: discover-rules
        name: Discover rules
        stories:
          - key: confirm-discovery-outcomes
            name: Confirm discovery outcomes
            release: walking-skeleton

releases:
  - id: walking-skeleton
    name: Walking Skeleton
  - id: release-2

Example Mappings

Example mappings are YAML files stored in discoveries/example-mappings/. They capture the rules, examples, and questions discovered during an Example Mapping session for a story.

Format

story: story-key

rules:
  - id: R-01
    name: Rule description
    examples:
      - id: EX-01
        name: Example description
    issues:
      - https://github.com/owner/repo/issues/1
    automated: true
  - id: R-02
    name: Rule the spec no longer asks for
    retired: true

questions:
  - id: Q-01
    text: Question text

ubiquitous:
  - term-key
  • story is optional (links to the corresponding story detail page)
  • IDs must be unique within their rule or question list
  • ubiquitous is optional: each entry is a ubiquitous language term key, rendered as a pink sticky linking to ubiquitous.html#{term-key}. A key with no matching term file renders as a plain pink card.
  • issues is optional: the rule’s automation Issue URLs on implementation repos (Issue URLs only). The livt repository records the links; their state lives at the URL target. A rule without issues is unlinked.
  • automated is optional: records the judgment that the rule is actually automated by tests, which is independent of Issues being filed or closed. Absent means not automated. Set it when the rule’s automation lands; unset it when the rule changes.
  • retired is optional and applies to a rule, an example, or a question: it records that the item is no longer part of the spec. Absent means live.

Retiring an item

An item that no longer holds is marked retired: true — never deleted, and never commented out:

  • Deleting frees the ID. With R-01/R-02/R-03 on file, deleting R-03 makes R-02 the highest, so the next rule takes R-03 back. A livt://mapping/{story-key}/rule/R-03 reference already quoted in an Issue or a test comment then resolves to a different rule instead of failing — the quietest way for a reference to break. Retired items keep their IDs taken: new IDs are numbered from the max including them.
  • Commenting out loses the record. A comment is not part of the YAML structure, so any tool that rewrites the file drops it. retired: true is a field and survives.

A retired item stays readable in the file and still resolves by its livt URI, carrying retired: true so the reader can tell. It leaves the board and the Tasks page: a retired question is not an open question, and a retired rule is not waiting for a test.

Visual Layout

The board renders cards in the Example Mapping format:

  • Yellow card: Story (top)
  • Blue cards: Rules (row below story)
  • Green cards: Examples (stacked under their rule)
  • Red cards: Questions (separate column)
  • Pink cards: Ubiquitous language terms (referenced via ubiquitous, below the board)

Example

discoveries/example-mappings/confirm-discovery-outcomes.yaml:

rules:
  - id: R-01
    name: An example mapping can be rendered as a sticky view with only a story reference
    examples:
      - id: EX-01
        name: A YAML with only a story reference displays a single yellow Story card

  - id: R-02
    name: Cards are laid out following the Example Mapping format
    examples:
      - id: EX-01
        name: Rules are displayed as blue cards in a row below the Story card
      - id: EX-02
        name: Examples are displayed as green cards stacked under their Rule
      - id: EX-03
        name: Questions are displayed as red cards in a separate column

questions: []

Ubiquitous Language

Ubiquitous language terms are Markdown files with YAML frontmatter, stored in the ubiquitous/ directory. Each file is a single term, and livt renders them as a table you can browse like a database.

Format

---
name: Term display name
---

Definition in Markdown.

The name field in frontmatter is the display term. The term key is derived from the filename (without .md) and must be kebab-case, using lowercase letters, numbers, and hyphens. The body is the term’s definition.

Example

ubiquitous/story-map.md:

---
name: Story Map
---

A board to overview activities, steps, and stories alongside release slices.

Contexts

A term can be scoped to a context by putting it in a directory:

ubiquitous/
  story-map.md                # holds across contexts
  billing/
    invoice.md                # what "invoice" means in billing
  shipping/
    invoice.md                # what "invoice" means in shipping

A context is optional. Cut one when a word means different things to different parts of the domain — the DDD bounded context — and leave a term at the root when its meaning holds everywhere. Most livt repositories start with every term at the root and grow contexts only where the language actually diverges.

The directory is what makes a term unique, so invoice above is three separate terms, not one with three labels. Each has its own definition, its own row, and its own address. A term reference is therefore {ctx}/{term-key} for a scoped term and {term-key} for a context-free one, and the two never resolve to each other: a bare invoice names the root term even when billing/invoice exists.

Contexts are one directory deep. ubiquitous/billing/eu/invoice.md is not a term livt can address, and is left out of the glossary.

Visual Layout

livt build renders every term as a row on a single page at ubiquitous.html, with Term, Key, and Definition columns. A scoped term shows its context beneath its key. Each row carries an id={term-ref} anchor, so a term is linkable as ubiquitous.html#{term-key} or ubiquitous.html#{ctx}/{term-key}.

When a livt repository cuts at least one context, the page carries a filter bar above the table. Selecting a context narrows the table to that context’s terms, and the selection is mirrored in the ?context= query parameter so a filtered view is shareable. Context-free terms are not part of any single context, so they drop out while a filter is on rather than being repeated under every chip.

Referencing terms from boards

Story maps and example mappings can declare the terms they use with a top-level ubiquitous list of term references:

ubiquitous:
  - story-map
  - billing/invoice

Referenced terms render as pink stickies below the board, each linking to its glossary row, with the context shown on the sticky when it has one. A reference with no matching file renders as a plain pink card, so references degrade gracefully. See Story Maps and Example Mappings.

Commands

livt serve

Build artifacts and start a local server.

While the server is running, livt watches the input directories (discoveries/example-mappings, stories, discoveries/usm, and ubiquitous). When a file changes, livt rebuilds and reloads the page in the browser automatically, so you can preview refinements while editing.

livt serve [flags]
FlagShortDefaultDescription
--port-p3000Port to listen on
--out-odistOutput directory

livt build

Build static HTML from artifacts without starting a server.

livt build [flags]
FlagShortDefaultDescription
--out-odistOutput directory

livt mcp

Run an MCP (Model Context Protocol) server that exposes the livt repository (story maps, stories, example mappings, and the ubiquitous language). An implementation repo’s coding agent can then fetch the spec for a story or rule without reading livt’s source.

The livt repository usually lives in a separate checkout from the consumer, so point at it with --root or the LIVT_ROOT environment variable. The flag takes precedence; both default to the current directory.

livt mcp [flags]
FlagDefaultDescription
--root$LIVT_ROOT, then .Path to the root of the livt repository
--http(off; stdio)Serve over Streamable HTTP at this address (e.g. localhost:5488) instead of stdio; the MCP endpoint is <addr>/mcp

Transports

By default the server runs over stdio, spawned per consumer — the client launches livt mcp as a subprocess. stdio is the recommended form: the binding to the livt repository is read at spawn time from the consumer’s own environment, so each workspace resolves against what it declares, and a session never outlives the declaration it was started with.

Reach for --http when reachability is the problem — a client that cannot spawn processes, or several repos sharing one machine-wide server without each holding a checkout of the livt repository. It serves Streamable HTTP from one long-running process:

livt mcp --http localhost:5488

Each consumer points its MCP client at http://localhost:5488/mcp. The server is stateless and read-only, so one process backs many clients; keep git pull current on its checkout and the served spec (and spec_version) updates live. The binding trades the other way from stdio: the server fixes its repository once, at start, for every client — not per workspace. For distributing this client configuration to implementation repos, see the livt-mcp plugin. This mode assumes local use with no authentication — the server is meant to bind to localhost, not a public network.

Tools

ToolArgumentsReturns
list_storiesopportunity (optional) — an opportunity name, matched exactly against a story map display name; keeps only the stories on that map, and an unknown name yields an empty listEvery story with its key and name. Each entry links to its story resource (uri); stories that have an example mapping also include example_mapping_uri, and stories on a story map carry opportunities — one map name plus story map resource URI per map they sit on.
list_story_mapsEvery story map with its name and its story map resource URI (uri).
list_termsEvery ubiquitous language term with its key, display name, and term resource URI (uri) — including terms no board references. A term scoped to a context also carries ctx, which is part of what identifies it.

Resources

The spec itself is exposed as resources, addressable by URI (story map → story → mapping → rule → example, with questions and ubiquitous terms linked from mappings and story maps):

URIReturns
livt://story-map/{map_name}A story map: activities, steps, story cards, and releases. Committed story cards link to their story resource. {map_name} is the map’s display name (percent-encoded) — the same identifier the build output uses for story-map/{name}.html.
livt://story/{story_key}The story’s name, body, and frontmatter meta (e.g. issue), plus example_mapping_uri when a mapping exists and opportunities — the story maps the story sits on, as map name plus story map resource URI.
livt://mapping/{story_key}The story’s example mapping (rules, examples, questions, ubiquitous terms). Each rule, example, and question carries its own uri, and ubiquitous_terms resolves each referenced term to its resource URI. Retired entries are listed too, flagged — the mapping is the structural record their ids are numbered from.
livt://mapping/{story_key}/rule/{rule_id}A single rule and its examples, plus its recorded automation: issues (automation Issue URLs) and automated (whether the rule is automated by tests). Rules inside livt://mapping/{story_key} carry the same fields.
livt://mapping/{story_key}/rule/{rule_id}/example/{example_id}A single example of a rule. Example ids are numbered within their rule, so the address carries {rule_id}EX-01 alone does not identify an example.
livt://mapping/{story_key}/question/{question_id}A single question. Questions hang off the mapping rather than off a rule, so the address stops at {story_key}.
livt://ubiquitous/{term_key}A ubiquitous language term’s name and definition. This shape addresses a term whose meaning holds across contexts.
livt://ubiquitous/{ctx}/{term_key}A term scoped to one context, carrying ctx alongside its key. A context is optional and part of the address, so the same {term_key} can name one term at the root and another inside a context; the two never resolve to each other.

A retired rule, example, or question keeps resolving by its URI and carries retired: true, so a reference to it reads as retired rather than failing (or, worse, landing on whatever reused its id). Live items omit the field.

Read them with resources/read; all appear in resources/templates/list. The server advertises templates only — there is no concrete resource list and no change notification (subscribe); every read is served fresh from disk.

Every tool and resource payload also includes a spec_version field – the short git revision of the livt repository – so consumers can tell which version of the spec they are reading and detect drift.

Citing the livt repository

The handshake carries instructions, so every session tells the consuming agent how to reference the livt repository in what it produces: copy the uri from the result verbatim, and never write a bare id. Ids are unique only within one mapping file, so R-02 exists in every mapping and identifies nothing on its own.

// livt://mapping/place-order-with-saved-card/rule/R-13/example/EX-01

That applies wherever a reference leaves the livt repository – a test comment, an issue body, a commit message, a PR description. A published living-document URL is a convenience link for humans, not the citation form: it depends on where the site is deployed, and the livt URI does not.

A URI cited this way is read back with livt resolve, which needs no MCP client – so the citation stays followable for CI, an editor, or a reader who only has a checkout.

livt resolve

Resolve a livt URI against the livt repository, without running an MCP client. A rule, example, or question cited in a test comment, an issue body, or a commit message can then be read back by whoever needs it — CI, an editor, or an agent that is not connected to livt over MCP.

livt resolve <uri> [flags]
FlagDefaultDescription
--root$LIVT_ROOT, then .Path to the root of the livt repository
--formatjsonOutput form: json or url
--base-urlRoot of the deployed site; required by --format url

Every URI shape the MCP server exposes as a resource resolves here: mappings, rules, examples, questions, stories, story maps, and ubiquitous language terms.

Output forms

--format json (the default) prints the same payload an MCP resources/read of that URI serves, spec_version included — the two surfaces resolve through one code path, so a consumer sees one shape whichever way it asked:

livt resolve livt://mapping/trace-test-to-rule/rule/R-04

--format url prints the item’s page on the deployed site, using the same URI-to-page derivation the site build anchors its stickies to:

$ livt resolve livt://mapping/trace-test-to-rule/rule/R-04 \
    --format url --base-url https://boykush.github.io/livt
https://boykush.github.io/livt/mapping/trace-test-to-rule.html#rule-R-04

The livt repository is still read in this form, even though the page path derives from the URI alone: a link to a page that was never built is worse than an error.

When a URI does not resolve

Both cases exit non-zero and explain themselves on stderr, because they need different fixes:

Malformed URIThe string is not a livt URI at all. The error lists every shape one can take. Fix the URI.
Nothing to resolveThe URI is well formed but the livt repository holds no such item, e.g. rule "R-99" not found in story "trace-test-to-rule". Fix the reference, or add the item.

livt version

Print the version of livt.

livt version

File Structure

Input

project-root/
  stories/
    {story-key}.md                        # Story files
  discoveries/
    usm/
      {map-name}.yaml                     # Story map files
    example-mappings/
      {story-key}.yaml                    # Example mapping files
  ubiquitous/
    {term-key}.md                         # Terms holding across contexts
    {ctx}/
      {term-key}.md                       # Terms scoped to one context
  • Story keys are derived from filenames (without extension)
  • Story keys must be kebab-case: lowercase letters, numbers, and hyphens
  • The stories/ directory is the committed story registry, and stories/{story-key}.md provides story key uniqueness
  • Example mapping filenames must match story keys to link them
  • Term keys are derived from filenames, and a term’s context from the directory holding it. The path is what makes a term unique, so the same key can sit at the root and under a context as two separate terms
  • A context is optional and one directory deep; terms nested deeper are not addressable and are left out of the glossary
  • A term is anchored as ubiquitous.html#{term-key}, or ubiquitous.html#{ctx}/{term-key} when it is scoped

Output

livt build generates the following structure:

dist/
  index.html                              # Example mappings overview (home)
  story-maps.html                         # Story maps overview
  stories.html                            # Story list
  ubiquitous.html                         # Ubiquitous language table
  tasks.html                              # Open questions and un-automated rules
  story/
    {story-key}.html                      # Story detail pages
  mapping/
    {story-key}.html                      # Example mapping boards
  story-map/
    {map-name}.html                       # Story map boards

Every page shares a left sidebar that links the four resource types (Example Mappings, Story Maps, Stories, Ubiquitous Language) and, below them, Tasks. The overview pages render each example mapping and story map as a preview card.

tasks.html gathers what the livt repository leaves unfinished, so neither kind has to be hunted for board by board:

  • Open Questions — every questions entry across the example mappings. These close by a conversation, so they feed the next discovery session.
  • Un-automated Rules — every rule with no automated: true recorded. These close by a test, so they read as the list of behaviour still to build.

Retired items are on neither list, and off the boards as well: nothing can close them, so they would sit here forever.

Each item names the story it came from and links to its own sticky on that story’s mapping board. Both lists are filtered together by opportunity, and the selection is mirrored in the ?opportunity= query parameter so a filtered view is shareable.

Items carry no status beyond being listed. A rule records its automation issue URLs but not their open/closed state, and the build never queries the issue tracker, so an item leaves this page by being resolved — a question becoming a rule, a rule becoming automated — rather than by moving through states.

livt URI

A livt URI names one point in the livt repository — a rule, an example, a question, a story, a story map, or a ubiquitous language term. It is the form to reach for whenever a reference has to survive outside the livt repository: a test comment, an issue body, a commit message.

livt://mapping/{story-key}/rule/{rule-id}

The MCP server addresses its resources by these URIs — see Commands for what each one returns — and the generated site anchors its stickies to them. Neither depends on where the site is deployed.

Why not a bare ID

R-02 on its own is not a reference. IDs are numbered within the file that holds them, so every example mapping in this repository defines an R-02, and every rule that has examples defines an EX-01. Even R-13 EX-01 narrows it to nothing — the reader still cannot tell which mapping was meant, and neither can a search. The story key is the part that makes it addressable:

livt://mapping/trace-test-to-rule/rule/R-02/example/EX-01

Relative to a repository

The widening stops there: a livt URI never says which livt repository it belongs to. Its interpretation is uniform — one point in the livt repository at hand — the same shape http://localhost/ has, where every reader agrees on the meaning while the referent depends on where it is resolved. Which repository answers is supplied by the consumer’s own declaration (--root / LIVT_ROOT), so scope lives in the workspace, not in the name. That is what keeps a citation short, and what lets the same reference work in every checkout of the same repository.

The first segment is drawn from a closed set — mapping, story, story-map, ubiquitous — so a future form that names a repository in that position stays open without breaking any URI written today.

URI to page

livt build renders the livt repository as a static site, and this is where each URI lands in it. Paths are relative to the output root.

livt URIPage
livt://mapping/{story-key}mapping/{story-key}.html
livt://mapping/{story-key}/rule/{rule-id}mapping/{story-key}.html#rule-{rule-id}
livt://mapping/{story-key}/rule/{rule-id}/example/{example-id}mapping/{story-key}.html#rule-{rule-id}-example-{example-id}
livt://mapping/{story-key}/question/{question-id}mapping/{story-key}.html#question-{question-id}
livt://story/{story-key}story/{story-key}.html
livt://story-map/{map-name}story-map/{map-name}.html
livt://ubiquitous/{term-key}ubiquitous.html#{term-key}
livt://ubiquitous/{ctx}/{term-key}ubiquitous.html#{ctx}/{term-key}

An example’s anchor repeats its rule for the same reason its URI does: EX-01 recurs under every rule of a board, so #example-EX-01 would land on whichever one happened to be first. The sticky’s own badge still shows the local EX-01 — that is the ID the livt repository numbers — while the link behind it carries the rule.

A term takes a context the same way, and for the same reason: invoice can mean one thing in billing and another in shipping, so the context is part of the address rather than a label on it. The context is optional — a term whose meaning holds everywhere keeps the one-segment form — and the two shapes never resolve to each other.

A retired item has no sticky, so its URI lands on the board with nothing to scroll to. The URI still resolves: ask the tooling, which answers with the item and retired: true. Deriving a page is how a reference is shown, not how it is resolved.

Store the URI, render the URL

Nothing in the livt repository records where the site is deployed. The deployment URL is prefixed onto the paths above only while a page is being rendered, which is why the same livt repository can be served from a local livt serve, from GitHub Pages, and from an internal host at once:

livt://mapping/trace-test-to-rule/rule/R-02
  -> mapping/trace-test-to-rule.html#rule-R-02
  -> https://boykush.github.io/livt/demo/mapping/trace-test-to-rule.html#rule-R-02

A deployed URL is therefore a fine thing to paste into a chat and a poor thing to commit: it goes stale the moment the site moves, and it buries the identity it was meant to carry. Keep the URI; let the URL be derived.

Where each form comes from

  • A URL, to send to a person — take it from the board. Every sticky shows its own ID in the bottom-right corner; clicking it copies the deployed URL of that sticky, so what you paste already points at the exact card.
  • A livt URI, to leave in an artifact — take it from the tooling rather than the board. Every rule, example, and question the MCP server returns carries its own uri, so an agent writing a test can quote the point it is proving without composing the URI by hand.

Keeping the two apart is what stops the site from becoming a second source of identity. The board is for reading and sharing; the URI is what the livt repository actually knows about itself.

Reading one back

A URI left in a test comment is only worth as much as the ability to follow it, and that has to work for a reader who is not running an MCP client — CI, an editor, or someone with just a checkout. livt resolve turns any of the shapes above back into the point it names:

$ livt resolve livt://mapping/trace-test-to-rule/rule/R-02
{ "spec_version": "...", "rule": { "id": "R-02", ... } }

$ livt resolve livt://mapping/trace-test-to-rule/rule/R-02 \
    --format url --base-url https://boykush.github.io/livt
https://boykush.github.io/livt/mapping/trace-test-to-rule.html#rule-R-02

The URL form is derived from the table above rather than restated, so a link the CLI hands out and the anchor the build writes cannot drift apart.