Add a plan to implement improved documentation and support for running notarius as a subprocess

This commit is contained in:
2026-07-26 12:08:53 -05:00
parent 94b3eafb1a
commit db8db5ffc5
2 changed files with 492 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
# Subprocess Integration Implementation Plan
## Status
Ready for implementation.
## Objective
Implement the accepted [Subprocess Integration Contract](subprocess.md) as a
small, generic extension of the existing CLI boundary. The completed work must
let a subprocess caller discover a successful Notarius output bundle through a
versioned JSON receipt without parsing human prose, while preserving current
pipeline, output, rejection, warning, configuration, and interactive CLI
behavior.
Complete the stages below in order. Each stage must leave its affected package
passing before the next begins. Do not add Narratio-specific production code,
a public Go client, a new output encoder, or any feature listed as out of scope
in the feature roadmap.
## Decisions And Invariants
- `--json` is a boolean flag on `notarius run`; it does not affect pipeline
resolution, execution, checkpoint identity, output encoding, or publication.
- The machine result is a private CLI Go type implementing the public
`notarius.run-result.v1` wire contract. Do not expose framework or CLI Go
packages for external import.
- The finalized run manifest is authoritative for `run_id`, `pipeline_id`, and
`validation_status`. Verify that its pipeline ID matches the effective
resolved pipeline rather than emitting conflicting provenance.
- Human-oriented stdout remains byte-for-byte governed by its current path when
`--json` is absent. Machine mode emits no human status or debug-path line on
stdout.
- Warnings and errors remain on stderr. Exit statuses remain 0 for success, 1
for runtime failure, and 2 for syntax failure.
- Validation rejection remains a successful pipeline outcome. The receipt
reports counts but does not decide which lanes an orchestrator requires.
- The receipt points to the existing bundle; it does not duplicate output
descriptors, lane payloads, warnings, rejections, or manifest content.
- Reported filesystem paths are lexical absolute paths produced with
`filepath.Abs`. Do not resolve symlinks or change the physical directories
used for publication.
- For the production `json` output module, `index_file` is exactly
`index.json` and is included only after confirming that the runner returned
exactly one logical file with that clean name. For any injected or future
non-`json` output module, omit `index_file`; the machine receipt remains
generic.
- Machine-result construction and JSON encoding occur before debug success
terminalization, but stdout writing occurs only after output publication and
successful requested debug terminalization.
- Standard output is not transactional. A final writer failure returns exit 1
and may leave partial bytes. Consumers must ignore stdout unless the process
exits 0. Because debug terminalization must precede receipt emission, a
result-delivery failure does not rewrite an already persisted successful run
report; it is additionally reported on stderr and the process exits 1.
- Do not add a stable JSON error envelope or promote diagnostic wording into a
compatibility contract.
## Stage 1: Define The Run-Result Wire Model
Add a focused private implementation under `internal/cli`, preferably
`run_result.go`, with:
- a constant for `notarius.run-result.v1`;
- a private result struct whose JSON fields and requiredness match the feature
roadmap;
- a constructor that accepts the resolved pipeline, completed runner output,
run output directory, and optional debug directory; and
- serialization into an owned byte slice containing exactly one compact JSON
object followed by one newline; and
- a complete-write helper for delivering those prepared bytes.
The result fields are:
- required `schema_version`, `run_id`, `pipeline_id`, `output_directory`,
`normalized_output_count`, `rejected_output_count`, and `warning_count`;
- required non-empty `validation_status`, copied without reinterpretation from
the final run manifest;
- optional `index_file`, set to `index.json` for the resolved production JSON
output module; and
- optional `debug_directory`, present only when debug capture was requested and
its path is non-empty.
The constructor must:
1. validate the required run ID, pipeline ID, final validation status, and
non-empty run output directory rather than emit a malformed receipt;
2. verify that the final manifest pipeline ID matches the effective resolved
pipeline ID;
3. convert the output bundle and optional debug bundle paths to lexical absolute
paths;
4. derive all counts from the completed `pipeline.RunOutput`;
5. identify the output module through the resolved module key, not concrete
encoder types or D&D knowledge; and
6. for module key `pipeline.DefaultOutputModule`, verify exactly one returned
logical output file is named `index.json` before setting `index_file`.
Treat a missing or duplicate `index.json` from the production JSON output as a
runtime contract error. Do not inspect or decode the index contents here; the
output encoder and its existing tests own that format.
Implement the writer with `encoding/json` and complete-write handling. It must
surface encoding errors, zero-progress writes, short writes, and underlying
writer errors to the command boundary. The command must map a delivery failure
to a fixed, bounded result-write error without wrapping caller-supplied writer
text into user or debug diagnostics. Do not add an injected serializer or a
generic serialization framework merely to manufacture unreachable error cases
for tests.
Add lean focused tests for the wire-model boundary:
- required fields, counts, schema identity, and compact newline-terminated JSON;
- blank required manifest identities, blank validation status, and a mismatch
between final-manifest and effective pipeline IDs;
- lexical absolute conversion for relative output and debug paths;
- production JSON entry-point discovery;
- omission of `index_file` for a non-JSON output module;
- missing and duplicate production entry points; and
- a representative failing or zero-progress writer.
Decode structured output in tests rather than snapshotting an entire JSON
string. Assert literal field names and the schema identity because they are the
public compatibility contract.
Stage completion:
- `go test ./internal/cli`
## Stage 2: Integrate Machine Mode With Run Finalization
Update the run command in `internal/cli/run.go`:
- register `--json` with the existing `flag.FlagSet`;
- include it in root usage without adding it to `runFlagTakesValue`;
- leave all existing flag combinations valid; and
- do not add configuration or environment equivalents.
After the runner succeeds and requested debug summaries have been written,
apply this exact finalization order:
1. when `--json` is selected, construct and encode the complete receipt into
owned memory; fail before physical output publication if its required fields
or production entry point are invalid;
2. publish the runner's logical output files through the existing confined
output writer;
3. terminalize requested debug reporting as successful and stop without
writing stdout if terminalization fails;
4. in machine mode, write the prepared receipt to stdout and return a runtime
failure if that write does not complete;
5. otherwise, use the unchanged human-oriented success reporting path; and
6. report successful-run warning counts to stderr as today.
The receipt's `output_directory` is the absolute path to the run-specific
bundle, not the configured output root. `debug_directory` is the absolute path
to the allocated run-specific debug bundle.
On a machine-result stdout failure after terminalization:
- report a fixed, bounded, code-owned result-write error on stderr through the
existing CLI failure reporting surface without including the underlying
writer error;
- include the existing debug-path discovery line when applicable;
- return exit 1;
- do not attempt terminal reporting a second time; and
- leave the successfully published output and debug bundle intact.
Do not change `pipelineCommandState`, the debug run-report wire shape, the
output encoder contract, or `writeOutputFiles` solely to support this feature.
The machine receipt belongs to CLI reporting after the runner and output module
have completed their existing responsibilities.
Stage completion:
- `go test ./internal/cli`
## Stage 3: Protect The Public CLI Contract
Add or extend behavior-level CLI tests at the narrowest stable boundaries.
Reuse existing production components, fakes, state harnesses, and maintained
examples rather than creating a second subprocess fixture framework.
Cover:
- a representative maintained production invocation with `--json`, decoding
exactly one stdout document and verifying the schema identity, run and
pipeline IDs, absolute output directory, `index_file`, counts, validation
status, and the existence of the referenced bundle entry point;
- a warning-bearing debug run, proving a correct warning count, warning
reporting on stderr, an absolute optional debug path, and no human prose in
stdout;
- a rejection-bearing successful run using an explicit deterministic rejecting
validator, proving zero or partial normalized outputs and the correct
rejection count without changing exit status;
- one representative syntax failure and one representative runtime failure
with `--json`, proving the established exit class, stderr ownership, and no
completed success document;
- an injected stdout writer failure after successful publication, proving exit
1, retained output files, fixed diagnostic reporting, and omission of a
recognizable writer-error sentinel from stderr and debug diagnostics; and
- the existing no-`--json` tests continuing to protect interactive output.
For the writer-failure case, use a writer that fails before accepting bytes when
asserting empty stdout. The public contract nevertheless remains that arbitrary
writers may leave partial bytes and consumers must ignore stdout on nonzero
exit.
Do not:
- duplicate the JSON output encoder's lane, manifest, warning, or rejection
serialization matrix;
- assert complete human error strings;
- add a golden file for the small receipt;
- test private helper call order; or
- add a prompt-language or unrelated end-to-end test.
Stage completion:
- `go test ./internal/cli`
- `go test ./internal/modules/generic/output/json`
- `go test ./internal/modules/integration`
## Stage 4: Publish The Consumer-Facing Documentation
After the code and behavior tests pass, update the canonical current-behavior
documentation:
- `docs/cli.md`
- add `--json` to run syntax and its flag table;
- define stdout/stderr behavior and the requirement to parse machine stdout
only after exit 0;
- preserve the existing exit-status table and link to the run-result
contract.
- `docs/integrations/run-result.md`
- own the complete `notarius.run-result.v1` field table, requiredness,
example, path semantics, production `index_file` rule, partial-write rule,
and additive compatibility policy;
- link to the published JSON bundle contract rather than repeating its
descriptors or payload schemas.
- `docs/integrations/json-output.md`
- add only a narrow cross-link explaining that a subprocess caller obtains
the physical bundle root from the run-result receipt before using
`index.json` for logical discovery;
- do not duplicate run-result fields or stream semantics.
- `docs/consumers/subprocess.md`
- provide a concise task workflow for preflight, invocation, separate stream
capture, exit checking, receipt decoding, confined index resolution, lane
lookup by ID, media-type and schema-identity checks, required-versus-optional
consumer policy, provenance retention, and sensitive-data handling;
- use a generic orchestrator example with placeholders, not private Narratio
paths, credentials, or a duplicate complete configuration.
- `docs/internal/cli.md`
- document receipt construction before publication, emission after
publication and debug terminalization, and the stdout failure behavior;
- preserve the CLI composition-root boundary.
- `README.md` and `docs/development.md`
- add only the smallest links needed to make subprocess consumer guidance and
its implementation owner discoverable.
Do not otherwise change `docs/config.md`, `docs/operations.md`, or the JSON
output bundle contract unless implementation reveals an actual change to
behavior they own. The machine receipt adds no configuration field, filesystem
surface, retention policy, or lane-output shape.
After all code, tests, and current-behavior documentation are complete:
- change [the feature roadmap](subprocess.md) status to `Implemented`;
- change this plan's status to `Completed`; and
- ensure neither roadmap is used as the canonical description of current
behavior.
Stage completion:
- validate documentation links and command examples against the implementation;
- `git diff --check`
- `go test ./...`
- `go vet ./...`
- `go build ./cmd/notarius`
- `go test -race ./internal/cli`
## Open Questions
None. The decisions above resolve stdout partial-write behavior, output-module
generality, path normalization, partial pipeline success, terminal reporting,
documentation ownership, and test boundaries.

