Add documentation and roadmap for a significant refactor around domain-focused module packages
This commit is contained in:
51
docs/adr/0002-linear-pipes-and-filters-pipeline.md
Normal file
51
docs/adr/0002-linear-pipes-and-filters-pipeline.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# ADR-0002: Linear pipes-and-filters pipeline, not a general DAG
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-07-13
|
||||
|
||||
## Context
|
||||
|
||||
Notarius processes source material through one known workflow:
|
||||
|
||||
```text
|
||||
input -> chunk -> extract -> merge -> normalize -> output
|
||||
```
|
||||
|
||||
Input and chunking apply to the source as a whole. Each selected artifact lane
|
||||
then performs extract, merge, and normalize, after which output aggregates the
|
||||
lane outcomes. Chunk extraction has a natural scatter-gather shape, but no
|
||||
current use case requires arbitrary branches, joins, or user-defined stage
|
||||
topology.
|
||||
|
||||
## Decision
|
||||
|
||||
Notarius implements a fixed six-stage pipes-and-filters pipeline. Configuration
|
||||
selects implementations for these stages but cannot add stages, reorder them,
|
||||
or define an arbitrary graph.
|
||||
|
||||
The framework owns stage sequencing and the scatter-gather boundary between
|
||||
chunk, extract, and merge. Extract results are handed to merge in deterministic
|
||||
source-chunk order regardless of execution strategy. Each artifact lane remains
|
||||
logically linear. Output runs after every selected lane has either produced an
|
||||
accepted normalized artifact or reached a recorded rejection. A framework
|
||||
execution failure aborts the pipeline.
|
||||
|
||||
The runner's concrete internal representation and stage-specific scheduling
|
||||
policies are implementation details. Concurrency must preserve the pipeline's
|
||||
deterministic handoffs, validation behavior, and provenance, and all execution
|
||||
strategies must continue to honor context cancellation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Build a general DAG engine now. This would support hypothetical branching
|
||||
topologies, but would add scheduling, topology validation, configuration, and
|
||||
state-management complexity without a current consumer. Revisit this choice
|
||||
only when a concrete workflow requires a topology the fixed pipeline cannot
|
||||
express.
|
||||
|
||||
## Consequences
|
||||
|
||||
The runner, configuration model, and operator mental model remain small. Stage
|
||||
ownership stays visible, and general chunking, merging, or normalization cannot
|
||||
be hidden inside extractors. A future DAG requirement will require an explicit
|
||||
architectural change rather than incremental exceptions to the fixed pipeline.
|
||||
119
docs/adr/0003-typed-interfaces-with-two-zone-data-model.md
Normal file
119
docs/adr/0003-typed-interfaces-with-two-zone-data-model.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# ADR-0003: Strongly typed stage interfaces with a two-zone data model
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-07-13
|
||||
|
||||
## Context
|
||||
|
||||
Pipeline stages must exchange source data and extracted artifacts. Universal
|
||||
source data has one engine-wide meaning, while extracted artifacts have
|
||||
domain-specific shapes. Passing opaque bytes or `any` between all stages would
|
||||
make invalid wiring and merge behavior runtime concerns. Requiring JSON at
|
||||
every handoff would preserve interoperability but discard useful Go type safety
|
||||
while all modules are in-process.
|
||||
|
||||
The framework must also support multiple configured artifact domains, durable
|
||||
checkpoints, diagnostics, and output encoders without making those consumers
|
||||
depend on every domain's Go types.
|
||||
|
||||
## Decision
|
||||
|
||||
Notarius uses two typed data zones followed by one serialized boundary.
|
||||
|
||||
### Source zone
|
||||
|
||||
Input and chunk stages use conservative, engine-owned document, segment, chunk,
|
||||
and source-reference types. Their exact Go names are implementation details.
|
||||
Every segment carries engine-owned source provenance identifying the source
|
||||
location from which it was produced. Chunks preserve the ordered provenance of
|
||||
their segments.
|
||||
|
||||
Source-format-specific fields remain in input modules or explicitly namespaced
|
||||
metadata; they do not become framework contracts.
|
||||
|
||||
### Domain artifact zone
|
||||
|
||||
Each artifact lane has one domain-owned Go artifact type `T`. Its extract,
|
||||
merge, normalize, and domain-aware validation implementations use generic,
|
||||
strongly typed contracts over the same `T`. Raw JSON, opaque bytes, and `any`
|
||||
are not stage-handoff contracts within a lane.
|
||||
|
||||
Each registered domain artifact type supplies a codec for `T`. The codec owns:
|
||||
|
||||
- stable schema identity and an explicit schema version;
|
||||
- JSON serialization and deserialization;
|
||||
- the media type and schema metadata required at serialized boundaries; and
|
||||
- rejection of data that cannot be represented by the declared artifact
|
||||
schema.
|
||||
|
||||
An artifact type's JSON representation is a maintained domain contract.
|
||||
Changing it incompatibly requires a new schema version.
|
||||
|
||||
Extract, merge, and normalize may change the contents of `T`, but they do not
|
||||
change the lane's canonical Go artifact type or artifact schema identity. An
|
||||
extractor maps any provider- or prompt-specific response type into `T` before
|
||||
returning. A future lane that requires different artifact types at different
|
||||
stages requires a new architectural decision.
|
||||
|
||||
### Serialized boundary
|
||||
|
||||
After normalization, each typed artifact is converted into an engine-owned
|
||||
serialized artifact containing bytes, media type, and schema metadata. Output
|
||||
aggregation and output encoders consume this type-erased form. Intermediate
|
||||
checkpoint and debug encodings do not become stage-handoff contracts.
|
||||
|
||||
LLM transport, checkpoints, and opt-in debug recording are also explicit
|
||||
serialization boundaries. They may encode or decode a typed artifact through
|
||||
its domain codec, but they do not change the in-memory type used between
|
||||
extract, merge, normalize, and typed validators. Checkpoint reuse requires a
|
||||
compatible schema identity and version.
|
||||
|
||||
An LLM structured-response schema is a module transport contract and may differ
|
||||
from the domain artifact schema. The calling module owns the response type and
|
||||
maps it into the canonical `T`; the artifact codec remains authoritative for
|
||||
artifact checkpoints and output serialization.
|
||||
|
||||
The framework may use private type-erased adapters to store heterogeneous lane
|
||||
registrations and execute configured domains. Such an adapter must assemble a
|
||||
type-consistent lane before execution and must not expose `any` or raw payloads
|
||||
as module-facing handoffs inside the domain artifact zone.
|
||||
|
||||
### Construction and dependencies
|
||||
|
||||
Every module operation accepts `context.Context`. Modules receive stable runtime
|
||||
collaborators through an injected dependency set at construction time. In
|
||||
particular, LLM-using modules receive the application-provided structured LLM
|
||||
client and do not construct provider clients or bypass shared scheduling.
|
||||
|
||||
The application boundary enforces one configurable global upper bound on
|
||||
in-flight LLM calls across all stages, lanes, retries, and validators.
|
||||
|
||||
Configuration options are parsed and validated while a module is constructed,
|
||||
before that module executes. Per-run data such as source material, references,
|
||||
session identity, and lane identity remains operation input rather than a
|
||||
construction dependency.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Pass raw bytes between stages. This maximizes decoupling but moves wiring,
|
||||
parsing, and merge errors to runtime and prevents domain types from being the
|
||||
canonical in-process contract.
|
||||
- Require JSON plus schemas at every stage boundary. This is appropriate for an
|
||||
out-of-process boundary, but adds serialization and parsing inside the current
|
||||
in-process pipeline. The stable codec contract preserves this upgrade path if
|
||||
remote plugins are introduced.
|
||||
- Use a uniform `Process(any) (any, error)` contract. This simplifies a fully
|
||||
dynamic engine but turns incompatible module composition into type assertions
|
||||
and runtime failures. The fixed topology does not require that tradeoff.
|
||||
|
||||
## Consequences
|
||||
|
||||
Domain pipelines gain compile-time handoff safety and explicit merge semantics.
|
||||
Serialization, schema compatibility, checkpoint decoding, and output erasure
|
||||
have named owners. Dynamic registration requires a small erased adapter around
|
||||
each typed lane, and generic stage implementations must be instantiated for a
|
||||
specific artifact type or behavior rather than manipulating arbitrary JSON.
|
||||
|
||||
The engine-owned source model becomes a long-lived contract and must evolve
|
||||
conservatively. Domain authors must maintain a codec and versioned schema in
|
||||
addition to their Go artifact type.
|
||||
80
docs/adr/0004-package-modules-by-domain.md
Normal file
80
docs/adr/0004-package-modules-by-domain.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# ADR-0004: Package modules by domain, not by stage
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-07-13
|
||||
|
||||
## Context
|
||||
|
||||
Module packages can be grouped first by pipeline stage, such as
|
||||
`modules/chunk/dnd/scenes`, or first by domain, such as
|
||||
`modules/dnd/chunk/scenes`. A domain's extract, merge, normalize, validation,
|
||||
schema, prompt, and artifact-codec implementations collaborate around the same
|
||||
artifact types and are likely to evolve together.
|
||||
|
||||
Go package dependencies also constrain registration. If shared types live in a
|
||||
domain root package, that package cannot import child implementation packages
|
||||
to register them because the children already import the root types.
|
||||
|
||||
## Decision
|
||||
|
||||
Production extensions are grouped by domain under:
|
||||
|
||||
```text
|
||||
internal/modules/<domain>/<stage>/<name>
|
||||
```
|
||||
|
||||
Shared artifact types live at the domain root, for example
|
||||
`internal/modules/dnd/types.go`. Domain-specific validators, prompt fragments,
|
||||
schemas, reference helpers, and codecs also live within that domain tree.
|
||||
|
||||
Each domain exposes one production registration entry point from a sibling
|
||||
registrar package, for example `internal/modules/dnd/register`. The registrar
|
||||
may import the domain root and its child implementations; the domain root does
|
||||
not import its registrar or child packages. This keeps shared types available
|
||||
as `dnd.SpellList` without creating a Go import cycle.
|
||||
|
||||
The `generic` tree is a peer extension family for reusable implementations that
|
||||
contain no concrete source-format or artifact-domain knowledge. Source-format
|
||||
and output-format families, such as Seriatim and JSON output, follow the same
|
||||
domain-first organization even when they do not define a Zone-B artifact type.
|
||||
|
||||
Concrete domain implementation packages do not import another concrete domain.
|
||||
Generic extension packages never import concrete domains. A domain registrar
|
||||
may import domain-neutral generic extension packages to instantiate a reusable
|
||||
strategy for that domain's artifact type; the generic implementation remains
|
||||
unaware of the concrete type's domain semantics. Reuse needed directly by a
|
||||
domain implementation lives in a domain-neutral framework or helper package,
|
||||
not in a peer extension package.
|
||||
|
||||
The application composition root may import multiple registrar packages, and
|
||||
black-box integration tests may compose multiple domains. Other cross-domain
|
||||
reuse occurs through engine contracts and composition-time registration rather
|
||||
than concrete peer-domain imports.
|
||||
|
||||
A domain registrar owns registration of that domain's modules, validators,
|
||||
default validator chains, artifact codecs, schemas, and prompt assets. It does
|
||||
not take ownership of application execution or process behavior.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- Group modules by stage. This keeps interchangeable strategies side by side,
|
||||
but scatters a domain's shared artifact model and collaborating extensions
|
||||
across the repository. It is preferable when generic strategy libraries
|
||||
dominate or when the project is primarily a stage-extension framework rather
|
||||
than an application composed from domain suites.
|
||||
- Put both shared types and `Register` in the domain root. This gives the
|
||||
shortest import path but creates an import cycle once child implementations
|
||||
import the root artifact types.
|
||||
|
||||
## Consequences
|
||||
|
||||
The repository layout makes supported domains immediately visible, and adding
|
||||
or extracting a domain affects one cohesive subtree. The CLI composition root
|
||||
depends on a small set of domain registrars instead of every leaf package.
|
||||
|
||||
Package moves must preserve user-visible module and validator keys unless a
|
||||
separate compatibility decision changes them. Shared behavior that cannot be
|
||||
expressed through framework contracts may need to move into a domain-neutral
|
||||
framework package rather than creating a concrete peer-domain import. Registrar
|
||||
packages become explicit composition points for instantiating generic typed
|
||||
strategies, in addition to registering domain-owned implementations.
|
||||
348
docs/roadmap/domain.md
Normal file
348
docs/roadmap/domain.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Domain-Typed Pipeline Feature Roadmap
|
||||
|
||||
## Status
|
||||
|
||||
Decision-complete; implementation pending. This roadmap defines the desired end
|
||||
state for [ADR-0002](../adr/0002-linear-pipes-and-filters-pipeline.md),
|
||||
[ADR-0003](../adr/0003-typed-interfaces-with-two-zone-data-model.md), and
|
||||
[ADR-0004](../adr/0004-package-modules-by-domain.md). The work needed to reach
|
||||
that state is owned by the
|
||||
[domain pipeline implementation plan](implementation.md).
|
||||
|
||||
Until that plan is complete, current behavior remains defined by the
|
||||
architecture, configuration, integration, operations, and internal
|
||||
documentation outside `docs/roadmap/`.
|
||||
|
||||
## User Intent
|
||||
|
||||
Notarius should remain a small, explicit pipes-and-filters application while
|
||||
making domain extensions safe to compose and straightforward to maintain. A
|
||||
configured pipeline should fail before execution when its modules are
|
||||
incompatible, should carry typed domain values rather than reparsed JSON between
|
||||
artifact stages, and should preserve provenance and durable output contracts.
|
||||
|
||||
The application must also enforce one configurable, process-wide ceiling on
|
||||
in-flight LLM calls. Pipeline scheduling may impose stricter limits, but no
|
||||
stage, lane, retry, validator, or future LLM-backed extension may bypass that
|
||||
global ceiling.
|
||||
|
||||
## Target Architecture
|
||||
|
||||
### Pipeline and outcome model
|
||||
|
||||
The topology remains:
|
||||
|
||||
```text
|
||||
input -> chunk -> extract -> merge -> normalize -> output
|
||||
```
|
||||
|
||||
Input and chunk are pipeline-wide. Extract, merge, normalize, and their
|
||||
validators operate per artifact lane. Output aggregates the terminal artifacts
|
||||
from all lanes. The resolved pipeline retains explicit fields for those roles;
|
||||
it does not become a general DAG or a heterogeneous ordered-stage list.
|
||||
|
||||
Framework errors abort the run. Validator rejection is a recorded domain
|
||||
outcome and does not abort unrelated work. Accepted extract results reach merge
|
||||
in source-chunk order, regardless of execution completion order. Warnings,
|
||||
rejections, artifacts, and reported errors are likewise ordered by stable
|
||||
pipeline scope rather than goroutine completion time.
|
||||
|
||||
### Engine-owned source model
|
||||
|
||||
The engine owns `source.SourceDocument`, `source.SourceUnit`, `source.SourceRef`,
|
||||
and `source.Chunk`. Domain modules may consume these types but must not redefine
|
||||
their provenance semantics.
|
||||
|
||||
- Every source unit has a canonical self-reference identifying its source and
|
||||
unit range.
|
||||
- A chunk contains ordered source units and one canonical reference spanning
|
||||
its first through last unit.
|
||||
- Chunk and unit references are validated for source identity, order, and
|
||||
containment.
|
||||
- Auxiliary reference material remains distinct from source provenance.
|
||||
- Cloning, canonicalization, checkpointing, debugging, and digest computation
|
||||
preserve the source references exactly.
|
||||
|
||||
`source.Chunk.Ref` replaces duplicate start/end boundary fields. Because that
|
||||
changes persisted workspace state, the workspace checkpoint schema advances to
|
||||
`notarius.workspace.v2`. Existing v1 checkpoints are left intact but treated as
|
||||
incompatible and recomputed; no in-place migration or deletion is required.
|
||||
|
||||
### Typed artifact lanes
|
||||
|
||||
Each lane has one canonical artifact type `T` from extraction through merge,
|
||||
normalization, and typed validation. Module-facing Zone-B contracts are generic:
|
||||
|
||||
- `Extractor[T]` produces typed per-chunk values plus framework-owned
|
||||
provenance and diagnostics;
|
||||
- `Merger[T]` combines accepted values in source-chunk order;
|
||||
- `Normalizer[T]` canonicalizes the merged value; and
|
||||
- `TypedValidator[T]` applies semantic checks at its configured artifact stage.
|
||||
|
||||
The framework may use private erased adapters to keep heterogeneous lanes in
|
||||
one resolved pipeline, but `any`, raw JSON, and a generic `Process(any)` API are
|
||||
not module-facing handoffs. The extractor selected for a lane establishes its
|
||||
artifact kind. Resolution uses that kind to select compatible merger,
|
||||
normalizer, validator, and codec variants and rejects an incompatible lane
|
||||
before any stage executes.
|
||||
|
||||
Generic strategies remain reusable without knowing concrete domains. In
|
||||
particular, append-order merge is parameterized by a typed combine function
|
||||
provided during domain registration, and no-op normalization is instantiated
|
||||
for the lane's concrete type.
|
||||
|
||||
### Artifact identity and codecs
|
||||
|
||||
Every typed artifact kind has exactly one registered `ArtifactCodec[T]`. An
|
||||
artifact kind is a stable logical identifier, separate from a Go type name. A
|
||||
codec owns:
|
||||
|
||||
- artifact kind;
|
||||
- schema identifier, name, and version;
|
||||
- media type and JSON Schema bytes; and
|
||||
- strict, deterministic encoding and decoding between `T` and the serialized
|
||||
representation.
|
||||
|
||||
Equal canonical values must encode to equal bytes. Those bytes are the basis
|
||||
for artifact digests. Codec decoding rejects malformed or schema-incompatible
|
||||
content. Domain validators continue to own semantic validity; codecs do not
|
||||
replace them.
|
||||
|
||||
`SerializedArtifact` is the Zone-C representation and includes the artifact
|
||||
kind, schema metadata, media type, encoded content, and framework metadata.
|
||||
Type erasure occurs through the codec after normalization for final output.
|
||||
Intermediate checkpointing and opt-in debug recording may also use the codec,
|
||||
but serialization for those side effects is not a stage handoff.
|
||||
|
||||
Checkpoint metadata records artifact kind, schema identifier, schema version,
|
||||
and schema digest. Reuse requires an exact compatible registered codec;
|
||||
otherwise the checkpoint is safely invalidated. Output remains domain-neutral
|
||||
and consumes serialized artifacts.
|
||||
|
||||
Generic serialized validators remain supported for representation-level checks
|
||||
such as valid JSON and JSON Schema validation. The framework encodes `T` through
|
||||
its registered codec before invoking them. Domain validators receive `T`
|
||||
directly. Chunk-stage validators remain in the source zone: semantic chunk
|
||||
validators receive engine-owned chunks, while representation-level validators
|
||||
receive the framework's canonical serialized chunk view. They do not force
|
||||
source-zone values through a domain artifact codec.
|
||||
|
||||
### Registration and resolution
|
||||
|
||||
Registries expose typed registration helpers while privately retaining the Go
|
||||
type identity needed to assemble erased lane executors.
|
||||
|
||||
- Codecs are keyed by artifact kind, with exactly one codec per kind.
|
||||
- Extractors are keyed by their existing module key and declare an artifact
|
||||
kind.
|
||||
- Mergers, normalizers, and validators are keyed by `(module key, artifact
|
||||
kind)`, allowing stable generic keys such as `appendorder` and `noop` to have
|
||||
multiple typed specializations.
|
||||
- Resolved pipeline identity and dependency fingerprints include artifact kind
|
||||
and schema identity, version, and digest.
|
||||
- Duplicate or incompatible registrations and selections fail deterministically
|
||||
during composition or resolution.
|
||||
|
||||
The framework's public typed registration surface uses free generic functions,
|
||||
because Go methods cannot declare their own type parameters. Private reflection
|
||||
may verify and erase registered types, but it is not exposed to module authors.
|
||||
|
||||
### Preparation, options, and dependencies
|
||||
|
||||
Pipeline execution is split into resolution, preparation, and running.
|
||||
Preparation constructs every selected module and validator before source input
|
||||
begins and returns a prepared pipeline with explicit input, chunk, lane, and
|
||||
output fields.
|
||||
|
||||
- Construction receives framework-owned dependencies, including the one shared
|
||||
scheduled structured-LLM client.
|
||||
- Raw configured options are decoded once into implementation-owned option
|
||||
structs during preparation.
|
||||
- Missing dependencies, malformed options, unknown options, and incompatible
|
||||
typed selections fail before stage execution.
|
||||
- Configuration validation uses the same option decoders without requiring live
|
||||
provider dependencies.
|
||||
- Per-run data such as sources, chunks, references, session identity, lane
|
||||
identity, and metadata remains in operation requests.
|
||||
- Constructed implementations that can be scheduled concurrently are immutable
|
||||
after preparation or otherwise explicitly concurrency-safe.
|
||||
|
||||
Modules and validators must not construct provider clients, wrap their own
|
||||
independent schedulers, or bypass the injected scheduled client.
|
||||
|
||||
### D&D artifact model
|
||||
|
||||
The D&D package root owns canonical `dnd.SpellList`, `dnd.SpellCast`, and
|
||||
related evidence types, using engine-owned `source.SourceRef` values. The spell
|
||||
extractor keeps its LLM response DTO and response schema private and maps the
|
||||
canonicalized response to the domain model.
|
||||
|
||||
The D&D spell codec separately owns the existing durable spell artifact schema.
|
||||
The LLM response schema and durable artifact schema remain distinct contracts
|
||||
even if their current JSON shapes are similar. Shape, source-reference, and
|
||||
source-relatedness validators operate on the canonical typed model. The
|
||||
validator-only duplicate spell model and inter-stage JSON reparsing disappear.
|
||||
|
||||
The migration preserves the existing D&D spell payload, logical output bundle,
|
||||
module and validator keys, prompt/schema identities, default validator chains,
|
||||
warnings, rejection semantics, and manifest provenance unless a separate
|
||||
compatibility decision explicitly changes one of those contracts.
|
||||
|
||||
## Package Ownership
|
||||
|
||||
The target production extension layout is:
|
||||
|
||||
```text
|
||||
internal/modules/dnd/
|
||||
types.go
|
||||
codec/spells/
|
||||
chunk/scenes/
|
||||
extract/spells/
|
||||
validate/spells/shape/
|
||||
validate/spells/source_refs/
|
||||
validate/spells/source_relatedness/
|
||||
shared/
|
||||
register/
|
||||
|
||||
internal/modules/generic/
|
||||
chunk/units/
|
||||
merge/appendorder/
|
||||
normalize/noop/
|
||||
validate/always_accept/
|
||||
validate/always_reject/
|
||||
validate/valid_json/
|
||||
validate/valid_json_schema/
|
||||
output/json/
|
||||
register/
|
||||
|
||||
internal/modules/seriatim/
|
||||
input/transcript/
|
||||
register/
|
||||
|
||||
internal/framework/promptfs/
|
||||
```
|
||||
|
||||
Shared domain types live at the domain root. Registration lives in a sibling
|
||||
`register` package so that the root never imports child implementations. Each
|
||||
registrar exposes one composition entry point accepting the pipeline registry
|
||||
set and LLM asset registry. The CLI composition root creates those registries
|
||||
and invokes the generic, Seriatim, and D&D registrars.
|
||||
|
||||
Concrete domain implementations do not import peer domains. Generic extensions
|
||||
never import a concrete domain. A domain registrar may import generic packages
|
||||
to register typed specializations for its domain. The application composition
|
||||
root and designated black-box integration tests may compose multiple
|
||||
registrars. Domain-neutral embedded prompt-asset filesystem support belongs to
|
||||
the framework rather than a domain package.
|
||||
|
||||
## Concurrency Policy
|
||||
|
||||
### Configuration
|
||||
|
||||
The existing `concurrency.total_llm` setting remains the application-wide
|
||||
ceiling on actual provider calls. Version-2 configuration gains an extensible
|
||||
stage-worker map:
|
||||
|
||||
```yaml
|
||||
concurrency:
|
||||
total_llm: 4
|
||||
stage_workers:
|
||||
extract: 4
|
||||
```
|
||||
|
||||
Initially, `extract` is the only recognized key. Unknown stage keys are rejected
|
||||
so misspellings cannot silently alter scheduling. If omitted, the effective
|
||||
extract worker count equals `total_llm`. Its valid range is
|
||||
`1..concurrency.total_llm`. The environment override is
|
||||
`NOTARIUS_STAGE_WORKERS_EXTRACT`; future stage overrides receive similarly
|
||||
explicit names that map to the extensible file representation.
|
||||
|
||||
The worker setting bounds framework jobs, not provider calls. Only an actual
|
||||
LLM call consumes a permit from the shared scheduled client. The global
|
||||
scheduled client remains authoritative even if future stages gain worker limits.
|
||||
|
||||
### Scheduling
|
||||
|
||||
After pipeline-wide chunking, all lanes may run concurrently. A central
|
||||
dispatcher submits `(lane, chunk)` jobs to one run-wide extract worker pool in
|
||||
round-robin order: source chunk first, then resolved lane order. This avoids one
|
||||
unbounded goroutine per job and prevents an early lane from monopolizing the
|
||||
queue.
|
||||
|
||||
One job contains extraction, its stage-local retry behavior, and extract-stage
|
||||
validation for that lane and chunk. Each lane begins its serial merge then
|
||||
normalize continuation when all of its extract jobs reach a terminal state.
|
||||
Different lanes' continuations may overlap, and any LLM-backed continuation or
|
||||
validator still shares the global scheduled client.
|
||||
|
||||
Workers publish immutable task results to a coordinator. Only the coordinator
|
||||
mutates aggregate results, manifests, checkpoint indexes, warnings, and
|
||||
rejections. Debug artifacts use attempt-specific paths and do not rely on
|
||||
concurrent writes to shared files.
|
||||
|
||||
Rejections do not cancel work. A framework error cancels the derived run
|
||||
context, stops undispatched jobs, and waits for started jobs to finish or
|
||||
observe cancellation. If the parent context was canceled, its error is
|
||||
returned. Otherwise, internal cancellation errors are ignored when at least one
|
||||
real framework error exists, and the primary returned error is selected from
|
||||
all started-task framework errors by this stable ordering:
|
||||
|
||||
1. stage order: extract, merge, then normalize;
|
||||
2. resolved lane order;
|
||||
3. source chunk index for chunk-scoped work; and
|
||||
4. configured validator or operation order within that scope.
|
||||
|
||||
The full per-task errors may be retained in opt-in diagnostics, but completion
|
||||
timing never chooses the public error. Output runs only after every lane reaches
|
||||
a successful or rejection-only terminal state and no framework error exists.
|
||||
|
||||
Extractors and any validator instance callable by multiple workers must be safe
|
||||
for concurrent use. Production implementations should normally satisfy this by
|
||||
being immutable after preparation.
|
||||
|
||||
## Compatibility and Safety
|
||||
|
||||
- Existing production module keys, validator keys, profiles, default chains,
|
||||
and maintained configurations continue to resolve.
|
||||
- Existing durable D&D JSON content and logical output paths remain unchanged.
|
||||
- Framework errors, validator rejections, retries, checkpoints, diagnostics,
|
||||
and debug behavior retain their current semantics except for the explicitly
|
||||
documented workspace-v2 compatibility boundary and deterministic concurrent
|
||||
ordering.
|
||||
- All provider calls pass through the shared global scheduler, across lanes,
|
||||
stages, retries, and validators.
|
||||
- Source text and LLM payloads remain subject to the existing opt-in debug and
|
||||
sensitive-data handling policies.
|
||||
- Package moves do not create user-visible key changes or concrete cross-domain
|
||||
dependencies.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This feature does not introduce:
|
||||
|
||||
- a general DAG or configurable stage topology;
|
||||
- out-of-process plugins or an RPC extension protocol;
|
||||
- cross-lane normalization;
|
||||
- a new durable D&D spell schema merely to mirror internal Go types;
|
||||
- per-stage LLM permit pools that could exceed or partition the global ceiling;
|
||||
or
|
||||
- concurrent work implemented through an unbounded goroutine per lane or chunk.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
The target state is reached when:
|
||||
|
||||
- all production lanes use one typed artifact from extract through normalize
|
||||
and typed validation;
|
||||
- codecs own schema-aware serialization at every type-erasure, checkpoint, and
|
||||
debug boundary;
|
||||
- incompatible lane composition and invalid options fail before source work;
|
||||
- source units and chunks carry validated canonical provenance;
|
||||
- production extensions follow the domain-first package and registrar rules;
|
||||
- extract scheduling is bounded, deterministic, concurrent across lanes, and
|
||||
race-free;
|
||||
- instrumented tests prove actual concurrent LLM calls never exceed
|
||||
`concurrency.total_llm` across all callers;
|
||||
- the maintained D&D example and compatibility baselines retain their durable
|
||||
contracts; and
|
||||
- current-behavior documentation is updated as each implemented boundary lands.
|
||||
@@ -4,6 +4,12 @@ Current Notarius behavior is documented in the canonical README, CLI,
|
||||
configuration, operations, internal, and integration docs. This roadmap records
|
||||
future work only.
|
||||
|
||||
## Focused Roadmaps
|
||||
|
||||
- [Domain-Typed Pipeline Implementation](domain.md): proposed migration to
|
||||
domain-owned typed artifact lanes, domain-first packages, explicit
|
||||
serialization boundaries, and bounded deterministic extract execution.
|
||||
|
||||
## Candidate Product Work
|
||||
|
||||
- Additional input adapters, such as Markdown or note-export formats.
|
||||
@@ -32,7 +38,6 @@ future work only.
|
||||
enforcement that a validator is suitable for a specific stage or module.
|
||||
- Batching or context-window controls for LLM-backed validators if validator
|
||||
inputs become large enough to require them.
|
||||
- Parallel execution where it preserves deterministic manifests and diagnostics.
|
||||
- Additional output encoders.
|
||||
|
||||
## Candidate Operational Work
|
||||
|
||||
836
docs/roadmap/implementation.md
Normal file
836
docs/roadmap/implementation.md
Normal file
@@ -0,0 +1,836 @@
|
||||
# Domain-Typed Pipeline Implementation Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
This document is the executable implementation plan for the target state in the
|
||||
[domain-typed pipeline feature roadmap](domain.md). It assumes the decisions in
|
||||
[ADR-0002](../adr/0002-linear-pipes-and-filters-pipeline.md),
|
||||
[ADR-0003](../adr/0003-typed-interfaces-with-two-zone-data-model.md), and
|
||||
[ADR-0004](../adr/0004-package-modules-by-domain.md).
|
||||
|
||||
The intended operator is an LLM coding agent working through one stage per
|
||||
implementation prompt. Complete the stages in order. Each stage must leave the
|
||||
repository buildable and tested; do not defer a broken intermediate state to a
|
||||
later stage.
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
For every stage:
|
||||
|
||||
1. Read `docs/development.md` and follow its task-specific reading guide. Read
|
||||
the current implementation and focused tests for every touched subsystem.
|
||||
2. Treat the feature roadmap as the canonical owner of desired behavior and
|
||||
this document as the canonical owner of task sequencing. Do not restate
|
||||
future behavior in current-behavior documentation before it exists.
|
||||
3. Preserve unrelated user changes. Use mechanical moves where possible so file
|
||||
history and test intent remain legible.
|
||||
4. Add focused tests with the change. Run those tests while iterating, then run
|
||||
`go test ./...`, `go vet ./...`, and `go build ./cmd/notarius` before ending
|
||||
the stage.
|
||||
5. Run `go test -race ./...` in stages that introduce or change concurrency and
|
||||
in the final stage.
|
||||
6. Update the canonical current-behavior documents in the same stage in which
|
||||
behavior changes. At minimum, reconsider `docs/policy/architecture.md`,
|
||||
`docs/internal/overview.md`, `docs/internal/pipeline.md`,
|
||||
`docs/internal/modules.md`, `docs/internal/llm.md`, `docs/config.md`,
|
||||
`docs/operations.md`, and `docs/integrations/` according to the documentation
|
||||
policy; edit only the documents whose owned facts changed.
|
||||
7. Do not change user-visible module keys, validator keys, output paths, durable
|
||||
D&D JSON, prompt/schema identities, default chains, or rejection semantics
|
||||
unless this plan explicitly requires it.
|
||||
8. Stop after a stage if an exit criterion cannot be met. Record the concrete
|
||||
blocker rather than implementing a second architecture alongside this one.
|
||||
|
||||
## Fixed Technical Decisions
|
||||
|
||||
The following choices are inputs to implementation, not questions to reopen in
|
||||
individual stages.
|
||||
|
||||
### Source types
|
||||
|
||||
Keep the existing names `source.SourceDocument`, `source.SourceUnit`, and
|
||||
`source.SourceRef`. Add `Ref source.SourceRef` to `SourceUnit`. Move
|
||||
`contracts.SourceChunk` to `internal/core/source` as `source.Chunk`, with this
|
||||
logical shape:
|
||||
|
||||
```go
|
||||
type Chunk struct {
|
||||
ID string
|
||||
SourceID string
|
||||
Index int
|
||||
Ref SourceRef
|
||||
Content []byte
|
||||
MediaType string
|
||||
Units []SourceUnit
|
||||
Metadata map[string]any
|
||||
}
|
||||
```
|
||||
|
||||
Remove `StartUnitID` and `EndUnitID`; `Ref` is the only chunk-boundary
|
||||
representation. A Seriatim unit's self-reference is
|
||||
`{SourceID: document ID, StartUnitID: unit ID, EndUnitID: unit ID}`. A chunk
|
||||
reference spans its first and last included units. Advance persisted workspace
|
||||
state from `notarius.workspace.v1` to `notarius.workspace.v2`; v1 state is
|
||||
incompatible and must be recomputed, but never deleted automatically.
|
||||
|
||||
### Artifact contracts
|
||||
|
||||
Place engine-owned artifact primitives with the other universal contracts under
|
||||
`internal/framework/contracts`:
|
||||
|
||||
```go
|
||||
type ArtifactKind string
|
||||
|
||||
type ArtifactSchema struct {
|
||||
ID string
|
||||
Name string
|
||||
Version string
|
||||
JSONSchema []byte
|
||||
}
|
||||
|
||||
type SerializedArtifact struct {
|
||||
Kind ArtifactKind
|
||||
Schema ArtifactSchema
|
||||
MediaType string
|
||||
Content []byte
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
type ArtifactCodec[T any] interface {
|
||||
Kind() ArtifactKind
|
||||
Schema() ArtifactSchema
|
||||
MediaType() string
|
||||
Encode(T) ([]byte, error)
|
||||
Decode([]byte) (T, error)
|
||||
}
|
||||
```
|
||||
|
||||
Use strict JSON decoding for JSON codecs: reject unknown fields and trailing
|
||||
tokens. Encoding must be deterministic for equal canonical values. Clone byte
|
||||
slices and maps at framework ownership boundaries. Validate non-empty artifact
|
||||
kind, schema ID, schema name, schema version, media type, and JSON Schema during
|
||||
registration. Compute and retain a SHA-256 digest of the JSON Schema bytes.
|
||||
|
||||
Use generic `Extractor[T]`, `Merger[T]`, `Normalizer[T]`, and
|
||||
`TypedValidator[T]` contracts. The exact request/result structs may retain
|
||||
existing names where that reduces churn, but they must satisfy these rules:
|
||||
|
||||
- the extractor returns `T`, warnings, and framework-owned chunk provenance;
|
||||
- merge receives accepted per-chunk typed values carrying lane ID, source ID,
|
||||
chunk ID, chunk index, and chunk reference, already sorted by chunk index;
|
||||
- normalize receives and returns `T`;
|
||||
- typed validators receive `T` plus the relevant universal source, chunk,
|
||||
reference, lane, stage, profile, and metadata context;
|
||||
- the LLM client and decoded module options are held by constructed
|
||||
implementations, not passed in operation requests; and
|
||||
- no module-facing Zone-B request or result contains `RawPayload`, `any`, or
|
||||
serialized JSON as its artifact value.
|
||||
|
||||
Retain separate framework wrappers around `T` for extract, merge, and normalize
|
||||
provenance. Do not put lane IDs, module keys, or framework warnings into the D&D
|
||||
domain value itself.
|
||||
|
||||
Support a second `SerializedValidator` contract for representation-level
|
||||
generic validators. Its request contains immutable bytes, media type, and
|
||||
optional schema metadata. For a Zone-B value, the framework produces that
|
||||
request with the lane codec. Keep a separate non-generic `ChunkValidator`
|
||||
contract for semantic validation of immutable `[]source.Chunk`; when a
|
||||
representation validator is selected at chunk, the framework instead supplies
|
||||
its canonical JSON chunk encoding. `valid_json` and `valid_json_schema` use the
|
||||
serialized path, domain validators use `TypedValidator[T]`, and generic
|
||||
approve/reject validators register explicit chunk and typed-artifact variants.
|
||||
|
||||
### Typed registry model
|
||||
|
||||
Because Go methods cannot introduce type parameters, expose free generic
|
||||
registration functions in `internal/framework/pipeline`, backed by private
|
||||
non-generic registry entries. Use `reflect.TypeFor[T]()` only inside registration
|
||||
and framework assembly to prove exact type equality.
|
||||
|
||||
- Codec registry key: artifact kind. Exactly one codec may be registered per
|
||||
kind.
|
||||
- Extractor registry key: existing module key. Each entry declares one artifact
|
||||
kind and exact Go type.
|
||||
- Merger and normalizer registry key: `(existing module key, artifact kind)`.
|
||||
- Typed validator registry key: `(existing validator key, artifact kind)`.
|
||||
- Chunk-validator registry key: existing validator key in the distinct chunk
|
||||
target namespace.
|
||||
- Serialized validators retain their existing validator key and declare whether
|
||||
they support chunk values, artifact values, or both.
|
||||
- Duplicate keys/variants, missing codecs, Go-type mismatches, and incompatible
|
||||
selected variants are errors.
|
||||
|
||||
The extractor selected for a lane establishes the lane artifact kind. During
|
||||
resolution, look up merger, normalizer, and validators against that kind.
|
||||
Record artifact kind, schema ID, schema version, and schema digest on the
|
||||
resolved lane and in the resolved-pipeline digest. A resolved pipeline with an
|
||||
incompatible lane must fail before preparation or source execution.
|
||||
|
||||
The private erased lane entry owns closures for construction and execution of
|
||||
one concrete `T`. It may store a value as `any` internally, but it must verify
|
||||
the exact registered `reflect.Type` at every erased boundary and return a
|
||||
descriptive framework error rather than panic.
|
||||
|
||||
### Construction and preparation
|
||||
|
||||
Use one uniform construction context:
|
||||
|
||||
```go
|
||||
type ModuleDependencies struct {
|
||||
LLM contracts.StructuredLLMClient
|
||||
}
|
||||
|
||||
type BuildRequest struct {
|
||||
Dependencies ModuleDependencies
|
||||
Options map[string]any
|
||||
}
|
||||
```
|
||||
|
||||
Each registry entry stores both an option-validation closure and a constructor.
|
||||
Implementations own concrete option structs and one decoder used by both
|
||||
closures. Resolution/configuration validation calls the decoder and discards
|
||||
the value; preparation calls it once and supplies the decoded value to the
|
||||
constructor. Reject unknown option fields. Empty options produce the
|
||||
implementation's explicit defaults.
|
||||
|
||||
Add:
|
||||
|
||||
```go
|
||||
func Prepare(
|
||||
resolved ResolvedPipeline,
|
||||
registries Registries,
|
||||
deps ModuleDependencies,
|
||||
) (*PreparedPipeline, error)
|
||||
```
|
||||
|
||||
`PreparedPipeline` retains explicit resolved input, chunk, artifact-lane, and
|
||||
output fields and all constructed validators. Construct in stable pipeline
|
||||
order: input; chunk and its validators; each resolved lane in order with extract,
|
||||
merge, normalize, and their validator chains in stage order; then output. On the
|
||||
first failure, return an error identifying stage, lane if any, module or
|
||||
validator key, and cause. No operation method may have run.
|
||||
|
||||
Create the one scheduled production LLM client first, inject it into
|
||||
preparation, and then run only the prepared pipeline. Test-only deterministic
|
||||
modules may accept a nil LLM dependency; any implementation that declares or
|
||||
uses LLM-backed execution must reject a nil client at preparation. Remove LLM
|
||||
clients and raw option maps from operation requests after every production
|
||||
implementation has migrated.
|
||||
|
||||
Constructed modules and validators are reused for a run. Anything callable from
|
||||
parallel extract workers must be concurrency-safe; production implementations
|
||||
should be immutable after construction.
|
||||
|
||||
### Package registration
|
||||
|
||||
Each package-family registrar exposes:
|
||||
|
||||
```go
|
||||
func Register(registries pipeline.Registries, assets *llm.AssetRegistry) error
|
||||
```
|
||||
|
||||
The generic and Seriatim registrars ignore the asset argument until they need
|
||||
it. Registrars validate the registry pointers they use and return contextual
|
||||
errors. The CLI creates one complete registry set and one asset registry, then
|
||||
calls registrars in this order: generic, Seriatim, D&D. The D&D registrar owns
|
||||
D&D codecs, implementations, typed generic specializations, prompt/schema
|
||||
assets, and default validator chains.
|
||||
|
||||
`internal/modules/dnd` owns D&D shared types. Its `register` sibling may import
|
||||
children and generic strategies; the root package must not import its children.
|
||||
`internal/modules/generic` never imports D&D. `internal/modules/seriatim` does not
|
||||
import D&D. Move domain-neutral embedded prompt-filesystem helpers to
|
||||
`internal/framework/promptfs`.
|
||||
|
||||
### D&D typed model
|
||||
|
||||
Define `dnd.SpellList`, `dnd.SpellCast`, and evidence/source-reference fields at
|
||||
the D&D package root. Use `source.SourceRef`; do not create another D&D unit-ref
|
||||
type for artifact provenance. The stable artifact kind is
|
||||
`dnd/spell-list`.
|
||||
|
||||
The spell extractor owns a private LLM DTO and the existing
|
||||
`dnd_spells_llm.v1.json` response schema. It canonicalizes and maps that DTO to
|
||||
`dnd.SpellList`. The codec package owns `dnd_spells.v1.json` and the durable
|
||||
encoding. Keep those schemas separate. Remove the validator-only
|
||||
`spellpayload` model after all three D&D validators consume `dnd.SpellList`.
|
||||
|
||||
Make `appendorder` a generic strategy that accepts a typed combine function at
|
||||
registration/construction. The D&D registrar supplies a function that appends
|
||||
spell casts in already-sorted source-chunk order. Make `noop` a generic typed
|
||||
strategy. Neither generic package imports D&D.
|
||||
|
||||
### Serialized boundaries
|
||||
|
||||
After normalize, encode `T` once to `SerializedArtifact` for final output.
|
||||
Output remains domain-neutral and receives serialized artifacts. Preserve the
|
||||
existing output bundle and index contract.
|
||||
|
||||
Extract, merge, and normalize checkpoints encode and decode through the same
|
||||
lane codec. Checkpoint identity includes artifact kind, schema ID, schema
|
||||
version, and schema digest. A missing codec or any mismatch invalidates reuse
|
||||
and recomputes the stage; it is not a fatal run error by itself. Codec decode
|
||||
failure also invalidates that checkpoint and records the reason. Never pass
|
||||
serialized checkpoint content directly to the next typed stage.
|
||||
|
||||
Debug recording uses the codec for typed artifact values and preserves existing
|
||||
opt-in/sensitive-content rules. Artifact/checkpoint digests use the stable codec
|
||||
bytes. Aggregate manifests, checkpoint indexes, warning slices, and rejection
|
||||
slices have one coordinator writer.
|
||||
|
||||
### Concurrency
|
||||
|
||||
Version-2 configuration gains:
|
||||
|
||||
```yaml
|
||||
concurrency:
|
||||
total_llm: 4
|
||||
stage_workers:
|
||||
extract: 4
|
||||
```
|
||||
|
||||
Represent effective stage worker limits as `map[string]int`. Initially accept
|
||||
only `extract`; reject unknown keys. Missing extract defaults to `total_llm` and
|
||||
its valid range is `1..total_llm`. Add the environment override
|
||||
`NOTARIUS_STAGE_WORKERS_EXTRACT`. Preserve precedence rules and keep the file
|
||||
configuration version at 2.
|
||||
|
||||
Use one run-wide fixed extract worker pool. Dispatch jobs in round-robin order
|
||||
with source chunk as the outer loop and resolved lane as the inner loop. Use one
|
||||
job channel whose capacity equals the effective extract worker count, so the
|
||||
dispatcher applies bounded backpressure. Do not create one goroutine per job. A
|
||||
job includes extract retries and extract-stage validators. Store results by
|
||||
lane index and chunk index.
|
||||
|
||||
When all extract jobs for a lane are terminal, run that lane's merge and then
|
||||
normalize serially. Lane continuations may overlap. All LLM calls at all stages,
|
||||
including retries and validators, use the single injected scheduled client, so
|
||||
`total_llm` remains the authoritative process-wide provider-call ceiling.
|
||||
|
||||
Rejections are terminal results and do not cancel other work. A framework error
|
||||
cancels a derived run context, stops dispatching jobs not yet started, and waits
|
||||
for started tasks to finish or observe cancellation. Choose the returned error
|
||||
as follows:
|
||||
|
||||
1. if the parent context is canceled, return its error;
|
||||
2. otherwise discard internal `context.Canceled`/`DeadlineExceeded` errors when
|
||||
at least one non-context framework error exists; and
|
||||
3. choose the earliest remaining error by stage order (`extract`, `merge`,
|
||||
`normalize`), resolved lane index, chunk index for chunk-scoped work, and
|
||||
configured validator/operation index.
|
||||
|
||||
Use a sentinel chunk index after all real chunks for lane-scoped merge and
|
||||
normalize errors. Retain other started-task errors only in opt-in diagnostics.
|
||||
Sort accepted artifacts, warnings, and rejections by resolved lane index, source
|
||||
chunk index where applicable, stage order, validator order, and original
|
||||
within-result order. Completion timing must not affect public output. Run output
|
||||
only if every lane has a successful or rejection-only terminal outcome and no
|
||||
framework error occurred.
|
||||
|
||||
## Staged Implementation
|
||||
|
||||
### Stage 1: Compatibility Baselines and ADR Acceptance
|
||||
|
||||
Goal: lock down behavior that subsequent internal migrations must preserve and
|
||||
record the architectural decisions as accepted.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add semantic or golden compatibility tests for the maintained Seriatim-to-D&D
|
||||
path: durable output files and JSON, output index, manifest provenance,
|
||||
warnings, rejections, and stable lane/chunk ordering.
|
||||
- Snapshot production module keys, validator keys, default validator chains,
|
||||
prompt/schema identities, and maintained example/profile resolution in tests.
|
||||
- Strengthen runner tests for fixed topology, validator rejection as a nonfatal
|
||||
outcome, framework-error abort, retries, parent cancellation, checkpoint reuse
|
||||
and invalidation, diagnostics, and opt-in debug behavior.
|
||||
- Add an instrumented scheduled-client test showing that all existing production
|
||||
LLM callers share `concurrency.total_llm`. It need not demonstrate parallel
|
||||
lanes yet.
|
||||
- Review the three ADRs against this decision-complete plan, set their status to
|
||||
`Accepted`, and update their dates only if ADR policy requires an acceptance
|
||||
date. Do not rewrite accepted decision text after this stage.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- compatibility tests fail on an unintended durable-output, key, chain,
|
||||
provenance, or outcome-semantics change; and
|
||||
- ADR-0002, ADR-0003, and ADR-0004 are accepted.
|
||||
|
||||
### Stage 2: Registrar Composition Without Package Moves
|
||||
|
||||
Goal: replace CLI leaf-by-leaf registration with package-family composition
|
||||
before changing imports.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add `internal/modules/generic/register`, `internal/modules/seriatim/register`,
|
||||
and `internal/modules/dnd/register` using the fixed registrar signature.
|
||||
- Initially let those registrars import the existing stage-oriented packages.
|
||||
Move ownership of production validators, default chains, and prompt assets out
|
||||
of `internal/cli/catalog.go` and into the appropriate registrar.
|
||||
- Have the CLI allocate complete registries and the asset registry once, invoke
|
||||
generic, Seriatim, then D&D registration, and retain its existing test
|
||||
injection paths.
|
||||
- Test nil registry handling, duplicate registration errors, stable registered
|
||||
keys, default chains, and asset identities.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- the CLI composition root names only the three registrar packages, framework
|
||||
registry types, and asset registry; and
|
||||
- no production key, chain, prompt, schema, or runtime behavior changes.
|
||||
|
||||
### Stage 3: Mechanical Generic and Seriatim Package Moves
|
||||
|
||||
Goal: establish the domain-first generic and source-format trees without
|
||||
changing contracts.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Move the Seriatim adapter and tests to
|
||||
`internal/modules/seriatim/input/transcript`.
|
||||
- Move the generic chunker to `internal/modules/generic/chunk/units`, retaining
|
||||
the configured key `generic`.
|
||||
- Move append-order merge, no-op normalize, JSON output, and all generic
|
||||
validators to their target paths under `internal/modules/generic`.
|
||||
- Update only registrar imports and affected black-box tests. Preserve package
|
||||
behavior and all public registry keys.
|
||||
- Remove the emptied old directories.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- generic and Seriatim production implementations exist only under their target
|
||||
trees; and
|
||||
- compatibility baselines remain green.
|
||||
|
||||
### Stage 4: Mechanical D&D Package Move and Import Guard
|
||||
|
||||
Goal: establish the D&D tree and enforce ADR-0004 dependency direction while
|
||||
legacy contracts are still intact.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Move the D&D scenes chunker, spell extractor, validators, schemas, prompt
|
||||
assets, and D&D shared helpers into the target D&D tree. Do not create the
|
||||
typed root model or codec yet.
|
||||
- Move domain-neutral prompt filesystem helpers from
|
||||
`internal/modules/sharedassets` to `internal/framework/promptfs`.
|
||||
- Move tests with their owning implementation. Relocate tests that intentionally
|
||||
compose domains to a black-box integration-test package rather than creating
|
||||
peer-domain production imports.
|
||||
- Add a Go-parser-based import-boundary test. It must reject concrete
|
||||
D&D-to-Seriatim and Seriatim-to-D&D imports, all generic-to-D&D imports, and
|
||||
root-domain imports of child implementations. Allow domain registrars, the CLI
|
||||
composition root, and designated external integration tests to compose
|
||||
packages.
|
||||
- Remove old empty stage-oriented and validator directories.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- all production extensions use the target domain-first package layout except
|
||||
the not-yet-created typed codec/model pieces;
|
||||
- the import guard detects a deliberate fixture violation; and
|
||||
- behavior and keys remain unchanged.
|
||||
|
||||
### Stage 5: Source-Unit Provenance
|
||||
|
||||
Goal: add canonical provenance to engine-owned source units without yet changing
|
||||
the chunk type.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add `Ref source.SourceRef` to `source.SourceUnit`, including clone and debug
|
||||
representations.
|
||||
- Make the Seriatim adapter assign the fixed self-reference for every unit.
|
||||
- Extend `source.ValidateDocument` to require the unit reference's source ID to
|
||||
match the document, require start and end IDs to equal the unit ID, reject
|
||||
missing/invalid/reversed references, and preserve the existing unit-order and
|
||||
uniqueness checks.
|
||||
- Make source digests and source checkpoint serialization include the new
|
||||
reference deterministically.
|
||||
- Add focused tests for valid refs and missing, foreign, non-self, and reversed
|
||||
refs, plus source checkpoint/debug round trips.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- every produced source unit has a validated self-reference; and
|
||||
- source state preserves it through clone, debug, digest, and checkpoint paths.
|
||||
|
||||
### Stage 6: Engine-Owned Chunks and Workspace v2
|
||||
|
||||
Goal: finish the Zone-A source model and make its persisted compatibility break
|
||||
explicit.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add `source.Chunk` with the fixed shape and update universal chunk contracts,
|
||||
modules, validators, runner code, checkpoints, debug envelopes, and tests to
|
||||
use it.
|
||||
- Derive `Chunk.Ref` from the first and last included unit references. Validate
|
||||
source identity, non-empty ordered units, contiguous boundary agreement, and
|
||||
exact correspondence between the chunk ref and first/last unit refs.
|
||||
- Remove `contracts.SourceChunk`, `StartUnitID`, and `EndUnitID` after all
|
||||
consumers migrate. Do not keep aliases.
|
||||
- Advance `workspace.WorkspaceSchemaVersion` to `notarius.workspace.v2`. Make
|
||||
loader behavior explicitly classify v1 as incompatible and recompute while
|
||||
leaving files untouched.
|
||||
- Update checkpoint identity/digest tests and operations documentation for the
|
||||
one-time v1 resume miss.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- no production code imports a framework-owned chunk type;
|
||||
- chunk provenance round-trips exactly; and
|
||||
- v1 workspaces are safely ignored while v2 workspaces reuse successfully.
|
||||
|
||||
### Stage 7: Artifact and Codec Foundation
|
||||
|
||||
Goal: add the typed primitives and prove strict serialization independently of
|
||||
production lanes.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add the fixed artifact types, codec interface, schema digest helper, and clone
|
||||
helpers under `internal/framework/contracts`.
|
||||
- Add `ArtifactCodecRegistry` to `pipeline.Registries` and `ModuleCatalog`.
|
||||
- Implement generic codec registration and private erasure/type tracking.
|
||||
- Validate registration metadata and duplicates. Ensure erased encode/decode
|
||||
returns typed errors, never reflection panics.
|
||||
- Use two small test artifact types to cover registration, exact type identity,
|
||||
deterministic encoding, strict decoding, cloning, duplicate kind rejection,
|
||||
and schema metadata/digest behavior.
|
||||
- Wire the new empty registry through CLI/test registry constructors without
|
||||
changing production lane execution.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- codecs for heterogeneous test types can coexist and safely round-trip through
|
||||
erased framework storage; and
|
||||
- current production behavior remains on the legacy raw path and unchanged.
|
||||
|
||||
### Stage 8: Typed Contracts, Variants, and Resolution
|
||||
|
||||
Goal: resolve a complete type-compatible lane before executing it.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add the typed stage, typed validator, chunk validator, serialized validator,
|
||||
and provenance wrapper contracts from the fixed decisions.
|
||||
- Extend extractor specs with artifact kind/type. Convert merger, normalizer,
|
||||
and typed validator registries to artifact-kind variants while retaining
|
||||
serialized-validator registration by key.
|
||||
- Implement free generic registration helpers and private erased entries.
|
||||
- Extend lane resolution to derive kind from extractor, require its codec,
|
||||
select exact merger/normalizer/validator variants, and include artifact/schema
|
||||
identity in resolved lanes and pipeline digest.
|
||||
- Keep legacy registration helpers only as explicitly named transitional APIs;
|
||||
do not let a raw registration satisfy a typed lane.
|
||||
- Add composition tests with two artifact types and heterogeneous lanes. Cover
|
||||
missing codec, missing variant, Go-type mismatch, duplicate variant, wrong
|
||||
validator kind, stable resolution order, and digest changes on schema identity
|
||||
or schema digest changes.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- heterogeneous typed test lanes resolve without module-facing erasure;
|
||||
- every incompatible selection fails before execution; and
|
||||
- existing raw production lanes continue to resolve only through their visible
|
||||
transitional path.
|
||||
|
||||
### Stage 9: Preparation and Construction Foundation
|
||||
|
||||
Goal: construct and validate an entire resolved pipeline before source work.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add `ModuleDependencies`, `BuildRequest`, `PreparedPipeline`, and `Prepare` as
|
||||
specified.
|
||||
- Extend registry entries/specs with option validation and construction
|
||||
functions. Supply adapters for legacy zero-argument constructors during the
|
||||
migration.
|
||||
- Call option validation for every selected module and validator during
|
||||
resolution/config validation. Reject unknown fields and contextualize errors.
|
||||
- Have preparation construct all selected components in fixed order and retain
|
||||
immutable prepared lane executors.
|
||||
- Update the runner API so `Run` receives a prepared pipeline. At the CLI, create
|
||||
the shared scheduled LLM client, prepare, and only then invoke the runner.
|
||||
- Prove with fakes that malformed options, missing required LLM dependencies,
|
||||
and late-component construction failures occur before the input adapter's
|
||||
`Parse` method.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- all components are constructed before source work;
|
||||
- preparation errors identify exact scope and perform no operations; and
|
||||
- legacy production modules still run through temporary construction adapters.
|
||||
|
||||
### Stage 10: Migrate Universal Modules to Construction
|
||||
|
||||
Goal: remove legacy option/dependency handling from input, chunk, and output.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Give Seriatim input, generic units chunking, D&D scenes chunking, generic JSON
|
||||
output, and all applicable chunk validators implementation-owned option
|
||||
structs and strict decoders.
|
||||
- Build those implementations with decoded options and injected dependencies.
|
||||
Require the LLM client for D&D scenes; keep deterministic implementations
|
||||
independent of it.
|
||||
- Remove `Options` and `LLMClient` from the corresponding operation requests.
|
||||
Retain per-run source, reference, profile, session, and metadata fields.
|
||||
- Update registrars and focused tests. Verify options are decoded once during
|
||||
preparation and operation methods do not inspect raw maps.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- no universal production module parses raw options during execution; and
|
||||
- every universal LLM call uses the injected shared client.
|
||||
|
||||
### Stage 11: Canonical D&D Model, Codec, and Typed Extractor
|
||||
|
||||
Goal: establish the first production `T` and its extraction boundary.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add canonical spell types at the D&D root using engine-owned source refs.
|
||||
- Add `internal/modules/dnd/codec/spells`, register kind `dnd/spell-list`, and
|
||||
make it own the existing durable `dnd_spells.v1.json` schema and strict stable
|
||||
encoding.
|
||||
- Keep the private spell-extraction LLM DTO and
|
||||
`dnd_spells_llm.v1.json` in the extractor package. Map canonicalized DTO values
|
||||
to `dnd.SpellList` and do not expose the DTO to validators or the codec.
|
||||
- Convert the extractor to `Extractor[dnd.SpellList]`, construction-time options
|
||||
and dependency injection. Preserve prompt assets, retries, warnings, evidence,
|
||||
and LLM response validation.
|
||||
- Register the codec and typed extractor from the D&D registrar.
|
||||
- Add codec compatibility tests against existing durable fixtures and tests
|
||||
proving LLM schema ownership is separate from durable schema ownership.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- the typed extractor returns the canonical domain model;
|
||||
- codec output is semantically identical to the maintained durable spell JSON;
|
||||
and
|
||||
- no downstream production consumer is switched until the next stages.
|
||||
|
||||
### Stage 12: Typed Validators and Generic Typed Strategies
|
||||
|
||||
Goal: complete all typed components required by the D&D lane.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Convert D&D spell shape, source-reference, and source-relatedness validators
|
||||
to `TypedValidator[dnd.SpellList]` with construction-time options/dependencies.
|
||||
- Remove JSON reparsing from those validators. Remove the duplicate
|
||||
`spellpayload` package after its final consumer migrates.
|
||||
- Convert `valid_json` and `valid_json_schema` to serialized validators. Register
|
||||
them so the framework uses the D&D codec when they occur in the spell chain.
|
||||
- Implement generic typed append-order merge and no-op normalize. In the D&D
|
||||
registrar, register D&D variants using a spell-list append function and
|
||||
`noop[dnd.SpellList]`.
|
||||
- Convert always-accept/reject into explicit chunk and typed variants and
|
||||
register the D&D variants without changing their keys. Verify serialized
|
||||
validators operate on the framework encoding at chunk and the codec encoding
|
||||
at artifact stages.
|
||||
- Test typed validator requests, source refs, relatedness LLM injection, generic
|
||||
strategy reuse with a second test type, default chain order, and rejection
|
||||
behavior.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- every selected spell-lane component has a compatible D&D typed variant;
|
||||
- generic packages import no D&D code; and
|
||||
- the duplicate validator payload model and inter-validator JSON parsing are
|
||||
gone.
|
||||
|
||||
### Stage 13: Typed D&D Runner Vertical Slice
|
||||
|
||||
Goal: execute one complete production lane as `dnd.SpellList` while retaining
|
||||
temporary raw output/checkpoint adapters.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Implement the erased typed lane executor and runner path for extract, stage
|
||||
retries, typed/serialized validation, merge, and normalize.
|
||||
- Keep accepted extract values indexed by chunk and pass them to merge in source
|
||||
order. Preserve warnings and rejections in stable scope order.
|
||||
- Add narrow transitional adapters from typed stage outputs to the existing raw
|
||||
checkpoint/debug/output envelopes. These adapters must use the registered
|
||||
codec and be named/commented as migration-only.
|
||||
- Route the production D&D lane through the typed path; leave no production raw
|
||||
D&D stage module registered in parallel.
|
||||
- Add end-to-end and checkpoint-disabled tests proving the maintained D&D output
|
||||
and outcome semantics are unchanged. Add incompatible-lane tests proving
|
||||
failure occurs before input.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- D&D values remain typed from extractor through normalize and typed validation;
|
||||
- the runner's only D&D erasure is its private lane adapter and explicit codec
|
||||
boundary; and
|
||||
- durable output remains unchanged through the transitional adapter.
|
||||
|
||||
### Stage 14: Typed Checkpoints, Debugging, and Output
|
||||
|
||||
Goal: move every serialization side effect and the final Zone-C boundary to the
|
||||
codec model.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Change runner/output contracts so final normalized results are
|
||||
`SerializedArtifact` values. Update generic JSON output without importing D&D.
|
||||
- Preserve logical file names, index fields, media types, manifest contents, and
|
||||
durable spell JSON.
|
||||
- Change extract, merge, and normalize checkpoints to store codec bytes plus
|
||||
artifact kind, schema ID, version, and schema digest. Decode reused values
|
||||
back to `T` before the next stage.
|
||||
- Implement safe invalidation for missing/mismatched codecs and decode failure,
|
||||
with explicit checkpoint-event reasons.
|
||||
- Change typed debug envelopes to serialize through the codec, preserving opt-in
|
||||
and redaction behavior. Use stable codec bytes for artifact digests.
|
||||
- Remove the transitional raw output/checkpoint/debug adapters introduced in
|
||||
Stage 13 after all tests use the typed boundaries.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- typed values round-trip at every stage checkpoint;
|
||||
- incompatible checkpoint artifacts recompute safely;
|
||||
- output and debug code are domain-neutral; and
|
||||
- no D&D typed lane depends on a raw-boundary adapter.
|
||||
|
||||
### Stage 15: Finish Construction Migration and Remove Raw Contracts
|
||||
|
||||
Goal: leave one production extension system rather than parallel raw and typed
|
||||
models.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Migrate any remaining merge, normalize, extract, and validator implementations
|
||||
to construction-time option decoding and dependency injection.
|
||||
- Remove LLM clients and raw option maps from all remaining operation requests.
|
||||
- Remove legacy raw extractor/merger/normalizer/validator contracts,
|
||||
constructors, registry entries, `RawPayload`, `ResponseSchema` if superseded,
|
||||
raw clone helpers, raw checkpoint envelopes, and migration-only adapters.
|
||||
- Remove dead duplicate models and compatibility helpers. Search for production
|
||||
references to old stage-oriented paths and raw Zone-B types.
|
||||
- Keep serialized artifacts only at codec, checkpoint/debug, and output
|
||||
boundaries.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- no production Zone-B handoff uses JSON bytes, `RawPayload`, or `any`;
|
||||
- all production configuration options are validated before execution;
|
||||
- all production LLM users receive the one injected client; and
|
||||
- there is no legacy production registration path.
|
||||
|
||||
### Stage 16: Stage-Worker Configuration
|
||||
|
||||
Goal: add the decided scheduling control without changing runner execution yet.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add `StageWorkers map[string]int` to effective concurrency configuration and
|
||||
`stage_workers` to the version-2 YAML shape. Deep-clone the map.
|
||||
- Accept only `extract`, reject unknown or empty keys, default missing extract to
|
||||
effective `total_llm`, and validate the inclusive range
|
||||
`1..total_llm` after file/environment precedence resolves.
|
||||
- Add `NOTARIUS_STAGE_WORKERS_EXTRACT` with the existing environment precedence
|
||||
and integer error style.
|
||||
- Preserve redaction/effective-config diagnostics and version 2.
|
||||
- Update `docs/config.md` and maintained examples that intentionally demonstrate
|
||||
concurrency. Do not add the field to every example when the default conveys
|
||||
the intended behavior.
|
||||
- Add file, default, merge/precedence, environment, unknown-key, boundary, and
|
||||
redacted-effective-config tests.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- every run has a validated effective extract worker count;
|
||||
- omitted configuration preserves current effective behavior at the default
|
||||
`total_llm: 1`; and
|
||||
- current configuration documentation owns the implemented contract.
|
||||
|
||||
### Stage 17: Concurrent Lane and Extract Scheduling
|
||||
|
||||
Goal: implement bounded concurrent lanes while preserving deterministic public
|
||||
behavior and the global LLM invariant.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Add the fixed-size run-wide worker pool and central round-robin dispatcher.
|
||||
Bound queued work so dispatch applies backpressure; do not enqueue the whole
|
||||
run into unbounded memory.
|
||||
- Treat extract, retries, and extract validators as one job. Publish immutable
|
||||
results to a coordinator indexed by lane and chunk.
|
||||
- Start each lane's serial merge/normalize continuation only after all its
|
||||
extract jobs are terminal. Permit different lane continuations to overlap.
|
||||
- Make the coordinator the sole writer of aggregate output, manifest,
|
||||
checkpoint-event collection, warnings, and rejections. Use attempt-specific
|
||||
debug paths and synchronize any recorder state that remains shared.
|
||||
- Implement rejection, cancellation, stable sorting, deterministic primary
|
||||
error selection, and output gating exactly as specified under Concurrency.
|
||||
- Audit every concurrently reused extractor, validator, codec, LLM/debug wrapper,
|
||||
checkpoint loader/recorder, and manifest metadata provider. Make production
|
||||
implementations immutable or add narrowly scoped synchronization.
|
||||
- Add deterministic barrier-controlled tests that force reverse completion
|
||||
order, simultaneous failures, parent cancellation, rejections mixed with
|
||||
successes, lane continuation overlap, and undispatched-job cancellation.
|
||||
- Add instrumented integration tests issuing calls from multiple lanes, retries,
|
||||
and LLM-backed validators. Assert provider calls never exceed
|
||||
`total_llm`, extract jobs never exceed the effective extract worker count, and
|
||||
both limits are exercised independently.
|
||||
- Run `go test -race ./...` and eliminate races rather than weakening tests.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- lanes and extract jobs actually overlap when configured above one;
|
||||
- job and provider-call limits are independently enforced;
|
||||
- public results and primary errors are identical across forced completion
|
||||
orders; and
|
||||
- full race testing passes.
|
||||
|
||||
### Stage 18: Documentation, Cleanup, and Final Verification
|
||||
|
||||
Goal: make the implemented repository and its canonical documentation agree,
|
||||
then close the focused roadmap.
|
||||
|
||||
Tasks:
|
||||
|
||||
- Review every current-behavior document routed by `docs/development.md` and
|
||||
update only its owned facts: architecture and dependency direction, package
|
||||
inventory, pipeline resolution/preparation/execution, typed module contracts,
|
||||
LLM scheduling, configuration, checkpoint compatibility, diagnostics,
|
||||
operations, and durable integration contracts.
|
||||
- Verify maintained examples and copyable files against the implementation.
|
||||
- Remove stale old package paths, raw-contract terminology, and superseded
|
||||
future-work entries. Validate all changed documentation links.
|
||||
- Update ADR consequences only in ways permitted for accepted ADR metadata; do
|
||||
not edit accepted decision text. Record a new superseding ADR if final code
|
||||
required an architectural change.
|
||||
- Mark the feature roadmap implemented and reduce this implementation plan to a
|
||||
concise completion record, or move it to the repository's established
|
||||
completed-roadmap location if one exists. Do not let this file become a second
|
||||
current-behavior reference.
|
||||
- Run final checks:
|
||||
|
||||
```sh
|
||||
go test ./...
|
||||
go test -race ./...
|
||||
go vet ./...
|
||||
go build ./cmd/notarius
|
||||
```
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- all feature-roadmap completion criteria are met;
|
||||
- no stale production paths or legacy typed/raw bridge remain;
|
||||
- current documentation and examples describe only implemented behavior; and
|
||||
- all final validation commands pass.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None. The feature-policy decisions and the implementation choices required to
|
||||
begin each stage are resolved above. If implementation evidence contradicts one
|
||||
of the accepted architectural decisions, stop and handle that as an ADR change
|
||||
or supersession rather than treating it as an implicit implementation choice.
|
||||
Reference in New Issue
Block a user