feat: turn the graph into documents a reader can open, and keep them as blocks - #179
Merged
Merged
Conversation
mroops0111
force-pushed
the
feat/view-axis
branch
2 times, most recently
from
September 15, 2026 06:18
5a3a359 to
1f4630b
Compare
…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
force-pushed
the
feat/view-axis
branch
from
September 15, 2026 06:34
1f4630b to
ab42493
Compare
This was referenced Sep 15, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #174. Closes #130. Supersedes #147.
The view-generator axis was fully declared and never called.
ViewGeneratorPluginhadviewKind,configSchema,renderandskills, the registry had three ways to reach one, and nothing shipped a generator, nothing calledrender, andViewArtifact.formathad no reader. Meanwhilebraid:generate-docwrote 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-docprojects a subject into material JSON, which is a function of the graph, so the same graph and subject give the same bytes. Two skills,referenceandtutorial, 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,expandedUnderchains give the nesting,sectiontypes 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
renderis 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.
showSectionis a heading with a level and the nodes it covers.showCheckis a question atrecall,applyorjudge, with optional choices and which one is right, refused at the schema if it offers choices and names no answer.showCustomcarries akindits 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 anaskrun was offeredshow_matrixand ageneraterun 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 reachgenerateruns 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.remoteSigningTaskmid-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-docis gone. It becomes thereferenceform of thedocview kind. Nothing read its output, so no reader is broken by the move.GET /workspaces/:id/views,GET /workspaces/:id/views/:kind/:form/:subjectandPOST /workspaces/:id/viewsare new, along withGET /view-kinds.defineViewGeneratorPlugintakessubjectsandformaton each form.Verified
pnpm lintclean,pnpm typecheck16/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-kuzufails 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
node.descriptionas well.finishhook on the run. A form's last act is the last block it emits, and the service takes the sequence off the run's log. A hook letting a form say "this is the document" would be a second way to say the same thing.braid.artifactannouncement. The blocks themselves already reach a client asbraid.block. What is missing is the event saying a document now exists at this address, and the service assembles it after the run ends, so the skill is not what knows the address and emitting into a completed run is a shape the runner does not have. Recorded on A generated document has one form, one format, and no reader, because the axis built to carry it is never called #174 rather than rushed.🤖 Generated with Claude Code
https://claude.ai/code/session_01PL1PNnaEeFuZA9fdDJCpMP