Skip to content

Structured descriptions, the transcript query object, a read-only MCP server, and capability guides - #790

Merged
lmoresi merged 10 commits into
developmentfrom
feature/describe-render
Sep 27, 2026
Merged

lmoresi merged 10 commits into
developmentfrom
feature/describe-render

Conversation

@lmoresi

@lmoresi lmoresi commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

This PR now carries the whole stack (#792, #796, #802, #803 merged into it after review), on top of #716.

  • Descriptions and views (describe(), uw.render, uw.view): every core object says what it is once, as data; one renderer shows it as Markdown, text, LaTeX, YAML or JSON; twenty-five notebook-only viewers deleted. describe_class() on every family; uw.capabilities() is the catalogue. Docs: subsystems/describe-and-view.md.
  • uw.Transcript: one query object over a run's record (abandoned steps with what stopped them, rewinds with the reason given, failed and capped solves, step patterns, a part at a step, compare two steps). The record gains abandoned_by, rewind(reason=, **detail), and part records that are written again when a parameter changes. Docs: subsystems/transcript-query.md.
  • MCP server (python -m underworld3.mcp, .mcp.json): read-only tools over transcripts, the capabilities catalogue and the guides. The mcp package is not in pixi.toml yet; the test skips without it. Docs: guides/mcp-server.md.
  • Capability guides are docs: .claude/skills are symlinks to pages in docs/developer/guides/ with front matter naming the families they apply to; two new guides (transport schemes, boundary-condition rulings) need Louis's rulings on the marked points; the review contract and CLAUDE.md carry the rule; test_0030 enforces the single source.

Adversarial review posted below; its must-fixes are in the last commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V

lmoresi and others added 2 commits September 25, 2026 15:22
…any form

Every core object now says what it is once, as data, and one renderer
shows it. describe() returns a plain tree of kind, name and summary with
facts, terms, forms, conditions and children as the object has them;
uw.render turns the tree into Markdown with mathematics, plain text, a
LaTeX fragment, YAML or JSON, and view() is that render in the form the
session wants. The solver already described itself for the transcript;
constitutive models, time histories, meshes, mesh and swarm variables,
swarms and the model join it, and the twenty-five notebook-only
_object_viewer methods they carried are gone. A class without a
description still falls back to its viewer.

Along the way: the Model class carried two view() methods, the second
silently overriding the first; a history's description now names its
scheme, order and weighting, which is the method metadata a run record
needs; the transcript's part record no longer takes the description's own
kind or children; a constitutive model lists each parameter once, since a
property alias is the same descriptor under another name.

Contract test for every kind in every format, with the serial formats
round-tripping and the transcript's part record unchanged (test_0017);
the developer page describe-and-view.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
…s it lacked

The transcript said what a run did; asking it anything meant walking the
JSON by hand, and the digest, a notebook and a test each did that their
own way. uw.Transcript reads a file, a live model or read_transcript's
list and answers the questions a debugging session asks: the steps that
were abandoned and by what, the rewinds and why, the solves that failed or
ran capped, the run collapsed to its distinct step patterns, what a part
was solving at a given step, and what changed between two steps. Every
answer is the record's own data, with the outcome of a solve read by the
rule the figure and the table use, and describe() gives the run in the
same shape as every other object. The renderers take the query object as
a source.

The record gains what the questions needed. A step stopped by an
exception carries abandoned_by, the exception's class and message. A
rewind takes a reason and detail from the caller, since the acceptance
test lives in the caller's loop and the transcript cannot infer it. A
part record carries the solver's run-time constants, the clock excluded,
and is written again when they change: before this a parameter changed
between solves left the record quoting the old value, since nothing was
rebuilt.

Two things found on the way. After a rewind the run numbers its steps
again from where it went back to, so an index can name several attempts;
the query object keys its patterns by position and exposes the attempts.
And the mesh view tested uw.is_notebook as a flag when it is a function,
so every serial run tried to plot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
python -m underworld3.mcp speaks MCP on stdio and answers questions about
runs from their transcripts: the runs under a directory, one run's
summary, its steps paged, its step patterns, everything that went wrong
or went back, events filtered by kind, part, outcome or step, one step
with its attempts, what differs between two steps, the parts that acted,
what a part was solving at a step in three levels of detail, the key, the
adjoint segments, and any description record rendered in another form.
Every tool is a projection of uw.Transcript and the description layer and
returns the query's own answer as YAML, so what a model is told is what
the digest, a notebook and a test read. Nothing runs a model or writes to
one; every tool is annotated read-only.

The repository's .mcp.json registers it for Claude Code under the name
"underworld" through scripts/mcp-server.sh, which starts it in the
checkout's pixi environment. The mcp package (2.x) is not in pixi.toml
yet; the guide says how to install it into an environment meanwhile, and
the test skips without it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
lmoresi and others added 3 commits September 27, 2026 10:06
…e class; view() on a class renders it

view() on a class rendered the docstring and nothing else, and the
instance view rendered the description: two paths. Now a family describes
itself with no instance and no mesh. describe_class() reads what the
class declares: the residual templates with their symbols and
descriptions, the terms in _solver_terms, the add_*_bc methods it
accepts, a constitutive model's parameter descriptors with symbol, units
and description, a history scheme's defaults, and the docstring as its
documentation. view() on a class renders that record, and
class_documentation=True on an instance renders the family before the
instance, so the two views are one renderer over two records.

The MCP server gains uw_capabilities, the catalogue of every solver,
constitutive model and history family built from those records, and
uw_capability for one family in full. "Can Underworld solve this" is
answered from the classes and cannot drift from them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
…rd for notebooks and the server

The class-level description exists for discovery, so the catalogue built
from it belongs in the library rather than in the MCP server. uw.capabilities(kind,
detail) gathers every solver, constitutive model and history family into
one description record, a child per group and a child per family, one line
each or the whole class record; uw.view renders any record, so a notebook
reads the same catalogue a tool does. The server's uw_capabilities and
uw_capability are now consumers of it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
…ts guides

Curated guidance the code cannot state about itself — which transport
scheme, which boundary treatment, how to make a hard solve converge — was
split between the AI skills in .claude/skills and the rulings in CLAUDE.md,
two paths with no index and no reader outside an assistant. Each guide is
now one MyST page under docs/developer/guides with front matter naming the
families it applies to; the seven skills are symlinks to those pages, so a
skill cannot drift from its guide. Two guides are new: transport schemes,
drafted from what the tests established this month with the rulings still
open marked as such, and the boundary-condition rulings, gathered from
CLAUDE.md and the issues.

uw.capabilities() reads the front matter and lists a guide beside its
family; the class-level view does the same, so uw.systems.Stokes.view()
ends with the guides that apply to Stokes; the server serves them with
uw_guides and uw_guide. The developer index carries the table. The
adversarial review contract gains the rule, and CLAUDE.md the pointer: a
change to a family is reviewed against every guide that names it, in the
same change. test_0030 fails on a copied skill, a guide without front
matter, or a family no class carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
@lmoresi
lmoresi changed the base branch from docs/timestepping-pattern to development September 27, 2026 18:24
The level-0 view tested uw.is_notebook as a flag, so every serial run
tried to plot the mesh through pyvista; on CI that import fails once
another test has touched matplotlib (test_0017, test_view_prints_outside_a_notebook).
The same line is fixed in #792; taken here so this PR is green on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
Copilot AI lite review requested due to automatic review settings September 27, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

lmoresi and others added 2 commits September 27, 2026 11:46
From the adversarial review of the stack before merging. Three must-fixes:
mesh.view() described on rank 0 only while the cell-quality summary in
describe() is a collective, so a two-rank view deadlocked; every rank now
describes and the renderer prints on rank 0. Part records were written
again every step whenever the timestep changed, since each history's
\Delta t is a run-time constant; the timesteps are left out beside the
clock, and a record is written again only when a parameter changes.
Swarm.__init__ had lost its timing decorator to the describe() inserted
between them.

And the rest: displaying an instance renders its description rather than
a subclass view with side effects (a mesh no longer plots itself when
evaluated in a cell); a fact that said the same for every solver family
is gone; Model.view honours show_materials; the guides are read once per
change of the directory rather than per class description; the constants
in a part record are documented, with their frame.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
@lmoresi lmoresi changed the title describe() as the one structured introspection; view() renders it in any form Structured descriptions, the transcript query object, a read-only MCP server, and capability guides Sep 27, 2026
@lmoresi

lmoresi commented Sep 27, 2026

Copy link
Copy Markdown
Member Author

Adversarial review of the stack (development..feature/capability-guides, read-only, 42 checks), findings and what was done.

Must-fix, all fixed in the last commit:

  1. Mesh.view() deadlocked under MPI: describe() calls quality(), a collective, and the view called it on rank 0 only. Verified with mpirun -n 2, rank 0 never returned. Now every rank describes and the renderer prints on rank 0; re-verified, both ranks return.
  2. Part records were written again every step whenever the timestep changed: each history's \Delta t is a run-time constant and only the clock was excluded. Three AdvDiffusion steps at halving dt gave three 10 KB records. Timesteps are now excluded beside the clock; re-verified, one record at step 0 and one when the diffusivity changed.
  3. Swarm.__init__ had lost @timing.routine_timer_decorator to the describe() inserted between them. Restored; verified by __wrapped__.

Should-fix, done: facts["time dependent"] said True for every family (removed); the constants key in a part record was undocumented and its frame unstated (documented: nondimensional packed values, timesteps and clock excluded); describe.view() printed on every rank (rank 0 only now); _ipython_display_ on a mesh triggered a PyVista plot (instances now display their description, never a subclass view); Model.view(show_materials=False) was ignored (honoured); guides() re-read every page per describe_class() (cached per directory change).

Noted, not changed: test_0019 skips in CI since mcp is not in pixi.toml, so the server has no CI coverage until that dependency is decided; uw_object.view moved class_documentation to the third positional slot, no in-repo caller passes it positionally; Mesh.describe()["facts"]["cells"] is the rank-local count, as the old viewer's was; symlinked skills need core.symlinks on a Windows checkout; mesh.view(1) is slow on the boundary table, pre-existing.

@lmoresi
lmoresi merged commit 1391e4a into development Sep 27, 2026
2 checks passed
@lmoresi
lmoresi deleted the feature/describe-render branch September 27, 2026 21:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants