Skip to content

feat: turn the graph into documents a reader can open, and keep them as blocks - #179

Merged
mroops0111 merged 1 commit into
masterfrom
feat/view-axis
Sep 15, 2026
Merged

mroops0111 merged 1 commit into
masterfrom
feat/view-axis

Conversation

@mroops0111

Copy link
Copy Markdown
Owner

Closes #174. Closes #130. Supersedes #147.

The view-generator axis was fully declared and never called. ViewGeneratorPlugin had viewKind, configSchema, render and skills, the registry had three ways to reach one, and nothing shipped a generator, nothing called render, and ViewArtifact.format had no reader. Meanwhile braid:generate-doc wrote CommonMark into a path that resolves on the server host, so against a remote workspace a user could run the skill and never read what it wrote.

This wires the axis end to end, and settles what a document is made of.

What changed

A document is a stored sequence of render calls. The first cut wrote HTML fragments, and that was rejected on token economics. An HTML form has to spell out everything a reader sees, including the half the projection already knows: every heading, every node name, every status, every id in the footer. A block form emits calls against a fixed schema and the surface draws the rest. Measured on the same subject, the block form's output is about 61% smaller, and the deterministic half never passes through model output at all. Both are not written, because paying twice for the same information is the thing this was trying to avoid.

The writing is split in two. @braidhq/view-generator-doc projects a subject into material JSON, which is a function of the graph, so the same graph and subject give the same bytes. Two skills, reference and tutorial, read that material and write the page. Neither is handed a template, because a generator that dictates the shape of an explanation gets a document that obeys the shape and teaches nobody. What the plugin decides is what is in scope and in what order; what the sentences say is the skill's alone.

Order comes from the ontology, not from a type name. The projection walks renderHint: containers open a document, expandedUnder chains give the nesting, section types become flat groups. Nothing in the package names a DDD type, so a second ontology gets documents without this code changing. Nodes attached further up their own chain are recovered in a second pass, which is what stops a rule hanging off an aggregate from silently disappearing.

Staleness falls out of the split. Project the subject again and compare against the material the document was actually written from. Because render is a function, an edit that changes nothing a reader would read leaves every form of it current, and one that does is marked on the shelf. It compares the whole projected material rather than the container node, so a change three levels down still counts.

Three render calls for what a document needs and an answer does not. showSection is a heading with a level and the nodes it covers. showCheck is a question at recall, apply or judge, with optional choices and which one is right, refused at the schema if it offers choices and names no answer. showCustom carries a kind its own plugin declares a schema for: Braid validates it and refuses an unclaimed kind, but never draws it. A plugin wanting its own shape drawn renders it in its own surface, against its own design system.

Every render route is now scoped to the runs that may call it. forRuns() existed and had been applied to none of them, so an ask run was offered show_matrix and a generate run was offered everything. A render call costs a place in the tool list and the tokens to describe it whether or not it is ever made. The three new calls reach generate runs alone.

Which nodes a kind can be written about is declared. A generator says subjects: [{ by: 'container' }], which reads the ontology's hint, or { by: 'type' }, which names a type it ships with. Omitted takes any node, which is the honest answer for a kind with no opinion rather than a silent narrowing. The doc generator takes containers; a future generator over a metric or a single aggregate declares its own rule and needs no change here.

Format is declared rather than guessed. A form says what it writes, and the surface reads that declaration instead of the file extension. That is what lets a form written later produce something else, and what makes a stale file in a format its form no longer writes disappear from the list rather than sit beside the current one under the same name.

A Documents surface in Studio. Shelved by what each document is about rather than listed as paths, since a reader thinks in subjects and not in filenames. Each subject shows the forms it has and says when the graph has moved on. Writing one picks the form first and the subject second, because the form is what decides which subjects are available. Documents are drawn by the same block renderers the Ask surface uses, so an answer and a document are one set of components.

A resolved reference shows its node's name. It used to print the id inline. A reader meeting ctx.remoteSigningTask mid-sentence has been handed the machinery rather than the thing, so the name is set in the surrounding type and the id stays a hover away. This lands on Ask and the Inbox too.

