Structured descriptions, the transcript query object, a read-only MCP server, and capability guides - #790
Conversation
…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
…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
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
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
|
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:
Should-fix, done: Noted, not changed: |
…scan Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Na7qBenCp67rDTZhFGTh5V
This PR now carries the whole stack (#792, #796, #802, #803 merged into it after review), on top of #716.
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 gainsabandoned_by,rewind(reason=, **detail), and part records that are written again when a parameter changes. Docs:subsystems/transcript-query.md.python -m underworld3.mcp,.mcp.json): read-only tools over transcripts, the capabilities catalogue and the guides. Themcppackage is not inpixi.tomlyet; the test skips without it. Docs:guides/mcp-server.md..claude/skillsare symlinks to pages indocs/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 andCLAUDE.mdcarry the rule;test_0030enforces 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