208
docs/roadmap/subprocess.md Normal file
View File

@@ -0,0 +1,208 @@
# Subprocess Integration Contract
## Status
Accepted for implementation.
## Purpose
Make Notarius straightforward to invoke as a subprocess from an orchestrator
such as Narratio. A caller should be able to run a configured pipeline, discover
the published output bundle without parsing human prose or scanning a
directory, and hand selected structured artifacts to a later stage.
This work strengthens the public CLI boundary. It does not turn Notarius into a
Go library, embed Narratio-specific behavior, or change pipeline execution and
artifact semantics.
## Desired End State
A subprocess caller can:
1. validate a Notarius configuration and selected pipeline before execution;
2. invoke `notarius run` with explicit input, output-root, session, and
reference arguments;
3. request one versioned, machine-readable success result on standard output;
4. use that result to locate the published output bundle;
5. discover normalized lane payloads through the bundle's authoritative
`index.json`;
6. distinguish process failure from successful partial pipeline outcomes; and
7. record Notarius run provenance in its own manifest without depending on
internal packages, cache formats, debug formats, or human-readable messages.
The existing human-oriented command output remains the default for interactive
use.
## Machine-Readable Run Result
`notarius run` supports `--json`. On success, the flag makes standard output
contain exactly one JSON object followed by a newline. No human-oriented status
line is mixed into that stream.
The result uses the schema identity `notarius.run-result.v1` and contains:
| Field | Presence | Meaning |
| --- | --- | --- |
| `schema_version` | Required | Exactly `notarius.run-result.v1`. |
| `run_id` | Required | The Notarius run identifier. |
| `pipeline_id` | Required | The effective pipeline identifier. |
| `output_directory` | Required | Absolute path to the successfully published output bundle. |
| `index_file` | Required for the production JSON output | Logical bundle path `index.json`. |
| `normalized_output_count` | Required | Number of final normalized lane outputs returned by the pipeline. |
| `rejected_output_count` | Required | Number of recorded rejected outputs. |
| `warning_count` | Required | Number of final run warnings returned by the pipeline. |
| `validation_status` | Required | The run manifest's final validation status without reinterpretation. |
| `debug_directory` | Optional | Absolute debug-bundle path when debug capture was requested and completed. |
An illustrative successful result is:
```json
{
"schema_version": "notarius.run-result.v1",
"run_id": "run-1770000000000000000-0123456789abcdef0123456789abcdef",
"pipeline_id": "dnd-session",
"output_directory": "/srv/narratio/runs/session-7/notarius/run-1770000000000000000-0123456789abcdef0123456789abcdef",
"index_file": "index.json",
"normalized_output_count": 6,
"rejected_output_count": 2,
"warning_count": 1,
"validation_status": "approved"
}
```
The receipt is a discovery and summary document, not a duplicate output
envelope. It does not embed lane payloads, rejection entries, warnings, the run
manifest, or output-file contents. Consumers use `index_file` and the existing
published JSON output contract for those records.
The result contract must tolerate future additive optional fields. Any
incompatible field or semantic change requires a new run-result schema version.
## Stream, Publication, And Failure Semantics
Machine-readable output is emitted only after:
- the pipeline has completed without a framework error;
- all logical output files have been successfully published;
- requested debug terminal reporting has completed; and
- all result fields are known.
Writing or encoding the machine-readable result is part of successful command
completion. Failure to write it produces the existing runtime-failure exit
class.
With `--json`:
- successful stdout is exclusively the run-result JSON document;
- successful warnings remain on stderr under the existing CLI contract;
- syntax and runtime errors retain their existing exit statuses and stderr
diagnostics;
- consumers treat stdout as a valid result only when the process exits with
status 0; failures before result writing emit no result, while a failure
during the stdout write may leave incomplete bytes that must be ignored; and
- human-readable diagnostic wording is not promoted into a machine contract.
Without `--json`, current interactive stdout and stderr behavior remains
unchanged.
Successful runs may contain rejected outputs or omit some normalized lanes.
That remains a valid pipeline outcome. The run result reports counts, while
`index.json`, `rejected.json`, and `warnings.json` remain authoritative for
details. Notarius will not add a generic `--fail-on-rejection` policy as part
of this work.
## Output Discovery And Consumer Responsibilities
The production JSON encoder's `index.json` remains the authoritative mapping
from lane IDs to published payloads. A subprocess consumer should:
- resolve `index_file` beneath `output_directory` and reject path escape;
- locate expected outputs by `lane_id`, not by guessing filenames;
- check each selected descriptor's media type and schema identity;
- decode payloads according to their published integration contracts;
- decide which lanes are required or optional for its own later stages; and
- retain rejection, warning, and manifest files when they are needed for
provenance or review.
For Narratio, required report inputs and partial-success policy remain Narratio
stage configuration and orchestration concerns. Notarius does not acquire
knowledge of Narratio stages, manifests, workspace layout, publication policy,
or report formats.
## Invocation Guidance
The consumer documentation recommends that subprocess callers:
- use `notarius config validate --pipeline` as an optional preflight;
- pass explicit absolute paths for the input, configuration, output root, and
CLI-supplied references;
- use a stable, non-secret prompt session identifier when useful for provider
routing or caching;
- capture stdout and stderr separately;
- supply credentials through the configured environment mechanism rather than
command arguments or generated configuration containing secret values;
- place output, cache, debug, and subprocess logs under intentional
sensitivity and retention policies; and
- treat the Notarius manifest and run-result receipt as provenance while
leaving the caller's own manifest authoritative for its stage lifecycle.
Notarius configuration remains owned by Notarius. An orchestrator may select a
configuration and pass supported operational overrides, but should not
duplicate the complete Notarius configuration schema.
## Documentation End State
- `docs/cli.md` owns `run --json`, stream behavior, and exit semantics;
- a new `docs/integrations/run-result.md` owns the versioned run-result wire
contract and compatibility policy;
- `docs/integrations/json-output.md` remains the sole owner of output-bundle
discovery and lane publication;
- a new `docs/consumers/subprocess.md` provides the task-oriented invocation and
consumption workflow; and
- `docs/internal/cli.md` describes how the CLI constructs and emits the result
only after successful publication.
Other documents should link to these owners instead of repeating volatile
fields or command details.
## Acceptance Criteria
- An ordinary successful `run` retains its existing human-readable output.
- A successful `run --json` emits one valid `notarius.run-result.v1` document
and no human prose on stdout.
- Relative configured or overridden output and debug roots are reported as
absolute bundle paths.
- The receipt identifies the production JSON bundle entry point without
copying its lane descriptors or payloads.
- Warning-bearing and rejection-bearing runs remain successful and report
accurate counts.
- Syntax, configuration, provider, pipeline, publication, debug, and result
writing failures retain the correct nonzero exit class. Consumers are
explicitly required to ignore stdout from a nonzero invocation.
- The implementation does not expose internal Go types or couple generic CLI
code to D&D or Narratio concepts.
- Public and internal documentation assigns each new contract to one canonical
owner.
- Offline behavioral tests protect the structured-output contract, default
human behavior, absolute path reporting, stream separation, and failure to
serialize or write the success result without duplicating lower-level output
encoder tests.
## Out Of Scope
The following may be useful later but are not prerequisites for the Narratio
integration:
- a result-file flag in addition to machine-readable stdout;
- a JSON failure envelope or stable machine-readable error taxonomy;
- caller-supplied Notarius run IDs or exact output-bundle paths;
- a generic `--fail-on-rejection` or required-lane CLI policy;
- a public Go client package or importable Narratio adapter;
- Narratio stage, configuration, manifest, or report-generation changes;
- `notarius version --json`;
- installable or queryable artifact JSON Schemas;
- signal-aware CLI contexts and graceful SIGINT or SIGTERM handling;
- packaged release artifacts and a broader application-versioning policy.
These items should be promoted only in response to a demonstrated integration
need rather than bundled into the initial subprocess contract.