Breaking

  • braid:generate-doc is gone. It becomes the reference form of the doc view kind. Nothing read its output, so no reader is broken by the move.
  • GET /workspaces/:id/views, GET /workspaces/:id/views/:kind/:form/:subject and POST /workspaces/:id/views are new, along with GET /view-kinds.
  • defineViewGeneratorPlugin takes subjects and format on each form.

Verified

pnpm lint clean, pnpm typecheck 16/16, and the package suites: schema 322, core 512, studio 281, sdk 29, view-generator-doc 16, server 606. Server drops a worker on a different file under turbo contention and passes 606/606 in isolation, which predates this branch. storage-kuzu fails on the native crash of #58, and no file in that package is touched here.

End to end in the browser against a real workspace: both forms written on the same container off one projection; a document read back after the run, drawn from its stored blocks; the graph edited and the shelf marking the subject stale, then current again after a rewrite; a path carrying a separator refused before a handler sees it.

Not doing

🤖 Generated with Claude Code

https://claude.ai/code/session_01PL1PNnaEeFuZA9fdDJCpMP

@mroops0111
mroops0111 force-pushed the feat/view-axis branch 2 times, most recently from 5a3a359 to 1f4630b Compare September 15, 2026 06:18
…as blocks (#174)

The view-generator axis was fully declared and never called. Nothing shipped
a generator, nothing called `render`, and `ViewArtifact.format` had no reader,
so `braid:generate-doc` wrote markdown into a server path nobody could reach.

This wires the axis end to end, and settles what a document is made of.

A document is a stored sequence of render calls, not a page of markup. An HTML
form has to spell out every heading, name, and status the projection already
knows. A block form emits calls against a fixed schema and the surface draws
the rest, which measured about 61% smaller on the same subject. The
deterministic half never passes through model output at all.

The writing is split in two. `@braidhq/view-generator-doc` projects a subject
into material JSON, which is a function of the graph, so the same graph gives
the same bytes. Two skills, `reference` and `tutorial`, read that material and
write the page. Neither is handed a template, because a generator that dictates
the shape of an explanation gets a document that obeys the shape and teaches
nobody.

Staleness falls out of that split. Project the subject again and compare
against the material the document was written from, so an edit that changes
nothing a reader would read leaves every form of it current.

Three render calls are added for what a document needs and an answer does not:
`showSection`, `showCheck`, and `showCustom`. A custom block carries a kind its
own plugin declares a schema for. Braid validates it and refuses an unclaimed
kind, but never draws it, so a plugin wanting its own shape renders it in its
own surface.

A render call costs a place in the tool list whether or not it is ever made,
so every render route is now scoped with `forRuns`, which existed and had been
applied to none of them. The three new calls reach `generate` runs alone.

Which nodes a kind can be written about is declared rather than hardcoded.
A generator says `{ by: 'container' }`, which reads the ontology's hint, or
`{ by: 'type' }`, which names a type it ships with. Omitted takes any node.

Studio gets a Documents surface, shelved by what each document is about rather
than listed as paths, drawn by the same block renderers the Ask surface uses.
A resolved reference now shows its node's name instead of its id, because a
reader meeting `ctx.remoteSigningTask` mid-sentence has been handed the
machinery rather than the thing.

The AG-UI `braid.artifact` announcement stays open. The service assembles the
document after the run ends, so the skill is not what knows its address, and
emitting into a completed run is a shape the runner does not have.

Closes #130. Supersedes #147.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PL1PNnaEeFuZA9fdDJCpMP
@mroops0111
mroops0111 merged commit cc6175f into master Sep 15, 2026
6 checks passed
@mroops0111
mroops0111 deleted the feat/view-axis branch September 15, 2026 06:38
mroops0111 added a commit that referenced this pull request Sep 18, 2026
* docs: describe the product as the block protocol left it

The README last changed substantially in #105. Four merges since then moved
what it describes: #164 gave a run's output a typed block protocol and put
AG-UI on the wire, #179 wired the view axis end to end and added Documents,
#178 named a Handoff, and #182 retired the Actions surface.

Root README: Features gains typed blocks and AG-UI, Design gains a section
for each of the three mechanisms, Architecture stops saying a run travels
over SSE, and The Loop stops routing a reader through an Actions tab and two
review surfaces that no longer exist. A Deployment section names the two
variables whose absence costs an afternoon. Two shipped packages were missing
from the table.

Package READMEs: studio described seven surfaces and a run it reads over SSE,
schema listed a module that does not exist and omitted eight that do, core
still called a Clarification a ClarifyTicket, server claimed a per-run gateway
reads the whole spec, which forRuns ended, ontology-ddd omitted scan, and
source-loader-mcp never followed the shape the other packages use.

The demo gif and the architecture diagram still show the old surfaces. Both
are follow-ups.

Closes #172.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: make the README a front door rather than a manual

The first pass fixed what was false. This fixes what it spends its space on.

A worked example, which the README never had. `examples/conciergent` has been
in the repository the whole time: 120 nodes and 159 edges derived from one real
codebase with no intent documents, the reviewed clarifications and proposals
alongside. Every claim above it was abstract, so a reader could only believe or
not. One rule quoted in full, with the two source lines it was drawn from, is
the shortest way to show what a model a PM can read actually looks like.

Five features, not seven. Past five a reader stops ranking and starts skimming,
and the most distinctive item gets the weight of the most generic. Blocks and
AG-UI were one story told twice, so they are one bullet. Git-versioned history
becomes a clause of the gate it belongs to.

AG-UI moves to a badge. "We speak a standard protocol" is a credibility claim,
and a badge is where a project makes one. It costs no space in a list whose job
is to say why a reader should care.

The Deployment section goes. It is operator documentation, and
`packages/server/README.md` has held it since #117. Quick Start's job is to get
something running, and twelve lines about containers and a bare 401 belong
after that, not inside it. One sentence points at the two places that have it.

Quick Start now says what is needed first. A reader could install, create a
workspace, press run, and watch nothing happen, because a skill run is a
`claude` subprocess and the README never mentioned it.

Motivation gains the objection a reader actually has now. The two failure modes
it argued against were the 2024 ones. "Why not point an agent at the repository"
is the current one, and the answer is the thesis: the graph is not an index that
makes retrieval faster, it is where judgement is put so it outlives the
conversation.

A Status section, because a public repository at 0.6.0 that never says where it
stands reads as either abandoned or overclaiming.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: say what the opening paragraph promises

The Features list had five bullets and did not contain the one the tagline
sets up. "They drift apart every sprint, and the team ends up arguing about
which one is right" is the problem statement, and nothing below it said Braid
finds the disagreement or names who settles it.

Named Disagreement is new. CoverageState keeps `sourceChanged` and
`conflicted` apart on purpose, because reading a changed source again is
mechanical and a model contradicting its own evidence is not. A DriftIssue
carries at least two references for the same reason: the point is to name
which two things disagree. A person fixes the source or records the
difference as intended. This is the thesis, and it was the missing bullet.

Evidence-Backed Claims said a node traces back to "the file or document it
was drawn from", which is neither the vocabulary (a source) nor the useful
part (the line range). It now says both, and says that a node missing
evidence for a source role the ontology asks for names the gap.

Typed Output led on not being a wall of text, which is the least of it. The
point is that the model fills in a schema instead of choosing a presentation,
so a surface lays the result out, and the audiences an ontology declares
decide how much of a reference each reader sees. AG-UI stays, as the clause
it should always have been.

Docs That Never Drift promised something stronger than the code does. A
document is not regenerated on a graph change, it is marked stale, and the
honest claim is the one worth making: it is projected rather than maintained,
and it cannot go quietly wrong.

Continuous Reaction leaves the list. The reactor is `enabled: false` by
default, so a headline feature overstated it. Architecture still covers it.

The Architecture Model bullet gains the drift clause, so the new feature has
somewhere to land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: one section per mechanism, and put the reactor back

Four corrections, all from review.

The reactor should not have left the Features list. Default-off is a
deployment choice, not a statement about whether the feature matters, and
a graph that does not keep up with its sources is a snapshot of the day it
was built. That is the whole premise, so it goes back as its own bullet.

"Documents Regenerated, Not Maintained" named an implementation detail and
sold nothing. The point is the axis: a view kind is a plugin, so a document
is one kind of view rather than the only one there can be, and the next kind
is a plugin rather than a fork. The bullet is now Any Kind of View.

Typed Blocks and AG-UI were two sections for one mechanism, and they sat at
the same heading level as Framework and Architecture, which organise the
section rather than belonging to it. They merge into A Run's Output: a skill
fills in a schema, the surface lays it out, the ontology's audiences decide
how much each reader sees, and AG-UI carries the result. One purpose, one
section.

Reading the Graph is new, and covers what a reader asked for: Ask, views of
whatever kind a generator declares, and the read-only MCP endpoint, which
exchanges each caller's own token so a person's MCP client reads the graph
as them. That endpoint has been serving since #148 and appeared nowhere but
the server package.

Retrieval Alone was unreadable. It now says the plain thing: an agent
pointed at a repository answers the question and answers it again from
scratch next time, because nothing a reviewer concluded is written down
where the next question can reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: drop the worked-example section, keep the pointer

The section duplicated the recording. The gif already shows this workspace,
so a second telling in prose, with an 838KB graph render pushing Motivation
and Design down the page, bought nothing the reader had not just watched.

What was worth keeping is that the example is reachable, so one line sits
under the gif and says what is in it. It names no scene in the recording, so
re-recording against a different workspace does not make it wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: cut the gif caption to one line

A caption competing with the recording above it is not a caption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: show Braid running, in three clips, and say what extending it actually costs

The README opened on a clip of an interface that no longer exists, beside a
diagram drawn before tagging, embeddings, Ask, Documents, and the one Inbox
queue. Three clips replace it, each recorded as a single continuous take at
the viewport's own size, and `architecture.pen` is redrawn on a four-column
grid against the current text, with the git timeline dropped and Intent and
Code folded into one Sources block.

Only the hero autoplays. Three looping clips in one document is motion in the
corner of the eye for the whole read, so the two in Reading the Graph are
stills that link to their clip, and both sit together above the bullets rather
than with prose between them. All three are animated WebP, which halves the
bytes while raising the frame rate, and moves the README's first load from
4.2 MB to about 2 MB.

Media moves to `.github/assets/`, since it belongs to no package and ships in
none, and each file is named for what it shows rather than for being a demo.

Extending Braid said less than it should about what it costs. A plugin is
registered where you start the server, so swapping an axis means owning the
entry point rather than configuring `braid serve`. Of the three skill
categories only `build` is one a workspace can add by itself: Ask reaches for
a single `ask` skill, and a `generate` form is declared by a view generator
alongside the skill that writes it. `EXTEND.md` is documented for the first
time, being the way to add rules to a built-in skill without replacing it.

Reading the Graph now says that search ranks by meaning wherever an embedding
endpoint is configured, which the graph surface has done for a while and the
README never mentioned.

The badges are all read from a registry now. A hardcoded `TypeScript-5.7` goes
stale in silence and tells a reader nothing they can act on, and npm version
and downloads say something checkable in its place. `@braidhq/cli` declares
`engines.node` so that install warns on Node 20, and so the same badge can be
read off the package once it publishes.

Four interface faults the clips exposed are fixed alongside:

- The Inbox filter read `Asked`, which collided with the Ask surface. It reads
  `Questions`, like the kind it filters.
- The in-flight run banner printed `/ddd:extract`. A skill declares its own
  name now and the banner shows it, localised like the ontology's types.
- The Documents stale badge was the one outline badge of twelve not set in
  uppercase with wider tracking, so a status read as a phrase. The string is
  untouched, because the detail banner reads the same words as a sentence.
- The Documents empty state sat low in its column, squeezed by the shelf list
  beside it. It shares the scroll box rather than sitting below it.

The GitHub source loader reported a clean sync that wrote nothing when the App
was not installed on the repository. GitHub answers an uninstalled repo with a
null repository rather than an error, and `?? []` read that as "no linked PR",
which filed every issue as unrealized. It now names what to install and where.

`.env.example` and `compose.yaml` name the two variables that loader reads, and
`.env.example` no longer appends `/v1` to the embedding host, which the server
appends itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* docs: read the Node requirement off the repo, and stop opening on a caveat

The Node badge was held back because `shields.io/node/v/@braidhq/cli` reads
`engines` off the published package, which has none until the next release, so
it renders a grey `not specified`. A dynamic JSON badge reads the same field
off the default branch instead, which is right today and stays right, and can
be swapped for the canonical npm form once a release carries `engines`.

`Software by default.` opened the positioning paragraph. It was added to keep
`A shared model of your business` from clashing with `Braid Anything`, but a
reader cannot recover "software is the default domain" from those three words,
and the first bold sentence of a README is the worst place for a hedge. The
clash it was solving is already settled where it belongs, in `Braid Anything`
naming a codebase, a research corpus, and a product spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* docs: name the agent without hedging, and say what the harness is for

`Claude Code today` guarded against a reader concluding Braid is tied to one
agent, but a roadmap hedge is a weak guard and Swappable Axes already names
the agent as a plugin two screens down. The agent is named without apology.

`What comes out is a model, not a patch` was the second negation-as-punchline
in three paragraphs, after `not another code graph`, and `patch` is not really
the opposite of what a run produces. The paragraph lands on what the two
limits above it are there for instead, which is also the inversion the
AI-Native Design section opens on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* docs: read the architecture legend off the diagram, and level the four loop steps

The legend under the diagram was written against the version before the
redraw, so it described boxes that had moved or gone.

`History` was a group of its own and is not one any more. Git sits in the
Engine column now, reached by the commit arrow off a landed Handoff, and the
`model.json` snapshot sits in Model under Storage. The bullet is gone and both
facts are stated where the diagram puts them, which leaves five bullets for
the diagram's five groups instead of six for five.

Four smaller disagreements went with it. The wire is `REST · SSE · AG-UI` and
the legend named two of the three. Sources gained the Reactor, which the
legend covered as "kept in sync from there" without naming it or saying it is
opt-in. Model claimed the node-versus-evidence disagreement, which is a real
thing the product records but not a box in this diagram, and which Motivation
and Cross-Source Comparison already carry. Reads opened on "the graph itself",
which is not in the Reads column at all, the graph being in Model.

The Loop's four steps were uneven in a way that read as ranking them. Two
opened with `open X, which ...` and two did not, Review carried the only
sentence of rationale in the list, and Apply was thirteen words against
thirty-seven. Each step now names its act first and runs to about the same
length, and Review's rationale goes to Architecture, where the same sentence
was already doing the work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* fix(studio): call the GitHub credential what the server calls it

Three strings said `OAuth app` where the server says GitHub App throughout:
`GitHubOAuth.ts` is a user-to-server client, `composeApp.ts` builds a "GitHub
App client", and the loader's own new error tells a reader to install the App.
The two are different artifacts registered in different places, so a reader
following the dialog would have created the wrong one and then had nowhere to
install it.

The install step is in the dialog now as well. An uninstalled App answers a
repository query with a null repository rather than an error, which is the
failure the loader's error message exists for, and the dialog is where
somebody is standing when they can still avoid it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* chore(keycloak): hold the init script's comments to the house style

Punctuation and line length already held. Length did not. Four blocks ran
three to nine lines of justification, and a comment that has to be read as an
essay stops being read at all.

Each one keeps the part a reader cannot get from the line under it and drops
the reasoning around it, which takes the file from 34 comment lines to 27
while losing no fact. The two blank `#` separators go with them, since a
comment that needs paragraphs is documentation in the wrong place.

The block before the redirector carried two unrelated facts, why a default
provider exists and why the executions listing is the idempotency signal. The
second now sits beside the line that reads it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

* chore: break every comment this branch added at a clause, not at column 80

The lines were wrapped greedily against the budget, which put breaks inside
phrases. `answers with the whole / list` split a noun phrase, `issues an
audience / the requesting client's scope` split a noun from its relative
clause, and `the only signal saying / whether this ran before` split a verb
from its object. A line that ends mid-phrase has to be rejoined before it can
be read, which is the cost the budget was supposed to avoid.

Every comment line now ends at a clause, after a comma before its conjunction
or at the end of a sentence, so line lengths vary and each line reads alone.
Where a clause would not fit, the wording came down instead of the break
moving, and four lines that ran one or two over with their indent counted came
down with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXXoZwM5RfHUzW6vScoeqv

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant