Add artifact provenance and stage skip outcomes
This commit is contained in:
577
docs/roadmap/implementation.md
Normal file
577
docs/roadmap/implementation.md
Normal file
@@ -0,0 +1,577 @@
|
|||||||
|
# Notarius Extraction Implementation Plan
|
||||||
|
|
||||||
|
## Status And Audience
|
||||||
|
|
||||||
|
Proposed and not started.
|
||||||
|
|
||||||
|
This plan is written for a GPT-5.6 Terra coding agent. Implement the stages in
|
||||||
|
strict numerical order. Do not skip ahead, combine stages merely to reduce the
|
||||||
|
number of prompts, or describe unfinished behavior as current functionality.
|
||||||
|
|
||||||
|
The target state and acceptance criteria are owned by
|
||||||
|
[notarius-extract-stage.md](notarius-extract-stage.md). This document owns the
|
||||||
|
implementation sequence.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
| Stage | Status |
|
||||||
|
| --- | --- |
|
||||||
|
| Stage 1 | Complete |
|
||||||
|
| Stage 2 | Not started |
|
||||||
|
| Stage 3 | Not started |
|
||||||
|
| Stage 4 | Not started |
|
||||||
|
| Stage 5 | Not started |
|
||||||
|
| Stage 6 | Not started |
|
||||||
|
| Stage 7 | Not started |
|
||||||
|
| Stage 8 | Not started |
|
||||||
|
| Stage 9 | Not started |
|
||||||
|
|
||||||
|
After completing and validating a stage, update only that stage's row to
|
||||||
|
`Complete` and record any material deviation in the relevant stage section.
|
||||||
|
Do not mark a stage complete while required tests or exit criteria remain.
|
||||||
|
|
||||||
|
## Working Rules
|
||||||
|
|
||||||
|
Before Stage 1, read:
|
||||||
|
|
||||||
|
- `docs/development.md` and its task-specific references;
|
||||||
|
- `docs/roadmap/notarius-extract-stage.md` completely;
|
||||||
|
- `../notarius/docs/consumers/dnd-pipeline.md` and its linked subprocess,
|
||||||
|
receipt, JSON-output, and lane-contract documentation; and
|
||||||
|
- the focused Narratio documents and tests named by the current stage.
|
||||||
|
|
||||||
|
For every stage:
|
||||||
|
|
||||||
|
1. Inspect the current implementation before editing. Prefer the codebase
|
||||||
|
knowledge graph for code discovery as required by `AGENTS.md`.
|
||||||
|
2. Preserve unrelated worktree changes.
|
||||||
|
3. Implement the complete stage scope, including focused tests. Do not leave
|
||||||
|
TODO implementations, compatibility shims without owners, or knowingly
|
||||||
|
dead code for a later stage.
|
||||||
|
4. Use real internal collaborators in tests where fast and deterministic; fake
|
||||||
|
only subprocess, remote, clock, randomness, or other external boundaries.
|
||||||
|
5. Keep tests offline and independent of real Notarius, PromptKit, LLM
|
||||||
|
providers, credentials, and mutable services.
|
||||||
|
6. Run the focused package tests listed for the stage. Fix failures before
|
||||||
|
proceeding.
|
||||||
|
7. Run `go test ./...` after every stage. Also run `go vet ./...` and
|
||||||
|
`go build ./cmd/narratio` when the stage changes shared contracts,
|
||||||
|
composition, CLI behavior, or documentation examples.
|
||||||
|
8. Keep proposed behavior in `docs/roadmap/` until Stage 9. Intermediate code
|
||||||
|
is development work and must not cause current-behavior documentation to
|
||||||
|
claim that the end-to-end feature is complete.
|
||||||
|
|
||||||
|
Intermediate stages must compile and pass the repository test suite. The work
|
||||||
|
is not release-ready until Stage 9 is complete.
|
||||||
|
|
||||||
|
## Stage 1: Establish Shared Stage-Outcome And Artifact-Provenance Contracts
|
||||||
|
|
||||||
|
Introduce the reusable internal contracts needed by extraction without adding
|
||||||
|
Notarius configuration or a registered stage.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Under `internal/artifactmodel`, add optional neutral models for:
|
||||||
|
- contract metadata: `media_type`, `schema_id`, `schema_version`, and
|
||||||
|
optional `module_key`;
|
||||||
|
- external provenance: `system`, `run_id`, `pipeline_id`, and
|
||||||
|
`artifact_id`.
|
||||||
|
2. Extend `artifacts.Ref` with:
|
||||||
|
- optional explicit `SourceID`;
|
||||||
|
- optional contract metadata; and
|
||||||
|
- optional external provenance.
|
||||||
|
3. Extend `manifest.ArtifactRecord` with the same nested optional metadata.
|
||||||
|
Use backward-compatible `omitempty` JSON fields. Old manifests must load
|
||||||
|
and round-trip without fabricated metadata.
|
||||||
|
4. Refactor `internal/app.mapResultOutputs` to prefer `Ref.SourceID` when set,
|
||||||
|
copy both metadata structures, and retain existing output-kind and analyze
|
||||||
|
inference only as fallback behavior.
|
||||||
|
5. Add an explicit `StageResult` disposition with succeeded and skipped
|
||||||
|
values. Preserve zero-value success so existing stages do not need
|
||||||
|
mechanical edits.
|
||||||
|
6. Add a stable skip-reason field. Reject or fail safely if a skipped result
|
||||||
|
contains outputs.
|
||||||
|
7. Teach `executeStages` to persist a self-skipped stage as skipped in both
|
||||||
|
session and run manifests, apply bounded result metadata and diagnostics,
|
||||||
|
clear any older session-stage outputs, and continue to later stages.
|
||||||
|
Distinguish this from the runner's existing decision to skip an already
|
||||||
|
succeeded stage.
|
||||||
|
8. Keep successful and failed behavior unchanged for existing stages.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- `internal/artifactmodel`: JSON behavior for complete and omitted metadata.
|
||||||
|
- `internal/manifest`: old manifest compatibility and self-skip clearing of
|
||||||
|
earlier outputs.
|
||||||
|
- `internal/app`: explicit source ID precedence, fallback inference,
|
||||||
|
provenance copying, self-skip persistence, continuation after self-skip,
|
||||||
|
and rejection of skipped results containing outputs.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- Existing stage outputs and manifests remain compatible.
|
||||||
|
- A synthetic stage can self-skip without being recorded as succeeded.
|
||||||
|
- A later invocation will reconsider a self-skipped stage.
|
||||||
|
- `go test ./...`, `go vet ./...`, and `go build ./cmd/narratio` pass.
|
||||||
|
|
||||||
|
## Stage 2: Add Notarius Configuration And Extraction Source Policy
|
||||||
|
|
||||||
|
Add strict configuration and stable source identity, but do not invoke
|
||||||
|
Notarius or register `extract` yet.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Add an optional `Notarius *NotariusConfig` field to `PipelineConfig`.
|
||||||
|
2. Define:
|
||||||
|
- `enabled` with false default;
|
||||||
|
- `binary` with `notarius` default;
|
||||||
|
- `config_path`;
|
||||||
|
- `pipeline_id`;
|
||||||
|
- `timeout` with `3h` default;
|
||||||
|
- optional `working_directory`; and
|
||||||
|
- `outputs map[string]NotariusOutputConfig`.
|
||||||
|
3. Define each output with required `lane_id`, `media_type`, `schema_id`, and
|
||||||
|
`schema_version`, plus optional `module_key`.
|
||||||
|
4. Apply defaults centrally. When enabled, resolve `config_path` to an
|
||||||
|
absolute path using the same configuration-source semantics as comparable
|
||||||
|
adapter paths. Default `working_directory` to the resolved config file's
|
||||||
|
directory, and resolve an explicitly configured working directory to an
|
||||||
|
absolute path.
|
||||||
|
5. Validate enabled configuration before stage execution:
|
||||||
|
- non-empty binary, config path, pipeline ID, and output map;
|
||||||
|
- positive duration;
|
||||||
|
- non-empty output contract fields;
|
||||||
|
- output keys accepted by the existing configured-artifact key policy;
|
||||||
|
- unique normalized extraction source IDs;
|
||||||
|
- unique lane IDs;
|
||||||
|
- no collision with built-in, configured Scriptorium, or previous-session
|
||||||
|
source identities.
|
||||||
|
6. Add `SourceKindExtraction` and helpers for the exact
|
||||||
|
`narratio.extraction.<key>` family in `internal/artifactpolicy` and
|
||||||
|
`internal/artifacts`. Do not accept arbitrary unconfigured extraction
|
||||||
|
sources at configuration validation boundaries.
|
||||||
|
7. Extend Scriptorium input and publish source validation to accept only
|
||||||
|
extraction keys declared by the same effective pipeline configuration.
|
||||||
|
8. Do not expose a Notarius session-ID setting, lane-selection flag, prompt,
|
||||||
|
profile, model, retry, or reference configuration.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- strict YAML decoding and unknown-field rejection;
|
||||||
|
- omitted and disabled behavior;
|
||||||
|
- defaults and path resolution from pipeline-file location;
|
||||||
|
- enabled required fields and positive timeout;
|
||||||
|
- invalid/safe keys, normalized collisions, duplicate lanes, incomplete
|
||||||
|
contracts, and source-family collisions;
|
||||||
|
- valid and unknown extraction sources in Scriptorium inputs and publish
|
||||||
|
rules; and
|
||||||
|
- existing configurations and maintained examples continue to load.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- A complete Notarius configuration loads and normalizes deterministically.
|
||||||
|
- Invalid downstream extraction references fail during config validation.
|
||||||
|
- No runtime code invokes Notarius yet.
|
||||||
|
- `go test ./internal/config ./internal/artifactpolicy ./internal/artifacts`
|
||||||
|
and `go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 3: Add Safe Immutable Directory Promotion
|
||||||
|
|
||||||
|
Implement the filesystem primitive used to promote a validated external bundle
|
||||||
|
without exposing a partial or mixed durable directory.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Add a narrowly named directory-promotion operation to `internal/fileops`.
|
||||||
|
It accepts an existing source directory and a new destination directory.
|
||||||
|
2. Require a non-empty source and destination. The destination must not exist.
|
||||||
|
3. Create a temporary sibling of the destination so final installation uses a
|
||||||
|
same-filesystem rename.
|
||||||
|
4. Recursively walk with `Lstat` semantics:
|
||||||
|
- permit directories and regular files only;
|
||||||
|
- reject symlinks, devices, sockets, named pipes, and other special files;
|
||||||
|
- never follow a symlink;
|
||||||
|
- use deterministic lexical traversal;
|
||||||
|
- preserve relative layout; and
|
||||||
|
- use bounded safe permissions rather than copying unsafe mode bits.
|
||||||
|
5. Copy and sync each regular file, sync the completed temporary tree where
|
||||||
|
practical, and rename the temporary directory to the unique destination.
|
||||||
|
6. On any failure, remove only the operation's validated temporary sibling.
|
||||||
|
Never remove or overwrite the source, destination, workspace root, or a
|
||||||
|
broad caller-provided directory.
|
||||||
|
7. Return an error if a concurrent actor creates the destination before final
|
||||||
|
installation.
|
||||||
|
|
||||||
|
Tests using `t.TempDir()`:
|
||||||
|
|
||||||
|
- nested regular-tree success and byte-for-byte preservation;
|
||||||
|
- deterministic layout;
|
||||||
|
- destination already exists;
|
||||||
|
- source is not a directory;
|
||||||
|
- symlink to a file, symlink to a directory, and escaping symlink rejection;
|
||||||
|
- representative special-file rejection where portable;
|
||||||
|
- copy/install failure cleanup with no partial destination; and
|
||||||
|
- source preservation after success and failure.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- A promoted directory appears only as a complete unique tree.
|
||||||
|
- The operation cannot follow symlinks or replace an existing destination.
|
||||||
|
- `go test ./internal/fileops` and `go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 4: Implement The Notarius Adapter
|
||||||
|
|
||||||
|
Create the external subprocess boundary independently of stage policy.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Add `internal/adapters/notarius` with:
|
||||||
|
- a `Runner` interface;
|
||||||
|
- transport-neutral request and result models;
|
||||||
|
- receipt, index, lane-descriptor, and pipeline-wide-descriptor models;
|
||||||
|
- a production subprocess runner; and
|
||||||
|
- a small configurable fake for stage tests.
|
||||||
|
2. Build exactly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
notarius run <pipeline-id> --config <path> --input <path>
|
||||||
|
--output-dir <path> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use separate argument elements, absolute supplied paths, the configured
|
||||||
|
working directory, inherited environment, and Narratio's shared subprocess
|
||||||
|
timeout/cancellation behavior. Do not pass `--session-id`.
|
||||||
|
3. Write stdout directly to the configured receipt path and stderr to the
|
||||||
|
configured log path. Do not combine streams.
|
||||||
|
4. On nonzero exit, cancellation, or timeout, return the subprocess error and
|
||||||
|
do not parse stdout.
|
||||||
|
5. After exit zero, read receipt bytes with an explicit upper bound, decode
|
||||||
|
`notarius.run-result.v1`, tolerate unknown fields, and validate required
|
||||||
|
fields and matching pipeline ID.
|
||||||
|
6. Require an absolute `output_directory` and confine it beneath the absolute
|
||||||
|
output root from the request. Confine the relative `index_file` beneath the
|
||||||
|
returned directory using Narratio's root-scoped path helpers. Reject
|
||||||
|
absolute logical paths, traversal, symlinks at consumed paths, and
|
||||||
|
non-regular files.
|
||||||
|
7. Decode the supported production JSON index tolerantly. Validate required
|
||||||
|
management paths and lane descriptors, reject duplicate lane IDs, and
|
||||||
|
confine every lane and pipeline-wide descriptor file beneath the bundle
|
||||||
|
root.
|
||||||
|
8. Load `rejected.json` and `warnings.json` with explicit size bounds and
|
||||||
|
tolerant decoding. Return only their generic summaries, including lane ID
|
||||||
|
and reason code where present; do not read lane payload bodies or interpret
|
||||||
|
D&D fields. Keep free-form external messages bounded for immediate errors
|
||||||
|
and out of manifest metadata.
|
||||||
|
9. Return descriptors and paths without deciding which lanes are required or
|
||||||
|
whether their configured contracts match.
|
||||||
|
10. Do not implement automatic `notarius config validate` execution.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- exact argument order and omission of `--session-id`;
|
||||||
|
- working directory, environment inheritance, stream separation, cancellation,
|
||||||
|
timeout, and nonzero exit;
|
||||||
|
- stdout ignored after failure;
|
||||||
|
- receipt size limit, malformed JSON, unsupported version, missing fields,
|
||||||
|
pipeline mismatch, and tolerated unknown fields;
|
||||||
|
- output-directory escape from the requested output root;
|
||||||
|
- index malformed/unsupported shapes, duplicate lanes, missing management
|
||||||
|
paths, and tolerated optional fields;
|
||||||
|
- malformed or oversized rejection/warning documents and tolerated unknown
|
||||||
|
summary fields;
|
||||||
|
- absolute logical paths, lexical traversal, root-prefix confusion, symlink
|
||||||
|
paths, and descriptor escape attempts; and
|
||||||
|
- valid lane plus chunk-map/evidence-context discovery.
|
||||||
|
|
||||||
|
Use a test helper subprocess or injected shared runner. Do not require a real
|
||||||
|
Notarius binary.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- The adapter fully enforces the generic Notarius subprocess and discovery
|
||||||
|
boundary without D&D policy.
|
||||||
|
- `go test ./internal/adapters/notarius ./internal/adapters/subprocess` and
|
||||||
|
`go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 5: Implement Extract-Stage Execution And Bundle Materialization
|
||||||
|
|
||||||
|
Implement the stage behind direct package tests, but do not yet add it to the
|
||||||
|
canonical `stage.All()` registry.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Add `Notarius notarius.Runner` to `stage.Env` and default production
|
||||||
|
composition in `internal/app/runner.go` when enabled extraction is selected.
|
||||||
|
2. Add canonical path helpers for:
|
||||||
|
- the run-local extract directory;
|
||||||
|
- receipt and stderr paths;
|
||||||
|
- the run-local Notarius output root; and
|
||||||
|
- immutable `artifacts/notarius/<narratio-run-id>` destinations.
|
||||||
|
3. Implement `extractStage` with name `extract` and declarations matching its
|
||||||
|
final-trimmed input and structured outputs.
|
||||||
|
4. When configuration is absent or disabled, return the explicit skipped
|
||||||
|
result with `notarius_disabled` and no outputs.
|
||||||
|
5. Resolve `narratio.transcript.final_trimmed` through the manifest-aware
|
||||||
|
resolver. Do not fall back to guessed filenames outside that resolver.
|
||||||
|
6. Invoke the adapter once with resolved absolute paths.
|
||||||
|
7. Apply required-output policy:
|
||||||
|
- find each configured output by exact lane ID;
|
||||||
|
- require exactly one descriptor;
|
||||||
|
- compare media type, schema ID/version, and optional module key;
|
||||||
|
- treat rejected or absent configured lanes as failure;
|
||||||
|
- tolerate unconfigured lanes;
|
||||||
|
- require each selected file to be regular, non-empty, and syntactically
|
||||||
|
valid JSON; and
|
||||||
|
- compute a staging checksum for later copy verification.
|
||||||
|
8. Validate the complete bundle tree for promotion, then use Stage 3's helper
|
||||||
|
to copy it to the unique immutable destination.
|
||||||
|
9. Re-resolve the promoted index and configured descriptor paths rather than
|
||||||
|
retaining staging paths. Compute authoritative SHA-256 checksums from the
|
||||||
|
promoted files and fail if promoted bytes differ from the validated source.
|
||||||
|
10. Return one `artifacts.Ref` per configured lane with explicit extraction
|
||||||
|
source ID, checksum, contract metadata, and Notarius external provenance.
|
||||||
|
11. Return the promoted `index.json` as a `notarius_index` output without a
|
||||||
|
selectable source ID.
|
||||||
|
12. Return bounded stage metadata containing durable bundle root, receipt and
|
||||||
|
diagnostic paths, receipt counts/status, rejection/warning summaries, the
|
||||||
|
producing Narratio run ID, and a deterministic fingerprint of the resolved
|
||||||
|
binary, config path, pipeline ID, timeout, working directory, and sorted
|
||||||
|
output-contract map.
|
||||||
|
13. Do not delete run-local diagnostics or failed bundles.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- disabled self-skip;
|
||||||
|
- missing/invalid final-trimmed input before adapter invocation;
|
||||||
|
- exact adapter request construction;
|
||||||
|
- required, missing, rejected, duplicate, incompatible, and unconfigured
|
||||||
|
lanes;
|
||||||
|
- non-empty and JSON-syntax validation without D&D decoding;
|
||||||
|
- immutable promotion and promoted-path re-resolution;
|
||||||
|
- checksums, explicit source IDs, nested metadata, and external provenance;
|
||||||
|
- adapter and promotion failures do not return successful outputs; and
|
||||||
|
- complete bundle files are preserved while unknown files are not registered
|
||||||
|
as sources.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- Direct extract-stage execution produces a complete manifest-ready result
|
||||||
|
using only a fake Notarius runner.
|
||||||
|
- The stage is not yet in normal CLI plans.
|
||||||
|
- `go test ./internal/stage ./internal/artifacts ./internal/fileops` and
|
||||||
|
`go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 6: Integrate Canonical Ordering, CLI Lifecycle, Force, And Resume
|
||||||
|
|
||||||
|
Make `extract` a real pipeline stage and complete its orchestration semantics.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Register `extractStage` in `stage.All()` immediately after `trim` and before
|
||||||
|
`render`.
|
||||||
|
2. Update all hard-coded or user-visible stage inventories, stage-name
|
||||||
|
validation, CLI help, planning, prerequisite checks, and test fixtures.
|
||||||
|
3. Confirm full and single-stage commands can select `extract`. Do not add a
|
||||||
|
lane-selection flag and do not broaden `--artifacts` beyond Scriptorium
|
||||||
|
artifact selection.
|
||||||
|
4. Add an optional stage resume-validation contract. Use a result containing:
|
||||||
|
- resumable boolean; and
|
||||||
|
- bounded non-resumable reason.
|
||||||
|
Reserve an ordinary error for cases where a safe decision cannot be made.
|
||||||
|
5. Before skipping a succeeded stage, `executeStages` invokes the optional
|
||||||
|
validator. A non-resumable result marks the session stage stale with its
|
||||||
|
reason and schedules it to run. A validation error stops execution without
|
||||||
|
mutating the succeeded record.
|
||||||
|
6. Implement extraction resume validation against:
|
||||||
|
- enabled state;
|
||||||
|
- stored invocation/config-contract fingerprint;
|
||||||
|
- succeeded extract record;
|
||||||
|
- immutable bundle and index paths;
|
||||||
|
- exact configured source set;
|
||||||
|
- regular confined lane files;
|
||||||
|
- checksums;
|
||||||
|
- contracts; and
|
||||||
|
- Notarius external provenance.
|
||||||
|
Require the producing Narratio run ID recorded in extract metadata, output
|
||||||
|
records, and the immutable bundle path to agree. Do not compare it with the
|
||||||
|
session manifest's top-level `run_id`, which identifies the latest
|
||||||
|
invocation and may legitimately differ after resume.
|
||||||
|
7. Do not inspect or hash Notarius's transitive reference/profile closure.
|
||||||
|
8. Verify canonical downstream invalidation:
|
||||||
|
- force through `trim` stales extract and later succeeded stages;
|
||||||
|
- force `extract` stales render and later succeeded stages;
|
||||||
|
- force `render` does not stale extract.
|
||||||
|
9. Verify existing session manifests without an extract record normalize and
|
||||||
|
execute the new stage rather than failing migration.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- exact canonical order and CLI stage inventory;
|
||||||
|
- full-plan and `run-stage extract` behavior;
|
||||||
|
- disabled skip followed by later enablement;
|
||||||
|
- successful resume skip;
|
||||||
|
- missing, tampered, incompatible, or config-mismatched outputs trigger stale
|
||||||
|
and rerun;
|
||||||
|
- unsafe validation errors stop without corrupting prior success;
|
||||||
|
- force and downstream invalidation in each direction named above; and
|
||||||
|
- old manifest compatibility.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- Normal execution includes `extract` in the agreed position.
|
||||||
|
- Disabled and resume behavior cannot make obsolete outputs current.
|
||||||
|
- `go test ./internal/stage ./internal/app ./internal/manifest`,
|
||||||
|
`go test ./...`, `go vet ./...`, and `go build ./cmd/narratio` pass.
|
||||||
|
|
||||||
|
## Stage 7: Integrate Extraction Sources With The Runtime Catalog And Analyze
|
||||||
|
|
||||||
|
Make succeeded extraction lanes selectable by configured Scriptorium
|
||||||
|
artifacts.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Extend `ArtifactCatalog` with extraction definitions registered from the
|
||||||
|
effective Notarius output map.
|
||||||
|
2. Add one shared manifest-backed hydration operation for extraction sources.
|
||||||
|
It accepts session paths, manifest, and configured extraction definitions
|
||||||
|
and marks entries available only after verifying the roadmap's full
|
||||||
|
successful-stage, source, path, checksum, contract, and provenance rules.
|
||||||
|
3. Add a distinct extraction provenance value such as
|
||||||
|
`manifest.current_extract_run`.
|
||||||
|
4. Extend `ResolveSessionArtifactWithCatalog` to resolve configured extraction
|
||||||
|
sources through catalog availability. It must never scan the Notarius
|
||||||
|
artifact directory or accept an incidental file.
|
||||||
|
5. Refactor catalog construction enough that extraction registration and
|
||||||
|
hydration are reusable by analyze and publish. Do not create a generic
|
||||||
|
workflow abstraction or move Scriptorium execution policy into artifacts.
|
||||||
|
6. Update analyze catalog creation to include effective extraction
|
||||||
|
definitions and current manifest state before planning Scriptorium inputs.
|
||||||
|
7. Preserve required/optional input behavior. Required unavailable extraction
|
||||||
|
inputs fail with actionable extract configuration/rerun guidance; optional
|
||||||
|
inputs are omitted normally.
|
||||||
|
8. Do not automatically add extraction inputs to any configured artifact.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- registration, lookup, deterministic ordering, and source-family collision;
|
||||||
|
- successful current-manifest hydration;
|
||||||
|
- missing stage, skipped, failed, stale, interrupted, missing-source,
|
||||||
|
internally inconsistent producer identity, incompatible-contract,
|
||||||
|
missing-file, tampered-checksum, and unsafe-path rejection;
|
||||||
|
- required and optional analyze input behavior;
|
||||||
|
- selected Scriptorium artifact execution receives only explicitly configured
|
||||||
|
extraction inputs; and
|
||||||
|
- no object-store calls or directory scanning occur during analyze
|
||||||
|
extraction resolution.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- A fake successful extract run can feed selected JSON files into a fake
|
||||||
|
Scriptorium analysis run.
|
||||||
|
- No failed or incidental bundle can become available through the catalog.
|
||||||
|
- `go test ./internal/artifacts ./internal/stage ./internal/config` and
|
||||||
|
`go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 8: Integrate Extraction Sources With Publish And Operator Inspection
|
||||||
|
|
||||||
|
Complete downstream artifact handling without automatically publishing the
|
||||||
|
bundle.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Use Stage 7's shared extraction catalog registration and hydration in
|
||||||
|
publish planning and source resolution.
|
||||||
|
2. Permit explicit publish rules whose source is a configured extraction
|
||||||
|
source. Preserve existing destination policy, lock behavior, required
|
||||||
|
output policy, selected-Scriptorium behavior, and remote commit order.
|
||||||
|
3. Do not publish the complete Notarius bundle unless individual explicit
|
||||||
|
rules name selectable sources. `notarius_index` remains non-selectable in
|
||||||
|
the initial feature.
|
||||||
|
4. Ensure the published session manifest retains extraction artifact contract
|
||||||
|
and external provenance metadata through ordinary serialization.
|
||||||
|
5. Extend existing artifact/status/inspection output only where necessary so
|
||||||
|
configured extraction sources report planned, available, unavailable, and
|
||||||
|
published states consistently. Do not expose payload contents.
|
||||||
|
6. Confirm restore safely round-trips explicitly published extraction files
|
||||||
|
through existing manifest and confined-path behavior; add code only if a
|
||||||
|
real incompatibility is found.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- successful explicit lane publication;
|
||||||
|
- required missing or invalid extraction source failure;
|
||||||
|
- optional/unselected behavior remains consistent;
|
||||||
|
- publish lock and commit-marker order remain unchanged;
|
||||||
|
- `--artifacts` does not partially select Notarius lanes;
|
||||||
|
- manifest metadata survives publish serialization and restore loading; and
|
||||||
|
- operator inspection never reads or prints extraction payload bodies.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- Explicit extraction publish rules work through the shared catalog.
|
||||||
|
- No implicit whole-bundle publication is introduced.
|
||||||
|
- `go test ./internal/stage ./internal/app ./internal/artifacts` and
|
||||||
|
`go test ./...` pass.
|
||||||
|
|
||||||
|
## Stage 9: Add Maintained Examples, Current Documentation, And Final Validation
|
||||||
|
|
||||||
|
Finish the public and maintainer contract only after the implementation is
|
||||||
|
complete.
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
|
||||||
|
1. Read Notarius's current published lane contracts and add exact media type,
|
||||||
|
schema ID, schema version, and module-key constraints for all ten lanes to
|
||||||
|
a maintained Narratio complete example. Do not guess or copy stale roadmap
|
||||||
|
placeholders.
|
||||||
|
2. Add a small maintained Scriptorium example that consumes a purpose-specific
|
||||||
|
subset of extraction sources rather than all ten.
|
||||||
|
3. Add `docs/integrations/notarius.md` as Narratio's external-consumer
|
||||||
|
contract. Link to Notarius's canonical documents instead of duplicating
|
||||||
|
complete schemas.
|
||||||
|
4. Add `docs/internal/stage-extract.md` for implemented stage mechanics,
|
||||||
|
lifecycle, resume validation, failure behavior, and focused tests.
|
||||||
|
5. Update the canonical owners listed in the roadmap's Documentation
|
||||||
|
Deliverables section. Keep configuration fields/defaults only in
|
||||||
|
`docs/config.md`, commands only in `docs/cli.md`, physical layout and force
|
||||||
|
procedures only in `docs/operations.md`, and implementation mechanics only
|
||||||
|
in internal documents.
|
||||||
|
6. Update every implemented stage inventory to show `extract` between `trim`
|
||||||
|
and `render`.
|
||||||
|
7. Add troubleshooting guidance for missing Notarius, nonzero exit,
|
||||||
|
receipt/index incompatibility, required-lane rejection, resume
|
||||||
|
invalidation, and the requirement to force extraction after changing
|
||||||
|
Notarius's transitive configuration inputs.
|
||||||
|
8. Review the roadmap status. If every acceptance criterion is implemented,
|
||||||
|
mark it complete or move completed planning material according to repository
|
||||||
|
convention without deleting useful decision context prematurely.
|
||||||
|
9. Check all changed links, examples, commands, field names, defaults, schemas,
|
||||||
|
and paths against the implementation.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- focused example/configuration validation tests;
|
||||||
|
- `go test ./...`;
|
||||||
|
- `go vet ./...`;
|
||||||
|
- `go build ./cmd/narratio`;
|
||||||
|
- inspect `git diff --check`;
|
||||||
|
- confirm ordinary tests use no live Notarius, PromptKit, LLM provider,
|
||||||
|
credentials, network service, or mutable external state; and
|
||||||
|
- confirm no secrets or private campaign content were added.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- Every feature-roadmap acceptance criterion is satisfied.
|
||||||
|
- Maintained examples load under strict configuration validation.
|
||||||
|
- Current-behavior documentation accurately describes the shipped feature
|
||||||
|
without duplicating Notarius-owned schema definitions.
|
||||||
|
- The repository-wide validation suite passes.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
None. The target architecture, defaults, lifecycle behavior, compatibility
|
||||||
|
boundary, storage layout, source identity, downstream selection model, and
|
||||||
|
implementation order are decision-complete for implementation. If repository
|
||||||
|
inspection exposes a contradiction with an implemented invariant, stop at the
|
||||||
|
affected stage and document the concrete conflict rather than silently
|
||||||
|
changing this plan's target behavior.
|
||||||
@@ -2,266 +2,497 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed.
|
Proposed. Implementation has not started.
|
||||||
|
|
||||||
|
The ordered implementation plan is maintained in
|
||||||
|
[implementation.md](implementation.md). Until that plan is complete, this
|
||||||
|
document is the only Narratio documentation that describes the proposed
|
||||||
|
`extract` stage, Notarius configuration, or `narratio.extraction.*` sources.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
Add a first-class Narratio `extract` stage that runs Notarius against the
|
Add a first-class Narratio `extract` stage that runs Notarius against the
|
||||||
session's final trimmed transcript, validates and collects the resulting
|
session's final trimmed transcript and makes validated structured artifacts
|
||||||
structured D&D artifacts, and registers those artifacts for later use by the
|
available to later analysis and publish work.
|
||||||
`analyze` and `publish` stages.
|
|
||||||
|
|
||||||
This feature should integrate Notarius through Narratio's existing stage,
|
Notarius remains responsible for its D&D extraction pipeline, prompts,
|
||||||
adapter, manifest, workspace, and artifact-catalog boundaries. It must not turn
|
references, LLM profiles, retries, validation, normalization, and published
|
||||||
Narratio into a generic workflow engine or a second configuration language for
|
schemas. Narratio owns invocation, required-output policy, safe bundle
|
||||||
Notarius pipelines.
|
ingestion, artifact identity, manifest state, resume, and downstream
|
||||||
|
availability.
|
||||||
|
|
||||||
|
The integration must preserve Narratio's explicit stage model. It must not
|
||||||
|
turn Narratio into a generic workflow engine or reproduce Notarius's
|
||||||
|
configuration language.
|
||||||
|
|
||||||
## User Outcome
|
## User Outcome
|
||||||
|
|
||||||
An operator can enable one configured Notarius pipeline for a Narratio
|
An operator can enable one Notarius pipeline for a Narratio campaign. During a
|
||||||
campaign. During a normal run, Narratio will:
|
normal run, Narratio will:
|
||||||
|
|
||||||
1. finish producing the session transcript tiers;
|
1. produce the final trimmed Seriatim JSON transcript;
|
||||||
2. invoke Notarius once with the final trimmed Seriatim JSON transcript;
|
2. invoke Notarius once with that transcript;
|
||||||
3. collect and validate the configured structured artifact lanes;
|
3. discover the exact run bundle through Notarius's machine-readable receipt;
|
||||||
4. record their exact files and provenance in the Narratio manifest; and
|
4. validate every output contract Narratio is configured to require;
|
||||||
5. make those artifacts selectable as inputs to Scriptorium artifacts in the
|
5. promote the complete validated bundle into immutable session storage;
|
||||||
later `analyze` stage.
|
6. record exact artifact paths, checksums, contracts, and external provenance;
|
||||||
|
7. make configured lanes available as `narratio.extraction.<key>` sources; and
|
||||||
|
8. allow individual Scriptorium artifacts and publish rules to select those
|
||||||
|
sources explicitly.
|
||||||
|
|
||||||
The maintained D&D example should demonstrate all ten lanes emitted by
|
The maintained complete D&D example will require all ten lanes published by
|
||||||
Notarius's complete `dnd-session` pipeline.
|
Notarius's `dnd-session` pipeline. Ordinary deployments may configure a
|
||||||
|
narrower required set.
|
||||||
|
|
||||||
## Target Stage Architecture
|
## Chosen Architecture
|
||||||
|
|
||||||
|
### First-Class Stage
|
||||||
|
|
||||||
|
Extraction is an independently observable and resumable pipeline stage. It is
|
||||||
|
not part of `analyze`, a Scriptorium artifact producer, or an implicit external
|
||||||
|
preprocessing requirement.
|
||||||
|
|
||||||
|
This boundary is required because Notarius work is expensive, produces
|
||||||
|
multiple durable outputs, has its own compatibility and diagnostic contracts,
|
||||||
|
and may be consumed by both `analyze` and `publish`.
|
||||||
|
|
||||||
### Canonical Order
|
### Canonical Order
|
||||||
|
|
||||||
The canonical stage order becomes:
|
The canonical stage order becomes:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
prepare -> transcribe -> merge -> polish -> normalize -> trim -> render
|
prepare -> transcribe -> merge -> polish -> normalize -> trim
|
||||||
-> extract -> analyze -> publish -> notify
|
-> extract -> render -> analyze -> publish -> notify
|
||||||
```
|
```
|
||||||
|
|
||||||
`extract` is deliberately after all transcript-producing stages and before
|
`extract` consumes `narratio.transcript.final_trimmed`, produced by `trim`. It
|
||||||
analysis. Its source document is the manifest-resolved
|
does not consume rendered Markdown.
|
||||||
`narratio.transcript.final_trimmed` artifact, normally
|
|
||||||
`transcripts/final.trimmed.json`. It does not consume rendered Markdown.
|
|
||||||
|
|
||||||
Adding the stage must update full-plan construction, explicit stage selection,
|
Narratio currently invalidates succeeded stages by canonical downstream order.
|
||||||
downstream invalidation, prerequisite checks, resume behavior, run manifests,
|
Placing `extract` before `render` means forcing extraction may rerun the less
|
||||||
CLI stage validation and help, and every canonical-stage inventory. Forcing an
|
expensive deterministic render stage, while forcing render does not rerun the
|
||||||
upstream transcript stage must stale a previously successful `extract` stage
|
more expensive Notarius pipeline. This is preferable to placing extraction
|
||||||
and its downstream stages. Forcing `extract` must stale `analyze`, `publish`,
|
after render and does not require dependency-aware scheduling or a DAG.
|
||||||
and `notify` according to existing rules.
|
|
||||||
|
Adding the stage must update every canonical-stage inventory, full-plan and
|
||||||
|
single-stage selection, prerequisite validation, downstream invalidation,
|
||||||
|
resume behavior, run manifests, CLI validation and help, and focused tests.
|
||||||
|
|
||||||
### Stage Boundary
|
### Stage Boundary
|
||||||
|
|
||||||
The stage owns Narratio policy and state transitions:
|
The stage owns Narratio policy:
|
||||||
|
|
||||||
- resolve the final trimmed transcript through the runtime artifact catalog;
|
- resolve the final trimmed transcript through the manifest-aware artifact
|
||||||
- build a Narratio-level Notarius request from validated configuration and
|
resolver;
|
||||||
run-local paths;
|
- allocate run-local receipt, log, and output-root paths;
|
||||||
- call a narrow Notarius adapter;
|
- build a transport-neutral Notarius request from resolved configuration;
|
||||||
- apply the configured required-output policy;
|
- call the configured Notarius adapter once;
|
||||||
- materialize the validated bundle into its canonical session location;
|
- enforce Narratio's configured required-output policy;
|
||||||
- return manifest-ready artifact references and bounded metadata; and
|
- validate selected payloads as regular, non-empty, syntactically valid JSON;
|
||||||
- fail without marking the stage successful when any required contract or
|
- promote the validated bundle to immutable session artifact storage;
|
||||||
materialization step fails.
|
- return explicit source IDs, checksums, contracts, and external provenance;
|
||||||
|
and
|
||||||
|
- report an explicit skipped outcome when Notarius is disabled.
|
||||||
|
|
||||||
The stage must not construct subprocess arguments, infer Notarius output
|
The stage must not construct subprocess arguments, guess Notarius filenames,
|
||||||
filenames, parse provider logs, or decode individual D&D payload bodies.
|
parse interactive output, decode D&D payload structures, or reproduce
|
||||||
|
Notarius pipeline configuration.
|
||||||
|
|
||||||
### Adapter Boundary
|
### Adapter Boundary
|
||||||
|
|
||||||
Add a dedicated Notarius adapter package with a small interface, production
|
Add `internal/adapters/notarius` with a narrow runner interface, production
|
||||||
subprocess implementation, and test fake. Its request should contain only the
|
subprocess implementation, and small fake.
|
||||||
resolved Notarius binary, configuration path, pipeline ID, transcript path,
|
|
||||||
output root, working directory, timeout, and process-log destinations needed
|
The request contains only:
|
||||||
for one run.
|
|
||||||
|
- resolved executable path or name;
|
||||||
|
- absolute Notarius configuration path;
|
||||||
|
- pipeline ID;
|
||||||
|
- absolute transcript path;
|
||||||
|
- absolute output root;
|
||||||
|
- working directory;
|
||||||
|
- timeout; and
|
||||||
|
- stdout receipt and stderr log destinations.
|
||||||
|
|
||||||
The adapter owns:
|
The adapter owns:
|
||||||
|
|
||||||
- optional `notarius config validate` preflight for the configured pipeline;
|
|
||||||
- exact `notarius run ... --json` argument construction;
|
- exact `notarius run ... --json` argument construction;
|
||||||
- stdout and stderr separation;
|
- stdout and stderr separation;
|
||||||
- context cancellation and timeout propagation through Narratio's shared
|
- context cancellation and timeout through Narratio's shared subprocess
|
||||||
subprocess boundary;
|
boundary;
|
||||||
|
- environment inheritance;
|
||||||
- exit-status handling;
|
- exit-status handling;
|
||||||
- decoding the `notarius.run-result.v1` success receipt;
|
- bounded receipt loading after exit status zero;
|
||||||
- receipt and index path-confinement checks;
|
- tolerant decoding of supported `notarius.run-result.v1` documents;
|
||||||
- decoding `index.json` and resolving descriptor paths safely beneath the
|
- validation of required receipt fields;
|
||||||
reported output directory; and
|
- confinement of the receipt's absolute `output_directory` beneath the
|
||||||
- returning a transport-neutral result containing the bundle location,
|
absolute output root Narratio supplied for this invocation;
|
||||||
receipt summary, lane descriptors, pipeline-wide descriptors, warnings and
|
- confinement of `index_file` beneath the receipt's absolute
|
||||||
rejection locations, and diagnostic log paths.
|
`output_directory`;
|
||||||
|
- tolerant decoding of the supported `index.json` contract;
|
||||||
|
- confinement of every index descriptor path beneath the bundle root; and
|
||||||
|
- bounded tolerant decoding of `rejected.json` and `warnings.json` into
|
||||||
|
transport-neutral summaries without reading lane payload bodies; summaries
|
||||||
|
persisted by Narratio contain structured scope, lane, and reason fields but
|
||||||
|
not unbounded free-form external messages; and
|
||||||
|
- returning transport-neutral receipt, descriptor, diagnostic, and bundle
|
||||||
|
information.
|
||||||
|
|
||||||
Only exit status zero permits receipt decoding. Receipt, index, or descriptor
|
Only exit status zero permits receipt decoding. Unsupported schema versions,
|
||||||
paths that are absolute where a logical relative path is required, or that
|
malformed documents, missing required fields, absolute logical paths, path
|
||||||
escape their owning root, are integration failures. Unknown fields in a
|
escapes, symlinks at consumed paths, and incompatible structural metadata are
|
||||||
supported receipt or index schema should be tolerated. Unsupported schema
|
integration failures.
|
||||||
versions and incompatible descriptor metadata should fail clearly.
|
|
||||||
|
|
||||||
The adapter must not write Narratio manifests, choose required lanes, decide
|
The production runner will not execute `notarius config validate` before every
|
||||||
analysis inputs, or contain D&D domain logic.
|
session. Notarius run-time validation remains authoritative, while operators
|
||||||
|
may use the separate validation command as deployment preflight. A second
|
||||||
|
automatic subprocess can be added later only if operational evidence warrants
|
||||||
|
it.
|
||||||
|
|
||||||
|
The adapter does not write Narratio manifests, decide required lanes, choose
|
||||||
|
analysis inputs, or interpret D&D payloads.
|
||||||
|
|
||||||
|
## External Contract Baseline
|
||||||
|
|
||||||
|
The integration consumes the contracts documented by Notarius in
|
||||||
|
`../notarius/docs/consumers/dnd-pipeline.md` and its linked canonical
|
||||||
|
integration documents.
|
||||||
|
|
||||||
|
The initial compatibility baseline is:
|
||||||
|
|
||||||
|
- `notarius run <pipeline-id> --config ... --input ... --output-dir ... --json`;
|
||||||
|
- successful receipt schema `notarius.run-result.v1`;
|
||||||
|
- an absolute receipt `output_directory`;
|
||||||
|
- logical `index_file` discovery beneath that directory;
|
||||||
|
- the production JSON `index.json` descriptor model; and
|
||||||
|
- the exact media type and schema identity configured for each required lane.
|
||||||
|
|
||||||
|
Compatibility is decided from these published contracts, not by parsing
|
||||||
|
`notarius --version`. Unknown fields in supported receipt and index versions
|
||||||
|
are tolerated. Unsupported versions or incompatible required descriptors fail
|
||||||
|
before any artifact becomes current in Narratio.
|
||||||
|
|
||||||
## Configuration Contract
|
## Configuration Contract
|
||||||
|
|
||||||
Add a strict optional `pipeline.notarius` configuration section. Omission or
|
Add a strict optional `pipeline.notarius` section:
|
||||||
`enabled: false` keeps the current workflow usable and causes `extract` to
|
|
||||||
self-skip without outputs.
|
|
||||||
|
|
||||||
The section should provide:
|
```yaml
|
||||||
|
notarius:
|
||||||
- `enabled`: explicit opt-in;
|
enabled: true
|
||||||
- `binary`: Notarius executable, defaulting to `notarius`;
|
binary: notarius
|
||||||
- `config_path`: required when enabled;
|
config_path: /absolute/path/to/notarius.yml
|
||||||
- `pipeline_id`: required when enabled;
|
pipeline_id: dnd-session
|
||||||
- `timeout`: a positive stage timeout with a documented default;
|
timeout: 3h
|
||||||
- `working_directory`: optional explicit subprocess working directory,
|
working_directory: /absolute/path/to/deployment
|
||||||
defaulting to the directory containing `config_path`; and
|
outputs:
|
||||||
- an `outputs` map defining the Notarius lane artifacts Narratio promises to
|
npc_registry:
|
||||||
collect.
|
lane_id: npc-registry
|
||||||
|
media_type: application/json
|
||||||
Each output-map key is a stable Narratio extraction key. Each value must define:
|
schema_id: <published-schema-id>
|
||||||
|
schema_version: <published-schema-version>
|
||||||
- the exact Notarius `lane_id`;
|
|
||||||
- the expected `media_type`;
|
|
||||||
- the expected `schema_id`;
|
|
||||||
- the expected `schema_version`; and
|
|
||||||
- optionally an expected `module_key` when the operator needs to constrain the
|
|
||||||
producing module as part of compatibility.
|
|
||||||
|
|
||||||
Narratio derives the downstream source ID
|
|
||||||
`narratio.extraction.<output-key>` from the map key. Keys and lane IDs must be
|
|
||||||
non-empty, unique after normalization, path-safe under the existing artifact
|
|
||||||
policy, and collision-free with built-in and configured artifact identities.
|
|
||||||
Every configured output is required: a successful Notarius process that omits
|
|
||||||
one, rejects it, or reports incompatible descriptor metadata fails the
|
|
||||||
`extract` stage.
|
|
||||||
|
|
||||||
This explicit map keeps Narratio's consumer contract stable when a Notarius
|
|
||||||
lane ID or schema changes and avoids hard-coding the current D&D family into a
|
|
||||||
generic adapter. It also replaces a separate `required_lanes` list, which would
|
|
||||||
duplicate configuration.
|
|
||||||
|
|
||||||
Narratio should not reproduce Notarius lane selection, references, LLM
|
|
||||||
profiles, model settings, retries, concurrency, or prompt configuration. Those
|
|
||||||
remain in the referenced Notarius configuration. Narratio should not expose a
|
|
||||||
runtime lane-selection flag for `extract`; one stage invocation runs the
|
|
||||||
configured Notarius pipeline as a unit.
|
|
||||||
|
|
||||||
All configured paths should become absolute during Narratio configuration
|
|
||||||
resolution. The deterministic default working directory allows a Notarius
|
|
||||||
profile path relative to that directory, but operator documentation should
|
|
||||||
still recommend absolute deployment paths where practical. Notarius reference
|
|
||||||
paths continue to follow Notarius's own configuration-relative rules.
|
|
||||||
|
|
||||||
## Output And Artifact Model
|
|
||||||
|
|
||||||
### Canonical Bundle
|
|
||||||
|
|
||||||
Run Notarius against a run-local output root. After all configured descriptors
|
|
||||||
are validated, materialize the contents of the exact run-specific Notarius
|
|
||||||
bundle into a fixed canonical session directory:
|
|
||||||
|
|
||||||
```text
|
|
||||||
artifacts/notarius/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Preserve its relative layout, including `index.json`, `manifest.json`,
|
Fields:
|
||||||
`rejected.json`, `warnings.json`, `lanes/`, and any indexed `chunk-map.json` or
|
|
||||||
`evidence-context.json`. Materialize the complete directory as one narrow,
|
|
||||||
transactional replacement so a failed or interrupted rerun cannot mix files
|
|
||||||
from different Notarius runs.
|
|
||||||
|
|
||||||
The raw subprocess receipt and stderr log belong in the run-local `extract`
|
- `enabled` is an explicit opt-in and defaults to false;
|
||||||
report and log directories. The raw receipt identifies the original run-local
|
- `binary` defaults to `notarius`;
|
||||||
Notarius bundle and must not be rewritten to pretend that the canonical copy
|
- `config_path` is required when enabled;
|
||||||
was its original `output_directory`. Narratio's manifest is the durable ledger
|
- `pipeline_id` is required when enabled;
|
||||||
for the canonical materialized paths.
|
- `timeout` defaults to `3h` and must be positive;
|
||||||
|
- `working_directory` is optional and defaults to the directory containing
|
||||||
|
`config_path`; and
|
||||||
|
- `outputs` maps stable Narratio extraction keys to required Notarius lane
|
||||||
|
contracts.
|
||||||
|
|
||||||
### Registered Artifact Sources
|
All configured paths become absolute during configuration resolution. Notarius
|
||||||
|
reference paths continue to follow Notarius's configuration-relative rules,
|
||||||
|
while PromptKit profile paths remain relative to the chosen process working
|
||||||
|
directory where Notarius permits that behavior.
|
||||||
|
|
||||||
For each configured output, locate the lane through the canonical copy of
|
Each output entry contains:
|
||||||
`index.json` and record a manifest artifact with:
|
|
||||||
|
|
||||||
- source ID `narratio.extraction.<output-key>`;
|
- exact `lane_id`;
|
||||||
- canonical lane-file path discovered from the index;
|
- exact `media_type`;
|
||||||
|
- exact `schema_id`;
|
||||||
|
- exact `schema_version`; and
|
||||||
|
- optional `module_key`.
|
||||||
|
|
||||||
|
The map key produces `narratio.extraction.<key>`. Keys use Narratio's existing
|
||||||
|
path-safe configured-artifact key grammar. Keys, normalized source IDs, and
|
||||||
|
lane IDs must be non-empty and unique. Extraction source IDs must not collide
|
||||||
|
with built-ins or configured Scriptorium sources.
|
||||||
|
|
||||||
|
Every configured output is required for extraction-stage success. Operators
|
||||||
|
who need only a subset configure only that subset. Narratio does not add a
|
||||||
|
second `required_lanes` list or a runtime lane-selection flag.
|
||||||
|
|
||||||
|
Narratio does not configure Notarius lane topology, references, prompts, LLM
|
||||||
|
profiles, model settings, concurrency, retry behavior, or session IDs. One
|
||||||
|
stage execution runs the configured Notarius pipeline as a unit. Narratio does
|
||||||
|
not pass Notarius's `--session-id` override.
|
||||||
|
|
||||||
|
## Stage Outcomes And Resume
|
||||||
|
|
||||||
|
### Explicit Self-Skip
|
||||||
|
|
||||||
|
Extend the stage-result contract with an explicit disposition whose zero value
|
||||||
|
remains successful for backward compatibility. A stage may return:
|
||||||
|
|
||||||
|
- succeeded; or
|
||||||
|
- skipped with a stable reason.
|
||||||
|
|
||||||
|
When Notarius is absent or disabled, `extract` returns skipped with reason
|
||||||
|
`notarius_disabled`, no outputs, and bounded metadata. The runner records a
|
||||||
|
skipped session-stage and run-stage outcome rather than a successful empty
|
||||||
|
stage. Skipped stages are reconsidered on later invocations, so subsequently
|
||||||
|
enabling Notarius causes extraction to run without requiring force.
|
||||||
|
|
||||||
|
A genuine self-skip clears any older outputs for that stage before persisting
|
||||||
|
the new skipped state. Downstream consumers cannot resolve artifacts retained
|
||||||
|
from an earlier extraction after the stage is disabled.
|
||||||
|
|
||||||
|
### Resume Validation
|
||||||
|
|
||||||
|
Add a small optional resume-validation interface implemented by `extract`.
|
||||||
|
Before skipping an already-succeeded extract stage, the runner asks it whether
|
||||||
|
the recorded result remains resumable.
|
||||||
|
|
||||||
|
The resume validator confirms:
|
||||||
|
|
||||||
|
- Notarius is still enabled;
|
||||||
|
- recorded configuration identity matches the current config path, pipeline
|
||||||
|
ID, and output-contract map;
|
||||||
|
- the immutable bundle and canonical index still exist;
|
||||||
|
- every configured source is present in the succeeded extract record;
|
||||||
|
- lane files remain confined regular files;
|
||||||
|
- stored checksums still match; and
|
||||||
|
- stored descriptor contracts and external provenance remain compatible.
|
||||||
|
|
||||||
|
An ordinary contract mismatch returns a non-resumable decision with a bounded
|
||||||
|
reason. The runner marks the stage stale and executes it. An environmental
|
||||||
|
error that prevents making a safe decision returns an error and stops the run.
|
||||||
|
|
||||||
|
Narratio does not recursively interpret Notarius configuration, PromptKit
|
||||||
|
profiles, or reference files. Changes to those external inputs are therefore
|
||||||
|
not automatically detectable. Operator documentation must require
|
||||||
|
`--force extract` after changing them.
|
||||||
|
|
||||||
|
The configuration fingerprint covers the resolved binary, config path,
|
||||||
|
pipeline ID, timeout, working directory, and the deterministically sorted
|
||||||
|
output-contract map. It identifies Narratio's invocation contract, not the
|
||||||
|
transitive content of files owned by Notarius.
|
||||||
|
|
||||||
|
### Force And Invalidation
|
||||||
|
|
||||||
|
- Forcing `trim` or an earlier stage stales succeeded `extract` and all later
|
||||||
|
stages.
|
||||||
|
- Forcing `extract` stales succeeded `render`, `analyze`, `publish`, and
|
||||||
|
`notify` under the existing canonical-order rule.
|
||||||
|
- Forcing `render` does not stale `extract` because extraction precedes it.
|
||||||
|
- A failed, skipped, stale, or interrupted extract stage never supplies current
|
||||||
|
extraction sources.
|
||||||
|
|
||||||
|
## Bundle Storage And Commit
|
||||||
|
|
||||||
|
### Run-Local Execution
|
||||||
|
|
||||||
|
Notarius runs against a run-local output root beneath the Narratio run's
|
||||||
|
`extract` directory. Receipt bytes and stderr remain run-local diagnostics.
|
||||||
|
Failed and malformed bundles remain outside durable artifact storage for
|
||||||
|
inspection and never become current merely because files exist.
|
||||||
|
|
||||||
|
### Immutable Durable Bundles
|
||||||
|
|
||||||
|
After complete validation, promote the exact Notarius bundle into:
|
||||||
|
|
||||||
|
```text
|
||||||
|
artifacts/notarius/<narratio-run-id>/
|
||||||
|
```
|
||||||
|
|
||||||
|
The destination is unique and must not already exist. Promotion uses a sibling
|
||||||
|
temporary directory on the same filesystem, recursively copies only regular
|
||||||
|
files and directories, rejects symlinks and special files, preserves relative
|
||||||
|
layout, and renames the completed temporary tree into place.
|
||||||
|
|
||||||
|
The promoted tree preserves `index.json`, `manifest.json`, `rejected.json`,
|
||||||
|
`warnings.json`, `lanes/`, and any emitted pipeline-wide artifacts such as
|
||||||
|
`chunk-map.json` and `evidence-context.json`. Unknown regular files may be
|
||||||
|
preserved because the complete bundle is provenance, but no unknown file is
|
||||||
|
registered as a stable Narratio source.
|
||||||
|
|
||||||
|
There is no mutable filesystem `current` directory or symlink. The atomically
|
||||||
|
saved Narratio session manifest selects the current successful bundle. Older
|
||||||
|
successful bundles remain immutable until explicit cleanup policy removes
|
||||||
|
them.
|
||||||
|
|
||||||
|
The raw Notarius receipt is retained without rewriting its original
|
||||||
|
`output_directory`. Narratio's manifest records promoted durable paths.
|
||||||
|
|
||||||
|
## Artifact And Manifest Model
|
||||||
|
|
||||||
|
### Explicit Source Identity
|
||||||
|
|
||||||
|
Extend `artifacts.Ref` with an optional explicit `SourceID`. The application
|
||||||
|
runner prefers that value and retains existing stage-specific inference only
|
||||||
|
as a backward-compatible fallback. `extract` must not require another
|
||||||
|
stage-name special case in output mapping.
|
||||||
|
|
||||||
|
Add neutral optional artifact metadata models under `internal/artifactmodel`:
|
||||||
|
|
||||||
|
- contract metadata: media type, schema ID, schema version, and optional
|
||||||
|
module key; and
|
||||||
|
- external provenance: system, external run ID, pipeline ID, and external
|
||||||
|
artifact ID.
|
||||||
|
|
||||||
|
Both `artifacts.Ref` and `manifest.ArtifactRecord` carry these nested models.
|
||||||
|
Existing manifests remain readable because the fields are optional and use
|
||||||
|
`omitempty` encoding.
|
||||||
|
|
||||||
|
For a Notarius lane, external provenance uses:
|
||||||
|
|
||||||
|
- system `notarius`;
|
||||||
|
- receipt run ID;
|
||||||
|
- receipt pipeline ID; and
|
||||||
|
- lane ID as the external artifact ID.
|
||||||
|
|
||||||
|
### Registered Sources
|
||||||
|
|
||||||
|
For each configured output, `extract` records one artifact with:
|
||||||
|
|
||||||
|
- source ID `narratio.extraction.<key>`;
|
||||||
|
- durable lane path discovered through the promoted canonical index;
|
||||||
- producer stage and Narratio run ID;
|
- producer stage and Narratio run ID;
|
||||||
- checksum;
|
- SHA-256 checksum;
|
||||||
- Notarius lane ID; and
|
- configured and observed contract metadata; and
|
||||||
- descriptor media type, schema identity/version, and module key when present.
|
- Notarius external provenance.
|
||||||
|
|
||||||
If the current manifest model cannot carry descriptor compatibility metadata,
|
The canonical promoted `index.json` is also a stage output with kind
|
||||||
extend its artifact metadata in a backward-tolerant way rather than encoding
|
`notarius_index`, but it is not a selectable extraction source. Extract-stage
|
||||||
that information in filenames or source IDs.
|
metadata records the durable bundle root, receipt path, Notarius validation
|
||||||
|
status, counts, warnings/rejections paths and summaries, the producing Narratio
|
||||||
|
run ID, and the normalized configured-contract fingerprint used by resume
|
||||||
|
validation.
|
||||||
|
|
||||||
Also record the canonical Notarius index as a stage output or stage metadata so
|
The producing Narratio run ID belongs to the successful extract result. It is
|
||||||
operators can discover the complete bundle, including non-lane artifacts. The
|
not compared with the session manifest's top-level `run_id`, which advances on
|
||||||
configured lane sources are the stable interface for analysis; the index and
|
later invocations even when extraction is validly resumed. Resume and catalog
|
||||||
bundle remain the provenance and inspection interface.
|
checks instead require the extract outputs, immutable bundle path, and stored
|
||||||
|
extract-stage producer identity to agree with one another.
|
||||||
|
|
||||||
## Analysis And Publish Integration
|
## Artifact Catalog And Resolution
|
||||||
|
|
||||||
Extend the runtime artifact catalog and configured Scriptorium input validation
|
Add extraction as a first-class artifact-policy and runtime-catalog family:
|
||||||
so an enabled analysis artifact can declare, for example:
|
|
||||||
|
- source kind `extraction`;
|
||||||
|
- canonical prefix `narratio.extraction.`;
|
||||||
|
- registration from `pipeline.notarius.outputs`; and
|
||||||
|
- manifest-backed availability from the current successful `extract` record.
|
||||||
|
|
||||||
|
Extraction availability is never inferred by scanning
|
||||||
|
`artifacts/notarius/`. A source is available only when:
|
||||||
|
|
||||||
|
- it is declared in current configuration;
|
||||||
|
- the session manifest records `extract` as succeeded and not stale;
|
||||||
|
- the exact matching source output is present;
|
||||||
|
- its durable path is confined and valid;
|
||||||
|
- its checksum matches; and
|
||||||
|
- its recorded contract and external provenance are compatible.
|
||||||
|
|
||||||
|
The catalog should expose one shared registration and hydration path used by
|
||||||
|
both `analyze` and `publish`. Avoid parallel extract-specific resolution logic
|
||||||
|
inside each stage.
|
||||||
|
|
||||||
|
## Analyze Integration
|
||||||
|
|
||||||
|
An enabled Scriptorium artifact may select an extraction source through the
|
||||||
|
existing input contract:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
inputs:
|
inputs:
|
||||||
npc_registry:
|
npcs:
|
||||||
source: narratio.extraction.npc_registry
|
source: narratio.extraction.npc_registry
|
||||||
|
required: true
|
||||||
```
|
```
|
||||||
|
|
||||||
Resolution must remain manifest-first and verify that the recorded artifact
|
Required missing extraction inputs fail with guidance to enable/configure or
|
||||||
was produced by a successful current `extract` stage. A required extraction
|
rerun `extract`. Optional missing inputs follow the existing Scriptorium input
|
||||||
source that is unavailable must fail analysis with guidance to configure or
|
contract.
|
||||||
rerun `extract`; an optional source may be omitted according to the existing
|
|
||||||
Scriptorium input contract.
|
|
||||||
|
|
||||||
Publish source resolution should accept configured
|
Narratio never injects every extraction output into every analysis. Each
|
||||||
`narratio.extraction.<output-key>` sources through the same artifact catalog so
|
Scriptorium artifact chooses the smallest useful set. This limits context
|
||||||
operators may publish selected structured artifacts without manually copying
|
size, cost, and the risk of treating derived claims as transcript authority.
|
||||||
paths. The existing `--artifacts` flag remains scoped to Scriptorium artifact
|
Analysis prompts should continue to treat the transcript as authoritative and
|
||||||
selection and must not partially execute the Notarius pipeline.
|
Notarius artifacts as structured, cited, derived evidence.
|
||||||
|
|
||||||
No current-session analysis artifact should consume an incidental file from a
|
The existing `--artifacts` selection remains scoped to Scriptorium artifacts.
|
||||||
failed, stale, skipped, or superseded extraction run.
|
It does not select Notarius lanes or partially run the Notarius pipeline.
|
||||||
|
|
||||||
## Failure, Skip, Resume, And Diagnostics
|
## Publish Integration
|
||||||
|
|
||||||
- Missing or invalid enabled Notarius configuration fails configuration
|
Publish source validation and resolution accept configured
|
||||||
validation before stage execution where statically discoverable.
|
`narratio.extraction.<key>` sources through the shared runtime artifact
|
||||||
- A disabled or absent Notarius configuration makes `extract` skip with clear
|
catalog. Operators may publish individual structured lanes without manually
|
||||||
stage metadata and no new outputs.
|
copying files.
|
||||||
- A missing or invalid final trimmed transcript fails `extract` before starting
|
|
||||||
Notarius.
|
|
||||||
- Preflight failure, nonzero Notarius exit, cancellation, timeout, malformed or
|
|
||||||
unsupported receipt/index data, unsafe paths, incompatible descriptors,
|
|
||||||
rejected required outputs, or missing configured lanes fails the entire
|
|
||||||
stage.
|
|
||||||
- Process success does not override Narratio's required-output policy.
|
|
||||||
- A failed run retains bounded run-local receipt bytes, stderr, and the
|
|
||||||
unpublished Notarius bundle for diagnosis, subject to Narratio's existing
|
|
||||||
sensitive-data and cleanup policies.
|
|
||||||
- The canonical bundle and manifest artifacts are updated only after complete
|
|
||||||
validation and materialization.
|
|
||||||
- Resume skips a succeeded, non-stale `extract` stage only when its
|
|
||||||
manifest-recorded canonical index and configured lane outputs still validate.
|
|
||||||
- Force and staleness behavior follows the ordinary stage contract; it must not
|
|
||||||
depend on merely finding `artifacts/notarius/` on disk.
|
|
||||||
|
|
||||||
Transcripts, Notarius outputs, evidence context, manifests, receipts, and logs
|
The Notarius bundle is not automatically published wholesale. Bundle files or
|
||||||
are private campaign material. Subprocess arguments and manifest metadata must
|
lanes are published only through explicit configured publish rules. Existing
|
||||||
not contain secrets. Credentials remain in the environment or in mechanisms
|
publish locking, destination safety, commit ordering, and required/unselected
|
||||||
owned by Notarius and PromptKit.
|
artifact behavior remain unchanged.
|
||||||
|
|
||||||
## Maintained D&D Example
|
## Failure And Diagnostic Semantics
|
||||||
|
|
||||||
Add or update a Narratio example that enables Notarius's complete
|
The stage fails before invoking Notarius when enabled configuration or the
|
||||||
`dnd-session` pipeline and maps these ten required lanes to stable extraction
|
final trimmed transcript is invalid.
|
||||||
keys:
|
|
||||||
|
|
||||||
| Output key | Notarius lane ID |
|
The stage fails after invocation for:
|
||||||
|
|
||||||
|
- cancellation or timeout;
|
||||||
|
- nonzero process exit;
|
||||||
|
- malformed, oversized, or unsupported receipt data;
|
||||||
|
- malformed or unsupported index data;
|
||||||
|
- unsafe receipt, descriptor, or filesystem paths;
|
||||||
|
- symlinks or special files in the promoted bundle;
|
||||||
|
- mismatched receipt pipeline identity;
|
||||||
|
- missing configured lanes;
|
||||||
|
- duplicate lane descriptors;
|
||||||
|
- rejected required lanes;
|
||||||
|
- incompatible media type, schema identity/version, or module key;
|
||||||
|
- empty or syntactically invalid required JSON payloads;
|
||||||
|
- checksum or promotion failure; or
|
||||||
|
- manifest persistence failure.
|
||||||
|
|
||||||
|
Process success alone does not establish consumer success. Notarius may exit
|
||||||
|
zero while omitting or rejecting a lane, and Narratio's configured required
|
||||||
|
set remains authoritative.
|
||||||
|
|
||||||
|
Failure retains bounded receipt and stderr diagnostics plus the run-local
|
||||||
|
bundle where available. Durable artifact storage and extraction source records
|
||||||
|
are updated only after complete validation and promotion. A promoted unique
|
||||||
|
bundle whose later manifest save fails is unreferenced and may be reclaimed by
|
||||||
|
explicit cleanup; it is never inferred as current.
|
||||||
|
|
||||||
|
## Security And Privacy
|
||||||
|
|
||||||
|
Transcripts, Notarius lanes, evidence context, manifests, receipts, warnings,
|
||||||
|
rejections, debug data, and logs are private campaign material.
|
||||||
|
|
||||||
|
- Secrets do not appear in command arguments, generated configuration,
|
||||||
|
artifact metadata, logs, examples, or documentation.
|
||||||
|
- Credentials continue to enter through Notarius and PromptKit's documented
|
||||||
|
environment or secret mechanisms.
|
||||||
|
- Receipt and index paths are untrusted external input until confined.
|
||||||
|
- Recursive promotion never follows symlinks or copies special files.
|
||||||
|
- Diagnostics remain bounded and do not echo payload bodies.
|
||||||
|
- Automatic cleanup follows Narratio's existing post-publish gates and path
|
||||||
|
safety rules; it does not silently remove immutable extraction bundles
|
||||||
|
outside an explicit covered policy.
|
||||||
|
|
||||||
|
## Maintained Complete D&D Example
|
||||||
|
|
||||||
|
Add a maintained example that enables Notarius's complete `dnd-session`
|
||||||
|
pipeline and maps these required lanes:
|
||||||
|
|
||||||
|
| Extraction key | Notarius lane ID |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `item_registry` | `item-registry` |
|
| `item_registry` | `item-registry` |
|
||||||
| `npc_registry` | `npc-registry` |
|
| `npc_registry` | `npc-registry` |
|
||||||
@@ -274,106 +505,100 @@ keys:
|
|||||||
| `location_occurrences` | `location-occurrences` |
|
| `location_occurrences` | `location-occurrences` |
|
||||||
| `enemy_events` | `enemy-events` |
|
| `enemy_events` | `enemy-events` |
|
||||||
|
|
||||||
The example must include each lane's current media type and schema identity
|
The example obtains exact media types and schema identities from Notarius's
|
||||||
from Notarius's published contracts. It should also demonstrate at least one
|
published contracts at implementation time. It demonstrates at least one
|
||||||
Scriptorium analysis artifact consuming one or more
|
Scriptorium artifact consuming a small, purpose-specific subset of extraction
|
||||||
`narratio.extraction.*` sources. The example must use placeholders and relative
|
sources. It uses only placeholders and repository-relative example paths,
|
||||||
paths suitable for the example tree, contain no credentials, and pass the
|
contains no credentials, and passes maintained example validation.
|
||||||
repository's configuration validation tests.
|
|
||||||
|
|
||||||
## Compatibility Policy
|
## Documentation Deliverables
|
||||||
|
|
||||||
The initial integration baseline is the public subprocess contract available
|
When implementation lands, update current-behavior documentation in the same
|
||||||
in Notarius v0.3.0:
|
change:
|
||||||
|
|
||||||
- successful JSON receipt schema `notarius.run-result.v1`;
|
- add `docs/integrations/notarius.md` for the consumed CLI, receipt, index, and
|
||||||
- production JSON bundle discovery through `index.json`; and
|
compatibility contract, linking to Notarius's canonical documentation;
|
||||||
- the schema IDs and versions explicitly configured for required lanes.
|
- add `docs/internal/stage-extract.md` for stage flow, collaborators, state,
|
||||||
|
failures, resume validation, and focused tests;
|
||||||
Runtime compatibility should be decided from those published contracts, not
|
- update `docs/internal/overview.md`, `docs/internal/adapters.md`,
|
||||||
from textual parsing of `notarius --version`. New optional receipt or index
|
`docs/internal/artifacts.md`, `docs/internal/manifest.md`, and
|
||||||
fields must not break Narratio. An unsupported receipt version or lane schema
|
`docs/internal/workspace.md` within their canonical scopes;
|
||||||
must fail before the artifact is registered for analysis.
|
- update `docs/policy/architecture.md` for the Notarius boundary and explicit
|
||||||
|
skipped/resume-validation contracts;
|
||||||
## Documentation Deliverables When Implemented
|
|
||||||
|
|
||||||
Update current-behavior documentation in the same change that implements the
|
|
||||||
feature:
|
|
||||||
|
|
||||||
- add `docs/integrations/notarius.md` for the external CLI, receipt, bundle,
|
|
||||||
and adapter contract, linking to Notarius's canonical documentation;
|
|
||||||
- add `docs/internal/stage-extract.md` for stage inputs, outputs, collaborators,
|
|
||||||
state transitions, failures, and focused tests;
|
|
||||||
- update `docs/internal/adapters.md`, `docs/internal/artifacts.md`,
|
|
||||||
`docs/internal/manifest.md`, and the internal stage inventory;
|
|
||||||
- update `docs/policy/architecture.md` to list Notarius among isolated external
|
|
||||||
systems and preserve the adapter/stage boundary;
|
|
||||||
- update `docs/config.md`, `docs/cli.md`, `docs/operations.md`,
|
- update `docs/config.md`, `docs/cli.md`, `docs/operations.md`,
|
||||||
`docs/troubleshooting.md`, `README.md`, and maintained examples only to the
|
`docs/troubleshooting.md`, `README.md`, and maintained examples only within
|
||||||
extent their canonical scopes require; and
|
their canonical scopes; and
|
||||||
- update `docs/development.md` only to the extent its canonical contributor
|
- update `docs/development.md` only if its contributor routing changes.
|
||||||
routing scope requires.
|
|
||||||
|
|
||||||
Outside this roadmap, do not describe `extract`, Notarius configuration, or
|
Outside this roadmap, do not describe the feature as implemented until its
|
||||||
`narratio.extraction.*` sources as implemented until the code exists.
|
code and documentation are complete.
|
||||||
|
|
||||||
## Testing And Validation Expectations
|
## Testing Expectations
|
||||||
|
|
||||||
Implementation should provide focused tests for:
|
Tests should protect contracts and meaningful risks rather than private helper
|
||||||
|
structure. The implementation plan assigns detailed ownership, with coverage
|
||||||
|
for:
|
||||||
|
|
||||||
- strict configuration decoding, defaults, required fields, path resolution,
|
- strict configuration, defaults, normalization, cross-source validation, and
|
||||||
output-map validation, normalized-key collisions, and example loading;
|
maintained examples;
|
||||||
- exact stage order, selection, downstream staleness, resume, force, and
|
- stage result dispositions and clearing of self-skipped outputs;
|
||||||
prerequisite behavior;
|
- backward-compatible artifact metadata serialization;
|
||||||
- adapter command construction, deterministic working directory, environment
|
- exact adapter arguments, streams, cancellation, timeout, exit behavior,
|
||||||
inheritance, stdout/stderr separation, cancellation, timeout, and nonzero
|
receipt/index compatibility, and every path-confinement boundary;
|
||||||
exits;
|
- recursive promotion safety, atomic visibility, cleanup on failure, symlink
|
||||||
- supported and unsupported receipt versions, unknown optional fields,
|
rejection, and immutable destination behavior;
|
||||||
malformed receipts, index decoding, and path escapes at every boundary;
|
- required-lane policy, descriptor compatibility, JSON syntax, checksums, and
|
||||||
- descriptor lookup by lane ID rather than filename, expected metadata checks,
|
provenance;
|
||||||
missing/rejected configured lanes, and tolerated unconfigured lanes;
|
- canonical order, single-stage selection, force, staleness, resume
|
||||||
- run-local execution, transactional canonical-bundle replacement, checksums,
|
validation, and enable-after-skip behavior;
|
||||||
failed-run preservation, and manifest recording;
|
- manifest-backed extraction catalog resolution for required, optional,
|
||||||
- artifact-catalog resolution from `narratio.extraction.*` into analysis and
|
missing, stale, skipped, incompatible, and tampered artifacts;
|
||||||
publish, including required, optional, missing, stale, and skipped cases; and
|
- analyze and publish integration without automatic lane injection; and
|
||||||
- end-to-end stage execution with a fake Notarius adapter, without live LLM or
|
- representative assembled execution with a fake Notarius runner and no live
|
||||||
external subprocess requirements in the ordinary test suite.
|
LLM, credentials, or external subprocess in the ordinary test suite.
|
||||||
|
|
||||||
Run the repository-wide Go tests, vet, build, and maintained example validation
|
Repository-wide tests, vet, build, and maintained-example validation are
|
||||||
after focused tests pass.
|
required after focused tests pass.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
- `extract` is a first-class transactional stage between `render` and
|
- `extract` is a first-class stage between `trim` and `render` everywhere
|
||||||
`analyze` everywhere Narratio models stage order or state.
|
Narratio models stage order and lifecycle.
|
||||||
- Narratio invokes Notarius only through a narrow, tested adapter.
|
- Narratio invokes Notarius only through a narrow tested adapter.
|
||||||
- The stage consumes the manifest-resolved final trimmed Seriatim transcript.
|
- Extraction consumes the manifest-resolved final trimmed Seriatim JSON.
|
||||||
- The Notarius configuration remains owned by Notarius; Narratio configures
|
- Disabled extraction is recorded as skipped and runs normally if later
|
||||||
only invocation and its downstream consumer contract.
|
enabled.
|
||||||
- Every configured output is discovered through the receipt and `index.json`,
|
- Successful resume requires valid manifest-recorded immutable outputs rather
|
||||||
contract-checked, materialized transactionally, and recorded with a stable
|
than filesystem presence alone.
|
||||||
`narratio.extraction.*` source ID.
|
- The complete bundle is promoted to a unique immutable directory without
|
||||||
- The complete D&D example maps all ten current lanes and passes strict config
|
following symlinks or exposing a partial destination.
|
||||||
validation.
|
- Every configured lane is discovered by lane ID, contract-checked, checksummed,
|
||||||
- Analysis can consume extraction sources through the existing artifact input
|
and recorded with explicit source identity and external provenance.
|
||||||
model, and publish can select them through the artifact catalog.
|
- Analysis and publish resolve extraction sources only from a current
|
||||||
- Failed, partial, rejected, unsafe, stale, or incompatible output never becomes
|
successful extract manifest record.
|
||||||
a current analysis input.
|
- Failed, skipped, stale, partial, rejected, unsafe, incompatible, or tampered
|
||||||
- Resume and force behavior remains manifest-driven.
|
output never becomes a current input.
|
||||||
- Documentation accurately describes the implemented stage, adapter,
|
- The complete D&D example maps all ten current lanes and demonstrates curated
|
||||||
configuration, operations, and compatibility boundary without duplicating
|
analysis inputs.
|
||||||
Notarius's canonical schemas.
|
- Tests remain deterministic, offline, and independent of real Notarius,
|
||||||
|
PromptKit, LLM providers, and credentials.
|
||||||
|
- Current-behavior documentation is updated only as implementation becomes
|
||||||
|
complete.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
- Reimplementing Notarius extraction, prompts, schemas, references, retries,
|
- Reimplementing Notarius extraction, configuration, schemas, prompts,
|
||||||
profiles, or lane orchestration in Narratio.
|
references, retries, profiles, validation, or lane orchestration.
|
||||||
- Allowing one Narratio run to invoke arbitrary extractor programs or multiple
|
- Supporting arbitrary extractor programs or multiple Notarius pipelines in
|
||||||
Notarius pipelines.
|
one Narratio run.
|
||||||
- Making `extract` a configurable DAG or folding it into the Scriptorium
|
- Turning canonical stage execution into a DAG or generic workflow engine.
|
||||||
`analyze` stage.
|
- Folding extraction into `analyze` or Scriptorium.
|
||||||
- Partially selecting Notarius lanes through Narratio's `--artifacts` flag.
|
- Automatically injecting all structured outputs into every prompt.
|
||||||
- Decoding D&D payload bodies in the generic Notarius adapter.
|
- Selecting Notarius lanes through Narratio's `--artifacts` flag.
|
||||||
- Supporting previous-session extraction artifacts in the initial feature.
|
- Decoding D&D payload bodies in the generic adapter or stage.
|
||||||
|
- Automatically detecting changes throughout Notarius's referenced config,
|
||||||
|
PromptKit profile, and campaign-reference closure.
|
||||||
|
- Supporting previous-session extraction sources in the initial feature.
|
||||||
|
- Automatically publishing the complete Notarius bundle.
|
||||||
- Requiring live Notarius, PromptKit, an LLM provider, or external services in
|
- Requiring live Notarius, PromptKit, an LLM provider, or external services in
|
||||||
the ordinary unit test suite.
|
the ordinary test suite.
|
||||||
|
|||||||
@@ -195,6 +195,9 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
|||||||
env.Logger.Debug("manifest saved", "stage", s.Name(), "transition", "running", "path", manifestPath)
|
env.Logger.Debug("manifest saved", "stage", s.Name(), "transition", "running", "path", manifestPath)
|
||||||
|
|
||||||
result, err := s.Run(ctx, stageEnv, m)
|
result, err := s.Run(ctx, stageEnv, m)
|
||||||
|
if err == nil {
|
||||||
|
err = validateStageResult(result)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failedAt := nowUTC()
|
failedAt := nowUTC()
|
||||||
m.MarkStageFailed(s.Name(), failedAt, err.Error())
|
m.MarkStageFailed(s.Name(), failedAt, err.Error())
|
||||||
@@ -209,6 +212,25 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage
|
|||||||
env.Logger.Info("stage failed", "stage", s.Name(), "error", err)
|
env.Logger.Info("stage failed", "stage", s.Name(), "error", err)
|
||||||
return nil, fmt.Errorf("stage %q failed: %w", s.Name(), err)
|
return nil, fmt.Errorf("stage %q failed: %w", s.Name(), err)
|
||||||
}
|
}
|
||||||
|
if result != nil && result.Disposition == stage.StageDispositionSkipped {
|
||||||
|
skipped = append(skipped, s.Name())
|
||||||
|
skippedAt := nowUTC()
|
||||||
|
m.MarkStageSkipped(s.Name(), skippedAt, result.SkipReason)
|
||||||
|
clearStageResultDetails(m.Stages[s.Name()])
|
||||||
|
applyStageResultToManifest(m, s.Name(), result)
|
||||||
|
if err := env.ManifestStore.Save(ctx, manifestPath, m); err != nil {
|
||||||
|
return nil, fmt.Errorf("save manifest after self-skip %q: %w", s.Name(), err)
|
||||||
|
}
|
||||||
|
runManifest.MarkStageSkipped(s.Name(), skippedAt, result.SkipReason)
|
||||||
|
applyStageResultToRunManifest(runManifest, s.Name(), result)
|
||||||
|
syncRunManifestIdentityFromSession(m, runManifest)
|
||||||
|
if err := runManifestStore.SaveRun(ctx, runManifestPath, runManifest); err != nil {
|
||||||
|
return nil, fmt.Errorf("save run manifest after self-skip %q: %w", s.Name(), err)
|
||||||
|
}
|
||||||
|
env.Logger.Debug("manifest saved", "stage", s.Name(), "transition", "skipped", "path", manifestPath)
|
||||||
|
env.Logger.Info("stage skipped", "stage", s.Name(), "reason", result.SkipReason)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
outputs := mapResultOutputs(s.Name(), result, runID)
|
outputs := mapResultOutputs(s.Name(), result, runID)
|
||||||
succeededAt := nowUTC()
|
succeededAt := nowUTC()
|
||||||
@@ -407,26 +429,69 @@ func mapResultOutputs(stageName string, result *stage.StageResult, runID string)
|
|||||||
localPath = ref.RelativePath
|
localPath = ref.RelativePath
|
||||||
}
|
}
|
||||||
kind := ref.Kind
|
kind := ref.Kind
|
||||||
sourceID := ""
|
sourceID := strings.TrimSpace(ref.SourceID)
|
||||||
if stageName == "analyze" {
|
if sourceID == "" {
|
||||||
sourceID = artifacts.ConfiguredArtifactSourceID(ref.Kind)
|
if stageName == "analyze" {
|
||||||
kind = "scriptorium_artifact"
|
sourceID = artifacts.ConfiguredArtifactSourceID(ref.Kind)
|
||||||
} else {
|
kind = "scriptorium_artifact"
|
||||||
sourceID = sourceIDForOutputKind(kind)
|
} else {
|
||||||
|
sourceID = sourceIDForOutputKind(kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out = append(out, manifest.ArtifactRecord{
|
out = append(out, manifest.ArtifactRecord{
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
SourceID: sourceID,
|
SourceID: sourceID,
|
||||||
LocalPath: localPath,
|
LocalPath: localPath,
|
||||||
ProducerRunID: runID,
|
Contract: cloneContractMetadata(ref.Contract),
|
||||||
RemoteKey: ref.RemoteKey,
|
ExternalProvenance: cloneExternalProvenance(ref.ExternalProvenance),
|
||||||
Checksum: ref.Checksum,
|
ProducerRunID: runID,
|
||||||
|
RemoteKey: ref.RemoteKey,
|
||||||
|
Checksum: ref.Checksum,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateStageResult(result *stage.StageResult) error {
|
||||||
|
if result == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch result.Disposition {
|
||||||
|
case stage.StageDispositionSucceeded:
|
||||||
|
if strings.TrimSpace(result.SkipReason) != "" {
|
||||||
|
return fmt.Errorf("successful result contains a skip reason")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case stage.StageDispositionSkipped:
|
||||||
|
if strings.TrimSpace(result.SkipReason) == "" {
|
||||||
|
return fmt.Errorf("skipped result requires a skip reason")
|
||||||
|
}
|
||||||
|
if len(result.Outputs) != 0 {
|
||||||
|
return fmt.Errorf("skipped result contains %d output(s)", len(result.Outputs))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported stage result disposition %q", result.Disposition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneContractMetadata(value *artifactmodel.ContractMetadata) *artifactmodel.ContractMetadata {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cloned := *value
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneExternalProvenance(value *artifactmodel.ExternalProvenance) *artifactmodel.ExternalProvenance {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cloned := *value
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
func sourceIDForOutputKind(kind string) string {
|
func sourceIDForOutputKind(kind string) string {
|
||||||
trimmed := strings.TrimSpace(kind)
|
trimmed := strings.TrimSpace(kind)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
@@ -462,6 +527,15 @@ func applyStageResultToManifest(m *manifest.Manifest, stageName string, result *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearStageResultDetails(sr *manifest.StageRecord) {
|
||||||
|
if sr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sr.Logs = nil
|
||||||
|
sr.GeneratedConfigs = nil
|
||||||
|
sr.Metadata = nil
|
||||||
|
}
|
||||||
|
|
||||||
func ensureManifestIdentity(cfg *config.Config, m *manifest.Manifest, runID string) (bool, error) {
|
func ensureManifestIdentity(cfg *config.Config, m *manifest.Manifest, runID string) (bool, error) {
|
||||||
if cfg == nil || cfg.Pipeline == nil || cfg.Session == nil || m == nil {
|
if cfg == nil || cfg.Pipeline == nil || cfg.Session == nil || m == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/seriatim"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/seriatim"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/whisperx"
|
"gitea.maximumdirect.net/eric/narratio/internal/adapters/whisperx"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
@@ -38,6 +39,25 @@ type countingStage struct {
|
|||||||
runs *int
|
runs *int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type resultStage struct {
|
||||||
|
name string
|
||||||
|
result *stage.StageResult
|
||||||
|
runs *int
|
||||||
|
order *[]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s resultStage) Name() string { return s.name }
|
||||||
|
func (s resultStage) Declares() stage.IODecl { return stage.IODecl{} }
|
||||||
|
func (s resultStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) {
|
||||||
|
if s.runs != nil {
|
||||||
|
*s.runs = *s.runs + 1
|
||||||
|
}
|
||||||
|
if s.order != nil {
|
||||||
|
*s.order = append(*s.order, s.name)
|
||||||
|
}
|
||||||
|
return s.result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s countingStage) Name() string { return s.name }
|
func (s countingStage) Name() string { return s.name }
|
||||||
func (s countingStage) Declares() stage.IODecl { return stage.IODecl{} }
|
func (s countingStage) Declares() stage.IODecl { return stage.IODecl{} }
|
||||||
func (s countingStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) {
|
func (s countingStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) {
|
||||||
@@ -199,6 +219,61 @@ func TestExecuteStagesAnalyzeOutputsPersistAsScriptoriumArtifacts(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapResultOutputsPrefersExplicitSourceAndCopiesMetadata(t *testing.T) {
|
||||||
|
contract := &artifactmodel.ContractMetadata{
|
||||||
|
MediaType: "application/json",
|
||||||
|
SchemaID: "notarius.dnd.npc_registry",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
}
|
||||||
|
provenance := &artifactmodel.ExternalProvenance{
|
||||||
|
System: "notarius",
|
||||||
|
RunID: "external-run",
|
||||||
|
PipelineID: "dnd-session",
|
||||||
|
ArtifactID: "npc-registry",
|
||||||
|
}
|
||||||
|
result := &stage.StageResult{Outputs: []artifacts.Ref{{
|
||||||
|
Kind: "structured_data",
|
||||||
|
SourceID: "narratio.example.npcs",
|
||||||
|
RelativePath: "artifacts/npcs.json",
|
||||||
|
Contract: contract,
|
||||||
|
ExternalProvenance: provenance,
|
||||||
|
}}}
|
||||||
|
|
||||||
|
got := mapResultOutputs("analyze", result, "narratio-run")
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("outputs len = %d, want 1", len(got))
|
||||||
|
}
|
||||||
|
if got[0].SourceID != "narratio.example.npcs" {
|
||||||
|
t.Fatalf("source_id = %q, want explicit source", got[0].SourceID)
|
||||||
|
}
|
||||||
|
if got[0].Kind != "structured_data" {
|
||||||
|
t.Fatalf("kind = %q, want explicit output kind preserved", got[0].Kind)
|
||||||
|
}
|
||||||
|
if got[0].Contract == nil || *got[0].Contract != *contract {
|
||||||
|
t.Fatalf("contract = %#v, want %#v", got[0].Contract, contract)
|
||||||
|
}
|
||||||
|
if got[0].ExternalProvenance == nil || *got[0].ExternalProvenance != *provenance {
|
||||||
|
t.Fatalf("external provenance = %#v, want %#v", got[0].ExternalProvenance, provenance)
|
||||||
|
}
|
||||||
|
if got[0].Contract == contract || got[0].ExternalProvenance == provenance {
|
||||||
|
t.Fatal("mapped metadata should not alias the stage result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapResultOutputsRetainsFallbackInference(t *testing.T) {
|
||||||
|
transcript := mapResultOutputs("trim", &stage.StageResult{Outputs: []artifacts.Ref{{
|
||||||
|
Kind: artifactmodel.TranscriptOutputKindFinalTrimmed,
|
||||||
|
}}}, "run-id")
|
||||||
|
if len(transcript) != 1 || transcript[0].SourceID != artifacts.ArtifactTranscriptFinalTrimmed {
|
||||||
|
t.Fatalf("transcript fallback = %#v, want final-trimmed source", transcript)
|
||||||
|
}
|
||||||
|
|
||||||
|
analyze := mapResultOutputs("analyze", &stage.StageResult{Outputs: []artifacts.Ref{{Kind: "session_recap"}}}, "run-id")
|
||||||
|
if len(analyze) != 1 || analyze[0].SourceID != "narratio.artifact.session_recap" || analyze[0].Kind != "scriptorium_artifact" {
|
||||||
|
t.Fatalf("analyze fallback = %#v, want configured artifact inference", analyze)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNeedsObjectStoreForRunPrepareWithPreviousRequirements(t *testing.T) {
|
func TestNeedsObjectStoreForRunPrepareWithPreviousRequirements(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -738,6 +813,126 @@ func TestExecuteStagesRunManifestRecordsSkippedStage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteStagesPersistsSelfSkipAndContinues(t *testing.T) {
|
||||||
|
cfg := testConfig(t)
|
||||||
|
store := &manifest.LocalStore{}
|
||||||
|
manifestPath := manifestPathFor(cfg)
|
||||||
|
seed := manifest.New(cfg.Session.SessionID, time.Date(2026, 5, 3, 1, 0, 0, 0, time.UTC))
|
||||||
|
seed.MarkStageSucceeded("optional", time.Date(2026, 5, 3, 1, 1, 0, 0, time.UTC), []manifest.ArtifactRecord{{
|
||||||
|
Kind: "old_output",
|
||||||
|
SourceID: "narratio.example.old",
|
||||||
|
LocalPath: "artifacts/old.json",
|
||||||
|
}})
|
||||||
|
seed.Stages["optional"].Logs = []string{"old.log"}
|
||||||
|
seed.Stages["optional"].GeneratedConfigs = []string{"old.yml"}
|
||||||
|
seed.Stages["optional"].Metadata = map[string]any{"old": true}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := store.Save(context.Background(), manifestPath, seed); err != nil {
|
||||||
|
t.Fatalf("Save() seed manifest error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order := []string{}
|
||||||
|
optionalRuns := 0
|
||||||
|
stages := []stage.Stage{
|
||||||
|
resultStage{
|
||||||
|
name: "optional",
|
||||||
|
runs: &optionalRuns,
|
||||||
|
order: &order,
|
||||||
|
result: &stage.StageResult{
|
||||||
|
Disposition: stage.StageDispositionSkipped,
|
||||||
|
SkipReason: "integration_disabled",
|
||||||
|
Logs: []string{"runs/current/optional.log"},
|
||||||
|
GeneratedConfigs: []string{"runs/current/optional.yml"},
|
||||||
|
Metadata: map[string]any{"enabled": false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resultStage{name: "later", order: &order, result: &stage.StageResult{}},
|
||||||
|
}
|
||||||
|
summary, err := executeStages(context.Background(), cfg, stages, RunOptions{Force: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("executeStages() error = %v", err)
|
||||||
|
}
|
||||||
|
if strings.Join(order, ",") != "optional,later" {
|
||||||
|
t.Fatalf("execution order = %v, want optional then later", order)
|
||||||
|
}
|
||||||
|
if len(summary.Executed) != 2 || len(summary.Skipped) != 1 || summary.Skipped[0] != "optional" {
|
||||||
|
t.Fatalf("summary = %#v, want optional executed and self-skipped before later", summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionManifest, err := store.Load(context.Background(), summary.ManifestPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() session manifest error = %v", err)
|
||||||
|
}
|
||||||
|
selfSkipped := sessionManifest.Stages["optional"]
|
||||||
|
if selfSkipped == nil || selfSkipped.Status != manifest.StatusSkipped {
|
||||||
|
t.Fatalf("optional stage = %#v, want skipped", selfSkipped)
|
||||||
|
}
|
||||||
|
if len(selfSkipped.Outputs) != 0 {
|
||||||
|
t.Fatalf("optional outputs = %#v, want old outputs cleared", selfSkipped.Outputs)
|
||||||
|
}
|
||||||
|
if selfSkipped.Error == nil || selfSkipped.Error.Message != "integration_disabled" {
|
||||||
|
t.Fatalf("optional skip reason = %#v, want integration_disabled", selfSkipped.Error)
|
||||||
|
}
|
||||||
|
if len(selfSkipped.Logs) != 1 || selfSkipped.Logs[0] != "runs/current/optional.log" ||
|
||||||
|
len(selfSkipped.GeneratedConfigs) != 1 || selfSkipped.GeneratedConfigs[0] != "runs/current/optional.yml" ||
|
||||||
|
selfSkipped.Metadata["enabled"] != false || selfSkipped.Metadata["old"] != nil {
|
||||||
|
t.Fatalf("optional result details = %#v, want current bounded diagnostics and metadata", selfSkipped)
|
||||||
|
}
|
||||||
|
if later := sessionManifest.Stages["later"]; later == nil || later.Status != manifest.StatusSucceeded {
|
||||||
|
t.Fatalf("later stage = %#v, want succeeded", later)
|
||||||
|
}
|
||||||
|
|
||||||
|
runManifest, err := store.LoadRun(context.Background(), summary.RunManifestPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadRun() error = %v", err)
|
||||||
|
}
|
||||||
|
runStage := runManifest.Stages["optional"]
|
||||||
|
if runStage == nil || runStage.Action != manifest.RunStageActionRun || runStage.Status != manifest.StatusSkipped {
|
||||||
|
t.Fatalf("run optional stage = %#v, want run action with skipped status", runStage)
|
||||||
|
}
|
||||||
|
if len(runStage.Logs) != 1 || runStage.Metadata["enabled"] != false {
|
||||||
|
t.Fatalf("run optional stage details = %#v, want result diagnostics and metadata", runStage)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = executeStages(context.Background(), cfg, []stage.Stage{stages[0]}, RunOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second executeStages() error = %v", err)
|
||||||
|
}
|
||||||
|
if optionalRuns != 2 {
|
||||||
|
t.Fatalf("optional runs = %d, want self-skipped stage reconsidered", optionalRuns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteStagesRejectsSkippedResultWithOutputs(t *testing.T) {
|
||||||
|
cfg := testConfig(t)
|
||||||
|
invalid := resultStage{name: "optional", result: &stage.StageResult{
|
||||||
|
Disposition: stage.StageDispositionSkipped,
|
||||||
|
SkipReason: "integration_disabled",
|
||||||
|
Outputs: []artifacts.Ref{{Kind: "unexpected"}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
summary, err := executeStages(context.Background(), cfg, []stage.Stage{invalid}, RunOptions{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("executeStages() error = nil, want invalid skipped result failure")
|
||||||
|
}
|
||||||
|
if summary != nil {
|
||||||
|
t.Fatalf("summary = %#v, want nil", summary)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "skipped result contains 1 output") {
|
||||||
|
t.Fatalf("error = %q, want skipped-output validation", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, loadErr := (&manifest.LocalStore{}).Load(context.Background(), manifestPathFor(cfg))
|
||||||
|
if loadErr != nil {
|
||||||
|
t.Fatalf("Load() session manifest error = %v", loadErr)
|
||||||
|
}
|
||||||
|
if got := loaded.Stages["optional"]; got == nil || got.Status != manifest.StatusFailed {
|
||||||
|
t.Fatalf("optional stage = %#v, want failed", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecuteStagesRunLocalArtifactsAndCanonicalSync(t *testing.T) {
|
func TestExecuteStagesRunLocalArtifactsAndCanonicalSync(t *testing.T) {
|
||||||
cfg := testConfig(t)
|
cfg := testConfig(t)
|
||||||
stages := []stage.Stage{
|
stages := []stage.Stage{
|
||||||
|
|||||||
17
internal/artifactmodel/metadata.go
Normal file
17
internal/artifactmodel/metadata.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package artifactmodel
|
||||||
|
|
||||||
|
// ContractMetadata identifies the data contract implemented by an artifact.
|
||||||
|
type ContractMetadata struct {
|
||||||
|
MediaType string `json:"media_type"`
|
||||||
|
SchemaID string `json:"schema_id"`
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
ModuleKey string `json:"module_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalProvenance identifies an artifact produced by an external system.
|
||||||
|
type ExternalProvenance struct {
|
||||||
|
System string `json:"system"`
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
PipelineID string `json:"pipeline_id"`
|
||||||
|
ArtifactID string `json:"artifact_id"`
|
||||||
|
}
|
||||||
56
internal/artifactmodel/metadata_test.go
Normal file
56
internal/artifactmodel/metadata_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package artifactmodel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArtifactMetadataJSON(t *testing.T) {
|
||||||
|
type envelope struct {
|
||||||
|
Contract *ContractMetadata `json:"contract,omitempty"`
|
||||||
|
ExternalProvenance *ExternalProvenance `json:"external_provenance,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
complete, err := json.Marshal(envelope{
|
||||||
|
Contract: &ContractMetadata{
|
||||||
|
MediaType: "application/json",
|
||||||
|
SchemaID: "notarius.dnd.npc_registry",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
ModuleKey: "dnd/npc-registry",
|
||||||
|
},
|
||||||
|
ExternalProvenance: &ExternalProvenance{
|
||||||
|
System: "notarius",
|
||||||
|
RunID: "run-123",
|
||||||
|
PipelineID: "dnd-session",
|
||||||
|
ArtifactID: "npc-registry",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal() complete metadata error = %v", err)
|
||||||
|
}
|
||||||
|
wantComplete := `{"contract":{"media_type":"application/json","schema_id":"notarius.dnd.npc_registry","schema_version":"v1","module_key":"dnd/npc-registry"},"external_provenance":{"system":"notarius","run_id":"run-123","pipeline_id":"dnd-session","artifact_id":"npc-registry"}}`
|
||||||
|
if string(complete) != wantComplete {
|
||||||
|
t.Fatalf("complete metadata JSON = %s, want %s", complete, wantComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
omitted, err := json.Marshal(envelope{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal() omitted metadata error = %v", err)
|
||||||
|
}
|
||||||
|
if string(omitted) != `{}` {
|
||||||
|
t.Fatalf("omitted metadata JSON = %s, want {}", omitted)
|
||||||
|
}
|
||||||
|
|
||||||
|
withoutModule, err := json.Marshal(envelope{Contract: &ContractMetadata{
|
||||||
|
MediaType: "application/json",
|
||||||
|
SchemaID: "notarius.dnd.npc_registry",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal() contract without module key error = %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(withoutModule), "module_key") {
|
||||||
|
t.Fatalf("contract JSON unexpectedly contains omitted module_key: %s", withoutModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
package artifacts
|
package artifacts
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
|
)
|
||||||
|
|
||||||
// Ref identifies a pipeline artifact and its local/remote coordinates.
|
// Ref identifies a pipeline artifact and its local/remote coordinates.
|
||||||
type Ref struct {
|
type Ref struct {
|
||||||
Kind string
|
Kind string
|
||||||
Category string
|
SourceID string
|
||||||
SessionID string
|
Category string
|
||||||
RelativePath string
|
SessionID string
|
||||||
AbsolutePath string
|
RelativePath string
|
||||||
RemoteKey string
|
AbsolutePath string
|
||||||
Checksum string
|
RemoteKey string
|
||||||
|
Checksum string
|
||||||
|
Contract *artifactmodel.ContractMetadata
|
||||||
|
ExternalProvenance *artifactmodel.ExternalProvenance
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store is the local artifact/workdir abstraction used by orchestration code.
|
// Store is the local artifact/workdir abstraction used by orchestration code.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package manifest
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrorRecord captures structured error metadata at run or stage scope.
|
// ErrorRecord captures structured error metadata at run or stage scope.
|
||||||
@@ -28,9 +30,11 @@ type InputRecord struct {
|
|||||||
|
|
||||||
// ArtifactRecord captures one produced artifact and optional remote metadata.
|
// ArtifactRecord captures one produced artifact and optional remote metadata.
|
||||||
type ArtifactRecord struct {
|
type ArtifactRecord struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
SourceID string `json:"source_id,omitempty"`
|
SourceID string `json:"source_id,omitempty"`
|
||||||
LocalPath string `json:"local_path"`
|
LocalPath string `json:"local_path"`
|
||||||
|
Contract *artifactmodel.ContractMetadata `json:"contract,omitempty"`
|
||||||
|
ExternalProvenance *artifactmodel.ExternalProvenance `json:"external_provenance,omitempty"`
|
||||||
// ProducerRunID identifies the run that produced this durable artifact.
|
// ProducerRunID identifies the run that produced this durable artifact.
|
||||||
ProducerRunID string `json:"producer_run_id,omitempty"`
|
ProducerRunID string `json:"producer_run_id,omitempty"`
|
||||||
RemoteKey string `json:"remote_key,omitempty"`
|
RemoteKey string `json:"remote_key,omitempty"`
|
||||||
@@ -120,6 +124,7 @@ func (m *Manifest) MarkStageSkipped(name string, at time.Time, reason string) {
|
|||||||
s.Status = StatusSkipped
|
s.Status = StatusSkipped
|
||||||
s.CompletedAt = timePtr(at)
|
s.CompletedAt = timePtr(at)
|
||||||
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
||||||
|
s.Outputs = nil
|
||||||
s.UpdatedAt = at
|
s.UpdatedAt = at
|
||||||
m.UpdatedAt = at
|
m.UpdatedAt = at
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,3 +110,23 @@ func TestMarkStageSucceededClearsError(t *testing.T) {
|
|||||||
t.Fatalf("error = %#v, want nil on success", stage.Error)
|
t.Fatalf("error = %#v, want nil on success", stage.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarkStageSkippedClearsEarlierOutputs(t *testing.T) {
|
||||||
|
m := New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
|
||||||
|
m.MarkStageSucceeded("extract", time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), []ArtifactRecord{
|
||||||
|
{Kind: "structured_data", SourceID: "narratio.example.characters", LocalPath: "artifacts/characters.json"},
|
||||||
|
})
|
||||||
|
|
||||||
|
m.MarkStageSkipped("extract", time.Date(2026, 5, 3, 10, 2, 0, 0, time.UTC), "integration_disabled")
|
||||||
|
|
||||||
|
stage := m.Stages["extract"]
|
||||||
|
if stage == nil {
|
||||||
|
t.Fatal("missing stage record")
|
||||||
|
}
|
||||||
|
if stage.Status != StatusSkipped {
|
||||||
|
t.Fatalf("status = %q, want %q", stage.Status, StatusSkipped)
|
||||||
|
}
|
||||||
|
if len(stage.Outputs) != 0 {
|
||||||
|
t.Fatalf("outputs = %#v, want cleared", stage.Outputs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func (m *RunManifest) MarkStageSkipped(name string, at time.Time, reason string)
|
|||||||
s.Status = StatusSkipped
|
s.Status = StatusSkipped
|
||||||
s.CompletedAt = timePtr(at)
|
s.CompletedAt = timePtr(at)
|
||||||
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
||||||
|
s.Outputs = nil
|
||||||
s.UpdatedAt = at
|
s.UpdatedAt = at
|
||||||
m.UpdatedAt = at
|
m.UpdatedAt = at
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
|
func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
|
||||||
@@ -64,6 +66,76 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLocalStoreArtifactMetadataCompatibility(t *testing.T) {
|
||||||
|
store := &LocalStore{}
|
||||||
|
ctx := context.Background()
|
||||||
|
dir := t.TempDir()
|
||||||
|
oldPath := filepath.Join(dir, "old-manifest.json")
|
||||||
|
oldJSON := `{
|
||||||
|
"session_id": "2026-05-03",
|
||||||
|
"created_at": "2026-05-03T10:00:00Z",
|
||||||
|
"updated_at": "2026-05-03T10:01:00Z",
|
||||||
|
"stages": {
|
||||||
|
"analyze": {
|
||||||
|
"name": "analyze",
|
||||||
|
"status": "succeeded",
|
||||||
|
"created_at": "2026-05-03T10:00:00Z",
|
||||||
|
"updated_at": "2026-05-03T10:01:00Z",
|
||||||
|
"outputs": [{"kind":"scriptorium_artifact","local_path":"artifacts/recap.md"}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
if err := os.WriteFile(oldPath, []byte(oldJSON), 0o644); err != nil {
|
||||||
|
t.Fatalf("WriteFile() old manifest error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := store.Load(ctx, oldPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() old manifest error = %v", err)
|
||||||
|
}
|
||||||
|
oldOutput := loaded.Stages["analyze"].Outputs[0]
|
||||||
|
if oldOutput.Contract != nil || oldOutput.ExternalProvenance != nil {
|
||||||
|
t.Fatalf("old output metadata = %#v, %#v; want nil", oldOutput.Contract, oldOutput.ExternalProvenance)
|
||||||
|
}
|
||||||
|
if err := store.Save(ctx, oldPath, loaded); err != nil {
|
||||||
|
t.Fatalf("Save() old manifest error = %v", err)
|
||||||
|
}
|
||||||
|
roundTripped, err := os.ReadFile(oldPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile() round-tripped old manifest error = %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(roundTripped), `"contract"`) || strings.Contains(string(roundTripped), `"external_provenance"`) {
|
||||||
|
t.Fatalf("old manifest gained fabricated metadata:\n%s", roundTripped)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded.Stages["analyze"].Outputs[0].Contract = &artifactmodel.ContractMetadata{
|
||||||
|
MediaType: "application/json",
|
||||||
|
SchemaID: "example.recap",
|
||||||
|
SchemaVersion: "v1",
|
||||||
|
}
|
||||||
|
loaded.Stages["analyze"].Outputs[0].ExternalProvenance = &artifactmodel.ExternalProvenance{
|
||||||
|
System: "example",
|
||||||
|
RunID: "external-run",
|
||||||
|
PipelineID: "pipeline",
|
||||||
|
ArtifactID: "recap",
|
||||||
|
}
|
||||||
|
metadataPath := filepath.Join(dir, "metadata-manifest.json")
|
||||||
|
if err := store.Save(ctx, metadataPath, loaded); err != nil {
|
||||||
|
t.Fatalf("Save() metadata manifest error = %v", err)
|
||||||
|
}
|
||||||
|
withMetadata, err := store.Load(ctx, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() metadata manifest error = %v", err)
|
||||||
|
}
|
||||||
|
got := withMetadata.Stages["analyze"].Outputs[0]
|
||||||
|
if got.Contract == nil || got.Contract.SchemaID != "example.recap" {
|
||||||
|
t.Fatalf("contract = %#v, want persisted contract", got.Contract)
|
||||||
|
}
|
||||||
|
if got.ExternalProvenance == nil || got.ExternalProvenance.RunID != "external-run" {
|
||||||
|
t.Fatalf("external provenance = %#v, want persisted provenance", got.ExternalProvenance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocalStoreSaveUpdatesTimestamp(t *testing.T) {
|
func TestLocalStoreSaveUpdatesTimestamp(t *testing.T) {
|
||||||
store := &LocalStore{}
|
store := &LocalStore{}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -44,8 +44,19 @@ type Stage interface {
|
|||||||
Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error)
|
Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StageDisposition describes the outcome of a stage that returned without an error.
|
||||||
|
type StageDisposition string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StageDispositionSucceeded is the zero value so existing stages remain successful.
|
||||||
|
StageDispositionSucceeded StageDisposition = ""
|
||||||
|
StageDispositionSkipped StageDisposition = "skipped"
|
||||||
|
)
|
||||||
|
|
||||||
// StageResult is the declared output of a stage execution.
|
// StageResult is the declared output of a stage execution.
|
||||||
type StageResult struct {
|
type StageResult struct {
|
||||||
|
Disposition StageDisposition
|
||||||
|
SkipReason string
|
||||||
Outputs []artifacts.Ref
|
Outputs []artifacts.Ref
|
||||||
Logs []string
|
Logs []string
|
||||||
GeneratedConfigs []string
|
GeneratedConfigs []string
|
||||||
|
|||||||
Reference in New Issue
Block a user