Skip to content

Commit 178c628

Browse files
author
David Noble
committed
docs(cli): bring the reference and the plans current (#739, #740)
make check, make docs, and make test-scenario all pass. The command help strings documented flags this branch retired. docs/cli is gitignored -- generated by `make docs`, never committed -- so the staleness lived in the Go source, and regenerating only proved the fix. #739's title says the corruption "shipped into the docs"; it did not, and that claim is corrected on the issue. A fourth #739 site: lore inspect's long description read "Promise is JSON by default ... Use --format", in prose rather than in a flag string, so a grep aimed at flag descriptions never saw it. Same class of miss as counting writ's stdout writes by grepping os.Stdout and missing 24 fmt.Print calls: a grep for one syntactic form undercounts a semantic problem. Section 15 claimed no root registers the common set. Two of four do. It now also states what it had been carrying silently: writ status is bridged, not converted, so `writ status -o yaml` produces the human report rather than yaml, until its report goes through the pipeline. Plan and status checkboxes were stale -- phases 1, 2, and 3c were complete with every box unticked. Reporting progress in conversation while the tracked document says nothing is done is the failure that rule exists to prevent. extract-output-package.md is corrected. It was marked complete while describing an internal/output package that was never created; the code went to pkg/result and pkg/sink. It also holds the last measurement of adoption -- two call sites, one of which vanished with writ snapshot and went unrecorded -- which is the evidence behind the enforcement invariants. Also written down, having been noticed and left: the jq-versus-JMESPath divergence now sits in section 16 with the rest, and section 7 states that `value` expects a projection, as gcloud requires one for its csv and value. Two test defects, both invisible to make check: - scenario_integration_test.go invoked `writ status --json`, a flag deleted several commits ago. make check does not run scenario tests, so it would have gone red on CI rather than locally. - TestCLI_BadOutputPath passed for the wrong reason: after --output became a rendering, "graph=/no/such/dir/out.txt" parses as an unknown format named "graph" and exits non-zero without reaching a filesystem. It is now TestCLI_UnwritableStore, against the flag that takes a path.
1 parent ffba8a2 commit 178c628

9 files changed

Lines changed: 130 additions & 63 deletions

File tree

‎cmd/devlore-test/cli_test.go‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,17 @@ func TestCLI_UnknownRendering(t *testing.T) {
279279
assertContains(t, stderr, "unknown formatter")
280280
}
281281

282-
func TestCLI_BadOutputPath(t *testing.T) {
283-
_, _, code := run("run", "--output", "graph=/no/such/dir/out.txt", scriptPath)
284-
assertExit(t, 1, code)
282+
// TestCLI_UnwritableStore covers the flag that actually takes a path.
283+
//
284+
// This asserted a bad --output destination until --output became a rendering. Left as it was, it passed for
285+
// the wrong reason: "graph=/no/such/dir/out.txt" parses as an unknown format named "graph", so it exited
286+
// non-zero without ever reaching a filesystem.
287+
func TestCLI_UnwritableStore(t *testing.T) {
288+
_, stderr, code := runIn(t.TempDir(), "run", "--store", "/no/such/dir/store", scriptPath)
289+
if code == 0 {
290+
t.Error("an unwritable store exited 0, want non-zero")
291+
}
292+
assertContains(t, stderr, "store")
285293
}
286294

287295
func TestCLI_UnknownFlag(t *testing.T) {

‎cmd/lore/lore/commands.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ func newInspectCmd() *cobra.Command {
825825
Displays the resolved lifecycle manifest, platform support, features,
826826
dependencies, and deployment history for a package.
827827
828-
Promise is JSON by default for scripting. Use --format for alternatives.`,
828+
Output is JSON by default for scripting. Use --output for alternatives.`,
829829
Example: ` lore inspect docker
830830
lore inspect kubectl --format yaml
831831
lore inspect docker --format '{{.ReceiverName}}\t{{.Version}}'`,

‎cmd/writ/scenario_integration_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ func TestWritDeployScenario_Deploy(t *testing.T) {
462462
}
463463

464464
// The status report, machine-readable: every classified entry is healthy.
465-
statusOut, statusErr, err := runWrit(t, sandbox, "status", "--json")
465+
statusOut, statusErr, err := runWrit(t, sandbox, "status", "-o", "json")
466466
if err != nil {
467467
t.Fatalf("writ status failed: %v\nstderr: %s", err, statusErr)
468468
}
@@ -474,7 +474,7 @@ func TestWritDeployScenario_Deploy(t *testing.T) {
474474
} `json:"entries"`
475475
}
476476
if err := json.Unmarshal([]byte(statusOut), &report); err != nil {
477-
t.Fatalf("status --json is not parseable: %v\n%s", err, statusOut)
477+
t.Fatalf("status -o json is not parseable: %v\n%s", err, statusOut)
478478
}
479479
// With the implicit common project: Windows deploys the base pair + common + common.Windows (4);
480480
// the unix platforms add their variants (darwin 8, linux 7).

‎cmd/writ/writ/commands.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ Status indicators:
207207
identity: attribution indeterminate → writ upgrade`,
208208
Example: ` writ status # Report everything writ has deployed
209209
writ status noblefactor # Report one project
210-
writ status --json # Machine-readable report`,
210+
writ status -o json # Machine-readable report
211+
writ status -o table # Aligned columns`,
211212
RunE: runStatus,
212213
}
213214

@@ -246,7 +247,7 @@ is the signing policy ladder:
246247
reject Reject anything that is not valid`,
247248
Example: ` writ verify ~/.local/state/devlore/graphs/*.yaml
248249
writ verify --signing-policy=reject_external ~/Downloads/shared-plan.yaml
249-
writ verify --json --signing-policy=reject trace.yaml`,
250+
writ verify -o table --signing-policy=reject trace.yaml`,
250251
Args: cobra.MinimumNArgs(1),
251252
RunE: runVerify,
252253
}

‎docs/architecture/10-command-line-interface.md‎

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@ when observed is unreproducible. A human wanting a table asks for one: `-o table
182182
**Adding a formatter** is a change to `pkg/result`, never to a command. A command needing a shape the set
183183
does not have reshapes with `--jq` and renders with `value`.
184184

185+
**`value` expects a projection.** It renders whatever it is handed, so a whole nested structure comes out as
186+
every field on one line, pointer addresses included. That is the same expectation `gcloud` states by
187+
requiring a projection for its `csv` and `value` formats. The intended use is `--jq` first:
188+
189+
```
190+
writ status --jq '.entries[] | "\(.target) is \(.state)"' -o value
191+
```
192+
185193
**One table formatter, no exceptions.** `table` is a general rendering and belongs in `pkg/result` like the
186194
others. No command owns its own. The current hand-rolled table in `lore`'s `runSearch`
187195
(`cmd/lore/lore/commands.go:525-556`) is not an argument for a second one: it writes through `fmt.Printf`
@@ -279,10 +287,21 @@ including when the flag's value equals its default.
279287
Help text is the specification a user reads. It states what the command does, what its flags mean, and what
280288
its output is — including, for a multi-artifact command, the names of the artifacts it writes.
281289

282-
`docs/cli/**` is generated from the command tree by `devlore-docs`. **The generated files are never
283-
hand-edited.** A wrong word in the docs is a wrong word in the flag description, and it is fixed there —
284-
which is exactly how "Promise bundle path" reached three published pages
285-
([#739](https://github.com/NobleFactor/devlore-cli/issues/739)).
290+
`docs/cli/**` is generated from the command tree by `devlore-docs`. It is **gitignored in this repository and
291+
published from it**: `.github/workflows/docs-publish.yaml` runs `make docs` on every push to `develop`,
292+
`main`, or `release/*`, copies `docs/cli/` and `docs/guides/` into `NobleFactor/devlore.noblefactor.com`, and
293+
**opens and merges** the site PR unattended.
294+
295+
Two consequences follow, and the second is the one that bites.
296+
297+
A wrong word in the reference is a wrong word in a command's help string, and it is fixed at the source. The
298+
generated files are never hand-edited -- there is nowhere to hand-edit them, since this repository does not
299+
keep them.
300+
301+
**A help string reaches the public site on the next merge to `develop`, with no human in the path.** That is
302+
how "Promise bundle path" was published
303+
([#739](https://github.com/NobleFactor/devlore-cli/issues/739)). Being gitignored makes it *less* visible in
304+
review, not less published: a flag description gets no diff here and no approval there.
286305

287306
## 13. Stability
288307

@@ -303,22 +322,25 @@ Each rule below is greppable, and each has a test. These are the reason the docu
303322
| 3 | All four in-scope roots register the full common set | a test over the command tree |
304323
| 4 | `--store` relocates both subdirectories and the run index together | a store round-trip test |
305324
| 5 | Narration is absent from stdout under every format | a test capturing both streams |
306-
| 6 | Generated `docs/cli/**` matches the flag descriptions | the codegen determinism gate |
325+
| 6 | Help strings read as published prose; they ship unreviewed | `make docs` and read it, in the flag-changing work |
307326

308327
Invariants 1 and 2 are the ones that prevent regression, because both are mechanical and both are red today.
309328

310329
## 15. Per-app conformance
311330

312-
Current state, measured 2026-08-28. One command out of forty-six uses the convention, and no root
313-
registers the common set.
331+
Current state, 2026-08-30. Two of the four in-scope programs register the common set on their root.
314332

315-
| App | Commands | Conforming | Deviations |
316-
| --- | --- | --- | --- |
317-
| `lore` | 19 | 1 (`inspect`) | `bundle`, `onboard`, `list` hand-roll flags; 13 `fmt.Print` calls |
318-
| `writ` | 13 | 0 | `--json` booleans on `status` and `verify`; 7 direct `os.Stdout` writes |
319-
| `star` | 9 | 0 | a **second `cli` package** of its own -- see below |
320-
| `devlore-docs` | 3 | 0 | — |
321-
| `devlore-test` | 2 | 0 | `--output stream=dest` routing, `--receipt-format`, inverted artifacts |
333+
| App | Root registers the set | Remaining deviations |
334+
| --- | --- | --- |
335+
| `devlore-test` | **yes** | none |
336+
| `writ` | **yes** | `status`'s 22 `fmt.Print` calls; four dry-run dumps; `migrate`'s `--format` |
337+
| `lore` | no | hand-rolled flags on three commands; 13 `fmt.Print`; `runSearch` (#741) |
338+
| `star` | no | a **second `cli` package** of its own -- see below |
339+
| `devlore-docs` | not in scope | — |
340+
341+
`writ status` is bridged rather than converted: `cfg.JSONOutput` reads `outputOptions.Format == "json"` while
342+
its report still renders itself. The consequence is worth stating -- `writ status -o yaml` silently produces
343+
the human report, not yaml -- and it holds only until that report goes through the pipeline.
322344

323345
**`star` does not lack the convention -- it has a second copy of it.** `cmd/star/cli` duplicates eighteen
324346
exported names from `cmd/internal/cli`, including all ten exit codes and `AddOutputFlags`, and at 387 lines
@@ -423,6 +445,15 @@ same argument. Plain is a rendering, so it is a format value.
423445
the result stream: a pipeline whose data changes when observed is unreproducible. Narration adapts to a TTY
424446
(§10); results never do.
425447

448+
**A query language rather than a template engine.** clig.dev does not take a position; the prior art splits
449+
cleanly. `aws` and `az` ship JMESPath as `--query`, `gcloud` ships projections and transforms, and none of the
450+
three has a template engine. `kubectl` and `docker` have templates and no query language -- docker's
451+
`--format` *is* the template. Nobody ships both, because they solve one problem.
452+
453+
This suite uses `gojq`, which is strictly more capable than JMESPath and more widely known than gcloud's
454+
projection syntax. `template=<body>` exists for text layout a query cannot express, and is deliberately the
455+
only argument-taking format.
456+
426457
**A third stream.** clig.dev describes two streams. This suite has three, because a workflow engine produces
427458
durable artifacts that are neither the answer to a question nor progress narration. Documents go to the store
428459
(§6), and no clig.dev guidance is contradicted by their existence — it simply does not cover them.

‎docs/architecture/10-command-line-interface.status.md‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
**Document:** [10-command-line-interface.md](10-command-line-interface.md)
44
**Epic:** [#740](https://github.com/NobleFactor/devlore-cli/issues/740)
55
**Plan:** [cli-output-conventions.md](../plans/cli-output-conventions.md)
6-
**State:** Design draft (2026-08-28), written after measuring the suite against `aws`, `az`, `docker`,
7-
`gcloud`, and `kubectl`. The mechanism exists; adoption is one command in forty-six. No implementation.
6+
**State:** Specified 2026-08-28 against `aws`, `az`, `docker`, `gcloud`, and `kubectl`; the formatter layer
7+
and two of four programs landed 2026-08-30. `devlore-test` and `writ` register the common set; `lore` and
8+
`star` do not yet.
89

910
## Completion
1011

@@ -35,16 +36,19 @@
3536

3637
## Document discrepancies
3738

38-
- **`extract-output-package.md` is marked complete while describing absent code.** Its stated target was an
39-
`internal/output` package with a single `Render(w, data, options)`. No such package exists; the code went to
40-
`pkg/result` + `pkg/sink` with `BuildPipeline`. Correcting it is a Phase 5 task.
41-
- **The generated CLI reference carries corrupted flag help.** Three pages under `docs/cli/lore/` read
42-
"Promise bundle path" and similar, from an `Output` → `Promise` replace that caught flag descriptions
43-
([#739](https://github.com/NobleFactor/devlore-cli/issues/739)). The generated files are not hand-edited;
44-
the fix is in the flag strings.
39+
- ~~`extract-output-package.md` is marked complete while describing absent code.~~ **Corrected 2026-08-30**
40+
with a note at the head of that plan: no `internal/output` package exists, the code went to `pkg/result` +
41+
`pkg/sink`, and the plan is retained as history rather than as a map of where things live.
42+
- ~~The generated CLI reference carries corrupted flag help.~~ **Fixed 2026-08-30.** Four sites, not three:
43+
the fourth was `lore inspect`'s long description, which read "Promise is JSON by default" in prose rather
44+
than in a flag string and so escaped a grep aimed at flag descriptions. Source corrected
45+
([#739](https://github.com/NobleFactor/devlore-cli/issues/739)). `docs/cli` is gitignored here but
46+
published: `docs-publish.yaml` regenerates it on every push to `develop` and auto-merges it into
47+
`devlore.noblefactor.com`, so the wrong strings were live on the public site.
4548

4649
## Outstanding work
4750

4851
- Whether any command needs an exception to the json-always default. `lore list` defaults to `table` today.
49-
- `jq` versus the query languages the prior art uses — `aws` has JMESPath `--query`, `gcloud` has projections.
50-
Ours is defensible and more widely known, but the divergence is not yet written into §15.
52+
- Whether `value` should require a projection, as `gcloud` requires one for `csv` and `value`. Applied to a
53+
whole nested struct it prints every field, pointer addresses included. §7 states the expectation; nothing
54+
enforces it.

‎docs/architecture/index.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ Each architecture document has a companion `*.status.md` file tracking completio
9797

9898
- [Command Line Interface](10-command-line-interface.md) ([status](10-command-line-interface.status.md)) — One convention across every binary: the three streams (result to stdout, narration to stderr, documents to the execution store), the reserved flag set, and the conformance invariants that keep adoption from decaying
9999

100-
The devlore-cli consists of three tools that share a common execution engine:
100+
The devlore-cli consists of these tools, which share a common execution engine and one command-line convention ([10-command-line-interface.md](10-command-line-interface.md)):
101101

102102
- **writ** — File tree management (dotfiles, configs, templates)
103103
- **lore** — Package management (software installation, configuration)
104104
- **star** — Starlark-powered operations tool (linting, setup, hooks, project automation)
105+
- **devlore-test** — the graph test harness: plan, execute, verify
105106

106107
All three tools consume the shared provider framework in `pkg/op/` and the starlark runtime. Writ and lore produce sealed execution graphs run by `op.GraphExecutor` in `pkg/op/`. Star uses a lightweight command model where extensions define commands in Starlark and providers supply the bindings.

0 commit comments

Comments
 (0)