19 KiB
LLM And Stage Reference Expansion Implementation Plan
Purpose
Implement these future roadmap items:
- First-class LLM access for all LLM-eligible pipeline stages:
chunk,extract, andnormalize. - Expand reference support beyond extractors to
chunkandnormalizemodules, using the same declared-slot model and preserving stage-specific ownership boundaries.
This is a decision-complete implementation plan for an LLM coding agent. Follow the stages in order. Keep current-behavior docs unchanged until the corresponding behavior exists. Do not implement token budgeting, non-file reference producers, structured references, retrieval, or multiple effective LLM profiles in this pass.
Target Behavior
LLM access:
contracts.ChunkRequest,contracts.ExtractionRequest, andcontracts.NormalizeRequestall carryLLMClient,LLMProfile,Options, andMetadata.- The runner passes the configured structured LLM client to chunkers, extractors, and normalizers.
- The existing single-effective-LLM-profile CLI restriction remains in place.
References:
chunk,extract, andnormalizemodules can declare reference slots.- The framework treats references as opaque UTF-8 text plus provenance. Stage modules own slot names and semantics.
- Reference slots are declared in both runtime module contracts and
ModuleSpec.ReferenceSlots, so config and pipeline validation can inspect slots without constructing modules. ModuleSpec.ReferenceSlotsis valid only forchunk,extract, andnormalizemodules.input,merge,validate, andoutputmodules must not declare reference slots.- Pipeline-level
referencesremain defaults. A pipeline-level binding is valid when any declared eligible module in the pipeline declares the slot. In a run, it is applied only to selected targets that declare the slot. - Stage-local reference bindings override pipeline-level defaults for that target.
- CLI
--referenceand--without-referencecan target chunk, extract, and normalize references explicitly, while preserving existing unambiguous flat forms. - Reference materialization still accepts file producers only, validates UTF-8,
infers media types from extensions, enforces optional
AcceptedMediaTypesandMaxBytes, warns for empty files, and never writes content to diagnostics, logs, errors, or manifests. - Manifest and diagnostics provenance identifies the reference target stage and, for lane-owned stages, the lane ID.
Target Config Shape
Keep existing config compatibility and add stage-specific binding locations:
pipelines:
dnd-session:
input: seriatim
references:
campaign_context: ./campaign/context.md
chunk:
module: dnd/scenes
references:
scene_guide: ./campaign/scenes.md
artifacts:
spells:
extract: dnd/spells
references:
glossary: ./campaign/glossary.md
normalize:
module: noop
references:
normalization_notes: ./campaign/normalization.md
Compatibility rule:
- Existing
artifacts.<lane>.referencesremains supported as an alias for extractor references. - New
artifacts.<lane>.extract.referencesis the canonical extract-stage location. - If both the legacy lane-level
referencesmap andextract.referencesbind the same slot,extract.referenceswins. This keeps migration deterministic and avoids breaking existing configs.
chunk.references and normalize.references are supported only when their
module binding uses object form. Scalar bindings continue to work when no
stage-local references are needed.
Target CLI Reference Selectors
Preserve existing valid forms and add explicit stage selectors:
slot=path: valid only when exactly one selected reference target across chunk, selected extractors, and selected normalizers declaresslot.chunk.slot=path: targets the singleton chunk module.lane.slot=path: valid only when exactly one selected module in that lane, extract or normalize, declaresslot.lane.extract.slot=path: targets a lane extractor.lane.normalize.slot=path: targets a lane normalizer.
--without-reference accepts the same selector forms without =path.
Ambiguous selectors must fail before any LLM call and should name the ambiguous targets and the more specific selector forms.
Stage 1: Contract And Registry Expansion
Add reference declaration support to chunkers and normalizers and LLM delivery to normalizers. This stage should not change config shape or runtime reference resolution yet.
Implementation steps:
- Add
References contracts.ReferenceSettocontracts.ChunkRequest. - Add
LLMClient contracts.StructuredLLMClientandReferences contracts.ReferenceSettocontracts.NormalizeRequest. - Add
ReferenceSlots() []ReferenceSlottocontracts.Chunkerandcontracts.Normalizer. - Update every concrete chunker and normalizer plus all test fakes to implement
ReferenceSlots(). Existing modules without references should returnnil. - Change
validateModuleSpecsoModuleSpec.ReferenceSlotsis allowed forStageChunk,StageExtract, andStageNormalize, and rejected for all other stages. - Add registry/module tests proving:
- chunker specs may declare valid reference slots;
- normalizer specs may declare valid reference slots;
- extractor specs still may declare valid reference slots;
- input, merge, validate, and output specs reject reference slots;
- invalid slot names, duplicate slots, and negative
MaxBytesare rejected for all eligible stages.
- Add production registration tests comparing runtime
ReferenceSlots()with registeredModuleSpec.ReferenceSlotsfor chunkers, extractors, and normalizers that declare slots. Modules with no slots should be covered by simple nil/empty assertions.
Verification:
go test ./internal/framework/contractsgo test ./internal/framework/pipelinego test ./internal/modules/chunk/...go test ./internal/modules/normalize/...go test ./...
Stage 2: Resolved Reference Target Model
Replace extractor-only resolved reference storage with a target-aware model. Keep external config and CLI behavior unchanged in this stage where possible.
Implementation steps:
- Introduce a target model in
internal/framework/pipeline, for example:- target stage:
chunk,extract, ornormalize; - optional lane ID for lane-owned targets;
- module key;
- reference bindings;
- materialized
ReferenceSet.
- target stage:
- Add a resolved chunk reference holder to
ResolvedPipeline. - Split lane reference storage into extract and normalize holders on
ResolvedArtifactLane. - Keep JSON field names deterministic and redaction-friendly. Do not expose reference content through resolved pipeline JSON.
- Update
CloneReferenceSet,ReferenceProvenance, redacted effective config, resolved-pipeline diagnostics, and any affected tests for the target-aware model. - Preserve existing extractor behavior by mapping old lane extractor references into the new extract target.
- Keep the pipeline digest deterministic and based on resolved reference bindings, not materialized reference bytes. Materialized reference digests remain manifest provenance.
Verification:
go test ./internal/framework/pipelinego test ./internal/core/configgo test ./internal/core/diagnosticsgo test ./...
Stage 3: Config Parsing And Validation
Add stage-local reference fields while preserving existing configs.
Implementation steps:
- Extend the file config module binding object to accept
references. - Add
References map[string]stringtopipeline.ModuleBinding, or use a narrower equivalent if the implementation can do so without duplicating binding parsing. If added toModuleBinding, validation must reject references on non-eligible stages. - Apply file config references from:
pipelines.<id>.chunk.references;pipelines.<id>.artifacts.<lane>.extract.references;pipelines.<id>.artifacts.<lane>.normalize.references;- existing
pipelines.<id>.artifacts.<lane>.referencesas an extract-stage compatibility alias.
- Validate reference maps for empty names, duplicate-after-trim names, and empty sources at each new location.
- Reject references on
input,merge, validators, andoutputbindings with clear config errors. - Preserve existing
llm_profilevalidation for chunk, extract, and normalize bindings. No new LLM profile semantics are introduced in this pass. - Add config tests for:
- parsing
chunk.references; - parsing
extract.references; - parsing
normalize.references; - legacy lane
referencescompatibility; - duplicate conflicts where
extract.referencesoverrides legacy lane references for the same slot; - rejection of references on unsupported module binding locations;
- redaction/effective config cloning for all new reference maps.
- parsing
Verification:
go test ./internal/core/configgo test ./internal/framework/pipelinego test ./...
Stage 4: Pipeline Resolution
Resolve reference bindings against all eligible stage targets and selected lanes without constructing modules.
Implementation steps:
- During
ResolvePipeline, collect declared reference slots from:- the configured chunker spec;
- every declared artifact lane extractor spec;
- every declared artifact lane normalizer spec.
- Keep pipeline-level reference validation broad: a pipeline-level binding is
valid if any declared eligible target in the full pipeline declares that slot,
even when
--onlyselects lanes that do not consume it. - During a selected run, apply pipeline-level defaults only to:
- the singleton chunk target if its chunker declares the slot;
- selected extractor targets that declare the slot;
- selected normalizer targets that declare the slot.
- Apply stage-local references after pipeline defaults:
- chunk-local references override chunk pipeline defaults;
- extract-local references override extract pipeline defaults;
- normalize-local references override normalize pipeline defaults.
- Enforce required slots independently per selected target after defaults, local references, CLI overrides, and CLI unbinds.
- Keep lane-local strictness: extract-local references must be declared by that lane's extractor, and normalize-local references must be declared by that lane's normalizer.
- Return clear errors that name pipeline ID, stage, lane ID when present, slot, and module key.
- Add pipeline tests for:
- pipeline default consumed by chunk only;
- pipeline default consumed by extract only;
- pipeline default consumed by normalize only;
- one pipeline default consumed by several eligible targets;
- valid pipeline default declared only by an unselected lane target;
- unknown pipeline-level slot rejected when no declared target has it;
- extract-local reference rejected by normalizer-only slot;
- normalize-local reference rejected by extractor-only slot;
- required chunk reference missing;
- required normalize reference missing;
- local overrides over pipeline defaults for each eligible stage.
Verification:
go test ./internal/framework/pipelinego test ./internal/core/configgo test ./...
Stage 5: CLI Reference Selectors
Extend CLI reference override and unbind resolution to target chunk, extract, and normalize references.
Implementation steps:
- Replace extractor-only CLI reference target lookup with target-aware lookup over the resolved eligible targets for the selected run.
- Support selector forms listed in "Target CLI Reference Selectors".
- Preserve existing behavior for unambiguous
slotandlane.slotforms. - Reject ambiguous
slotandlane.slotforms with a message naming matching targets and suggestingchunk.slot,lane.extract.slot, orlane.normalize.slotas appropriate. - Reject selectors that target an unselected lane during
--onlyruns. - Reject selectors for stages that do not declare the slot.
- Keep
--referenceempty-path rejection and--without-referenceoptional unbinding semantics. - Add CLI tests for:
chunk.slot=path;lane.extract.slot=path;lane.normalize.slot=path;- flat slot resolution across exactly one target;
- lane slot resolution across exactly one lane-local target;
- ambiguous flat slot across chunk and extract;
- ambiguous lane slot across extract and normalize;
- unbind forms for chunk, extract, and normalize;
- unbind leaving required chunk or normalize slot missing;
--onlywith an unselected lane selector.
Verification:
go test ./internal/cligo test ./internal/framework/pipelinego test ./...
Stage 6: Materialization And Provenance
Materialize references for chunk, extract, and normalize targets with one shared implementation.
Implementation steps:
- Refactor materialization so it accepts a resolved target plus that target's declared slots, rather than assuming an extractor lane.
- Reuse the existing file behavior:
- config paths relative to the config file;
- CLI paths relative to the current working directory;
- UTF-8 validation for all files;
- extension-based media type inference;
- optional
AcceptedMediaTypesenforcement; - optional
MaxBytesenforcement; - empty-file warning;
sha256:content digest.
- Store materialized reference sets on the resolved chunk target, extract targets, and normalize targets.
- Update warnings to include target context in
Scope, such as:pipeline.<id>.chunk.reference.<slot>;pipeline.<id>.lane.<lane>.extract.reference.<slot>;pipeline.<id>.lane.<lane>.normalize.reference.<slot>.
- Update manifest and diagnostics provenance to include:
stage:chunk,extract, ornormalize;lane_id: omitted for chunk and present for extract/normalize;slot_name;- origin type and URI;
- digest;
- media type;
- size bytes;
- binding source.
- Keep reference content out of manifests, diagnostics, logs, and errors.
- Add tests for materialization and provenance for all three eligible stages, including media-type rejection and UTF-8 rejection on non-extractor targets.
Verification:
go test ./internal/framework/pipelinego test ./internal/core/artifactsgo test ./internal/core/diagnosticsgo test ./internal/modules/output/jsongo test ./internal/cligo test ./...
Stage 7: Runner Delivery
Deliver LLM clients and references to eligible stage modules at runtime.
Implementation steps:
- Pass
CloneReferenceSetof the materialized chunk reference set intocontracts.ChunkRequest.References. - Continue passing
LLMClientintocontracts.ChunkRequest. - Continue passing extract references and
LLMClientintocontracts.ExtractionRequest. - Pass
LLMClientandCloneReferenceSetof the materialized normalize reference set intocontracts.NormalizeRequest. - Ensure normalizers receive their configured
LLMProfile, options, and metadata as they do today. - Add runner tests proving:
- chunkers receive LLM client and chunk references;
- extractors still receive LLM client and extract references;
- normalizers receive LLM client and normalize references;
- reference sets are cloned before delivery so modules cannot mutate resolved pipeline state;
- missing LLM client still fails only when a module actually uses it, matching existing chunk/extract behavior.
- Update the walking skeleton to exercise LLM-backed normalize access if a small fake normalizer can do so without making the fixture noisy.
Verification:
go test ./internal/framework/pipelinego test ./...
Stage 8: Prompt Helpers And Module Usage
Make prompt reference helpers usable by chunk and normalize modules, then update production modules only where needed.
Implementation steps:
- Confirm
internal/framework/promptreference rendering is contract-based and not extractor-specific. If any naming or error text says extractor-only, generalize it to module/stage language. - Add prompt tests rendering references for a fake chunker or normalizer prompt bundle if existing tests cover only extractor usage.
- Update production chunkers and normalizers:
genericchunker: no reference slots.dnd/sceneschunker: no reference slots in this pass unless a clear module-owned slot already exists.noopnormalizer: no reference slots.
- Do not invent a new production normalize module in this pass. The goal is framework support, not a domain normalizer.
Verification:
go test ./internal/framework/promptgo test ./internal/modules/chunk/...go test ./internal/modules/normalize/...go test ./...
Stage 9: Documentation And Examples
After behavior is implemented and tested, update canonical docs and examples.
Implementation steps:
- Update
docs/cli.mdwith the expanded--referenceand--without-referenceselector grammar and examples. - Update
docs/config.mdwithchunk.references,artifacts.<lane>.extract.references,artifacts.<lane>.normalize.references, and the legacyartifacts.<lane>.referencescompatibility rule. - Update
docs/internal/modules.mdwith module-author guidance forReferenceSlots()on chunkers, extractors, and normalizers, plus LLM access expectations for chunk, extract, and normalize. - Update
docs/internal/pipeline.mdwith target-aware reference resolution, materialization, provenance, and runtime delivery. - Update
docs/integrations/json-output.mdwith the new reference provenance shape includingstageand optionallane_id. - Update
docs/troubleshooting.mdwith ambiguous selector examples for chunk, extract, and normalize targets. - Update maintained examples only if they can remain small and validated. Do not add speculative reference files for chunk or normalize modules without a production module that actually declares those slots.
- Update
docs/roadmap/future.mdby removing or revising the two completed items while leaving still-deferred work such as token budgeting and non-file producers. - Replace this implementation plan with a concise completed-note document after implementation is finished.
Verification:
rg -n "chunk.references|normalize.references|lane.extract.slot|lane.normalize.slot" docs/cli.md docs/config.md docs/internal docs/integrations docs/troubleshooting.mdgo test ./...go vet ./...go build ./cmd/notarius
Final Acceptance Criteria
The feature is complete when:
- chunkers, extractors, and normalizers can declare reference slots through runtime contracts and registry specs;
- normalizers receive the structured LLM client at runtime;
- config can bind references at pipeline, chunk, extract, and normalize levels;
- CLI reference overrides and unbinds can target chunk, extract, and normalize references without ambiguous selector behavior;
- pipeline resolution validates all reference bindings before any LLM call;
- materialization and provenance work uniformly for chunk, extract, and normalize references;
- reference manifest entries identify stage and lane context;
- existing extractor reference behavior remains backward compatible;
- canonical docs describe only implemented behavior;
go test ./...,go vet ./..., andgo build ./cmd/notariuspass.