feat(inspect): describe a module tree without running it#38
Conversation
The pretty handler's colors were reachable only from inside internal/log, so pkg/action forked its own copy of the escape codes for diff headers. A third surface would have forked a third copy. Style applies the same palette to text that is not a log record, and its zero value is uncolored so piped output stays byte-comparable. isTerminal becomes IsTerminal for callers outside the package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Describing an operation needs the same knowledge FromOperation already has — which field of an Operation is set. Adding a second switch for it would let a new action type be wired into one and forgotten in the other, so a module could be described as one kind and run as another. classify becomes the single switch, returning the Action plus the kind and one-line detail. FromOperation and the new DescribeOperation are both thin readers of it, and DescribeOperation rejects exactly what FromOperation rejects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Composition makes a module hard to read ahead of a run: the operations that will execute live in submodules pulled from other repositories, and the parameters they require are handed down through templates rather than declared where you invoke. The only way to find out what a module needed was to run it and read the failure. Inspect walks the tree and reports it: each module's operations in execution order, its target, and every parameter with the origin of its value — supplied, defaulted, dynamic, or missing. A value a parent hands down keeps the expression that produced it, so a templated hand-off stays traceable. Nothing executes. No operation runs, no dynamicParams command is evaluated, and no if condition is tested; the sole side effect is cloning a module source that is a git URL, into temp dirs removed before Inspect returns. Templates are rendered only against params whose values are actually known, so one depending on run-time state is reported as unresolved rather than resolved to a placeholder. Failures are contained to the node they happen at, so a broken submodule never hides the rest of the tree, and a walk stops at a module that is its own ancestor rather than recursing forever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renders the inspected tree as indented text, borrowing the run log's vocabulary — the "≡ … ≡" root chip, the "▸" hand-off marker, the shared palette — so a module looks the same whether you are inspecting it or watching it run. Each module's params, target, operations, and submodules hang off it as branches, so nesting reads from indentation alone. The report closes with every required parameter the tree is still missing, each located by the breadcrumb of the module that declares it: in a composed tree that module is frequently not the one you invoke. Missing params do not fail the command — reporting them is the point — but a module that could not be described does. -o json emits the same tree plus the missingParams and problems roll-ups, for CI checks that want to assert a module is fully parameterized. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the CLI reference page and wires it into the nav, README, and the two guides where it answers a question the reader already has: getting-started gains a step for seeing what a module needs before running it, and module-composition notes that a tree can require a value never visible at the top level. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Walking the whole tree by default made the common question — what is this module, and what does it need — expensive to ask. A module composing half a dozen remote submodules cloned all of them before printing anything, and buried its own operations in a wall of output. The default is now one module: described in full, with what it composes listed by name. A listed module is never resolved, so the default view does no network I/O at all. Three ways to go deeper, each explicit: --full for the whole tree, --depth N for a level or two, and --module NAME to make a submodule the subject — matched on the tail of its breadcrumb, so a bare name works until two modules share one, and qualifying it is then an error message rather than a wrong answer. Listing rather than dropping is what makes the shallow default honest. The summary's claims are scoped to the modules actually read, and any it skipped are named alongside, because a tree can only be called fully parameterized on the strength of modules someone looked at. The same reasoning applies to exit status: a module that was never resolved cannot have failed to resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reworked the default view: one module, submodules listed — expansion is now opt-in. Walking the whole tree by default made the common question expensive to ask. A module composing several remote submodules cloned all of them before printing anything, and buried its own operations in a wall of output. Three ways to go deeper:
A focused module is headed by its breadcrumb and shows the values its parents actually hand it: Why listing, not truncatingA module past the depth limit is still reported, carrying what its parent declares (name, source, That is also what keeps the shallow default honest. Everything the summary claims is scoped to the modules actually read, and the ones skipped are named alongside: a tree can only be called fully parameterized on the strength of modules someone looked at. Same reasoning for exit status — a module that was never resolved cannot have failed to resolve, so a broken submodule surfaces at
loom inspect ./mod --full -p env=prod -o json | jq -e '.missingParams == [] and .unexpanded == []'Notes
|
--module took one name, so comparing two siblings meant two invocations and holding the first in your head. It is now repeatable: each subject is reported with its own breadcrumb, in the order asked for, under one summary — what you have to supply is a single list however many modules you looked at. Subjects can overlap, since a module and one it composes may both be named. Nodes are shared within a walk, so trimming one subject to the depth limit would otherwise hollow out another sitting inside it; Clone gives each subject its own copy. The roll-ups deduplicate for the same reason: the same missing parameter reachable through two subjects is one parameter, not two. The JSON document now carries a modules list rather than a single module, of one entry when nothing was selected, so consumers index it the same way either way. The --module hint the summary prints picks the shortest suffix that resolves, rather than only choosing between a bare name and the fully qualified path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds
loom inspect, a subcommand that describes what a module is made of — its submodule hierarchy, the operations each one runs, and the parameters they require — without running any of it.Why
Composition makes a module hard to read ahead of a run. The operations that will execute live in submodules pulled from other repositories, and the parameters they need are handed down through templates rather than declared where you invoke. Until now the only way to find out what a module required was to run it and read the failure.
What it looks like
Modules are labelled with their instance name — the parent's
modules[].name, rendered — the same identity the run log andloom diffuse, withmetadata.namealongside when they differ. A value a parent hands down keeps the expression that produced it (← {{ .env }}-apps), so a templated hand-off stays traceable.The summary collects every required parameter the tree is missing, each located by the module that declares it. In a composed tree that module is frequently not the one you invoke.
Flags:
-p/--params-file,--depth,--no-fetch,-o tree|json.Nothing executes
No operation runs, no
dynamicParamscommand is evaluated, noifcondition is tested, and no target repo is cloned. The sole side effect is cloning a modulesourcethat is a git URL — unavoidable if its contents are to be read — into temp dirs removed before the command returns.--no-fetchskips even that.Templates are rendered only against params whose values are actually known, so one depending on run-time state is reported as
unresolvedrather than resolved to a<no value>placeholder and passed down as if real.Reuse over new code
module.ResolveSourceresolves root and child sources (git URLs,//subdir, local paths).config.Load/Validate/ValidateInDir, split so a structural error fails a module while a missingnewFiles.sourceonly warns — the description stays available, which is the whole point of the command.cmd.parseParamsfor-p/--params-file.pkg/action/registry.gorefactored soFromOperationand the newDescribeOperationread one switch. A second switch would let a new action type be wired into one and forgotten in the other, so a module could be described as one kind and run as another.internal/loggained aStyletype exposing the pretty handler's palette.pkg/actionhad already forked its own copy of the escape codes for diff headers; this stops a third copy appearing, and the tree shares the run log's≡ … ≡chip and▸hand-off marker.Robustness
--depthtruncates rather than failing.Testing
specs/inspect.mdadds rules IN1–IN15, each referenced by a test — the spectrace guard passes with nothing added touncovered.pkg/moduleandcmd. Full suite green, including-race.newFiles+patch+shell+commitPush), plus remote module sources viafile://URLs, cycles, depth limits, and--no-fetch. No temp clones leaked.registry.gorefactor:run --local-run,run --dry-run, anddiff --quickproduce byte-identical logs and git state against a binary built frommain.Note for reviewers
-psupplies the root module only. A requirement reported deep in the tree is satisfied by the parent forwarding a value, not by another-p. This surprised me while writing the tests, so it is called out in both the reference page and the composition guide.Commits are split by layer and each is independently green.
🤖 Generated with Claude Code