389 lines
17 KiB
Markdown
389 lines
17 KiB
Markdown
# D&D Spell Normalization Implementation Plan
|
|
|
|
## Status
|
|
|
|
Ready for implementation.
|
|
|
|
This plan implements the accepted target state in
|
|
[`dnd-spell-normalization.md`](dnd-spell-normalization.md). That document owns
|
|
feature intent and policy; this document owns implementation order and concrete
|
|
engineering decisions. Implement each stage in sequence and keep every stage
|
|
buildable and testable before proceeding.
|
|
|
|
## Scope and invariants
|
|
|
|
Add a deterministic `dnd/spells` normalize-stage module. It materializes the
|
|
same effective embedded-base-plus-overlay spell catalog used by extraction,
|
|
canonicalizes catalog-backed spell names, canonicalizes exact source-reference
|
|
sets, and conservatively collapses duplicate spell casts. It does not call an
|
|
LLM and does not add a prompt asset.
|
|
|
|
Preserve these boundaries throughout the work:
|
|
|
|
- Keep domain behavior under `internal/modules/dnd`; do not add D&D concepts to
|
|
the pipeline framework or generic contracts.
|
|
- Keep the fixed pipeline architecture unchanged. This work implements an
|
|
existing normalize stage; it does not introduce branches or DAG behavior.
|
|
- Keep `dnd.SpellList` and its JSON schema unchanged. Do not add artifact IDs,
|
|
schema constraints, or fields used only for normalization.
|
|
- Keep existing extractor, merger, validator, retry, and checkpoint formats
|
|
unchanged unless this plan explicitly identifies an integration adjustment.
|
|
- Do not add fuzzy spell matching, caster entity resolution, source-range
|
|
merging, prose reconciliation, generic duplicate normalization, or LLM-backed
|
|
validation.
|
|
- Do not mutate request values or slices that may share backing storage with an
|
|
upstream result. Construct an independent normalized result.
|
|
- Preserve nil and empty spell/reference collections rather than repairing
|
|
malformed shape. Validators own shape and source-validity rejection.
|
|
- Keep warnings stable, deterministic, bounded, and free of catalog content,
|
|
reference paths, or other sensitive material.
|
|
|
|
The normalize-stage public selector is `dnd/spells`. Stage-specific registries
|
|
make this unambiguous despite the extractor using the same selector.
|
|
|
|
## Stage 1: Typed normalizer and catalog contract
|
|
|
|
Create `internal/modules/dnd/normalize/spells` as a typed normalize module.
|
|
Follow the construction and registration patterns used by the existing D&D
|
|
spell extractor and deterministic normalizers.
|
|
|
|
### Module construction
|
|
|
|
- Define an empty `Options` type and use the repository's strict option decoder
|
|
so unknown configuration fields are rejected.
|
|
- Expose a constructor that accepts decoded options and the prepared
|
|
`contracts.ReferenceSet`.
|
|
- Resolve the effective catalog during module construction with the existing
|
|
`internal/modules/dnd/spells/catalog` resolver. Do not reimplement catalog
|
|
parsing, alias handling, or digest calculation.
|
|
- Declare one optional reference slot named `spell_catalog`, matching the
|
|
extractor's media type and size limit. Do not declare campaign references the
|
|
normalizer does not consume.
|
|
- Make malformed, duplicated, oversized, or otherwise invalid catalog
|
|
references fail during pipeline preparation, before run-state or checkpoint
|
|
composition.
|
|
- Register the module as `pipeline.StageNormalize`, artifact kind
|
|
`dnd.SpellListKind`, under selector `dnd/spells`.
|
|
- Do not require or consume an LLM profile. The normalizer remains fully
|
|
deterministic and offline.
|
|
|
|
### Prepared identity and audit metadata
|
|
|
|
Implement `pipeline.CheckpointFingerprintProvider`. Contribute exactly one
|
|
local fingerprint:
|
|
|
|
- name: `effective_catalog`
|
|
- value: the effective catalog's semantic digest
|
|
|
|
Preparation scopes this fingerprint by the normalize-stage component identity.
|
|
Return defensive data according to the provider contract.
|
|
|
|
Also implement `contracts.ManifestMetadataProvider` with the same non-secret
|
|
audit fields used by catalog-backed extraction components:
|
|
|
|
- `catalog_base_id`
|
|
- `catalog_digest`
|
|
- `catalog_overlay_ids`
|
|
|
|
Return defensive copies of collection values. Do not include catalog entries,
|
|
raw reference content, or local paths in either identity or metadata.
|
|
|
|
### Name canonicalization
|
|
|
|
For each input spell cast, call the effective catalog's existing `Lookup`
|
|
operation. This intentionally inherits its case, whitespace, apostrophe, and
|
|
explicit-alias policy.
|
|
|
|
- If lookup succeeds, write the catalog's canonical display name to the output.
|
|
- If the input is already byte-for-byte equal to the canonical name, emit no
|
|
name warning.
|
|
- If lookup succeeds and changes the value, emit reason code
|
|
`spell_name_canonicalized` at scope `spell_casts[N]`, where `N` is the merged
|
|
input index. The message identifies the input index, bounded original value,
|
|
and canonical replacement.
|
|
- If lookup fails, preserve the original value exactly and emit reason code
|
|
`spell_name_unresolved` at scope `spell_casts[N]`. Do not guess, drop, or
|
|
rewrite it.
|
|
|
|
Before quoting either an extracted name or an overlay-provided canonical name
|
|
in a warning, limit it to 128 Unicode code points by retaining the first 127 and
|
|
appending a visible ellipsis. Never truncate by byte position or produce invalid
|
|
UTF-8. Use normal Go quoting so control characters cannot corrupt diagnostic
|
|
structure.
|
|
|
|
### Source-reference canonicalization
|
|
|
|
For every cast, copy and sort references by this exact tuple:
|
|
|
|
1. `SourceID`, compared as an exact string;
|
|
2. `StartUnitID`;
|
|
3. `EndUnitID`.
|
|
|
|
Then remove exact structural duplicates. Do not trim or rewrite `SourceID`,
|
|
alter boundaries, merge adjacent or overlapping ranges, or consult auxiliary
|
|
reference text. Preserve nil as nil and an empty non-nil slice as empty.
|
|
|
|
If sorting changes order or exact duplicates are removed, emit one warning with
|
|
reason code `source_references_normalized` at `spell_casts[N]`. Its message
|
|
reports the input index, original count, final count, whether order changed, and
|
|
the duplicate-removal count. Do not emit it when the canonical result is
|
|
structurally identical to the input.
|
|
|
|
### Stage 1 tests
|
|
|
|
Add focused package tests that establish:
|
|
|
|
- strict option decoding and the exact module/reference-slot contract;
|
|
- successful embedded-base and overlay catalog construction;
|
|
- preparation-time failure for invalid catalog references;
|
|
- checkpoint fingerprint and manifest metadata values and defensive copying;
|
|
- canonical, case/whitespace/apostrophe, and overlay-alias lookups;
|
|
- canonicalization and unresolved-name warning semantics;
|
|
- safe truncation of long Unicode names and quoting of control characters;
|
|
- reference tuple ordering and exact de-duplication;
|
|
- no merging of adjacent or overlapping ranges;
|
|
- source-normalization warning semantics;
|
|
- preservation of nil versus empty collections and input immutability; and
|
|
- the repository's standard nil receiver, nil context, and already-canceled
|
|
context behavior for module calls.
|
|
|
|
Do not assert entire warning prose when reason code, scope, and semantic message
|
|
fragments are the durable contract.
|
|
|
|
## Stage 2: Conservative duplicate collapse
|
|
|
|
Perform duplicate detection after name and source-reference canonicalization.
|
|
Use original merged input positions as stable indices for warnings and retained
|
|
entry selection.
|
|
|
|
### Duplicate identity
|
|
|
|
A cast is eligible for duplicate grouping only when all of these hold:
|
|
|
|
- its spell name resolved successfully through the effective catalog;
|
|
- its canonical source-reference set is non-empty; and
|
|
- every canonical source reference passes `source.ValidateRef` against the
|
|
normalize request's source document.
|
|
|
|
Eligible casts are duplicates only when all of these keys are exactly equal:
|
|
|
|
- canonical catalog display name;
|
|
- normalized caster key; and
|
|
- complete canonical source-reference set.
|
|
|
|
Build the caster key by applying `strings.Fields`, joining with one ASCII space,
|
|
and then applying `golang.org/x/text/cases.Fold`. This implements Unicode case
|
|
folding rather than locale-specific lowercasing. Make `golang.org/x/text` a
|
|
direct module dependency when importing it; it is already present indirectly.
|
|
Leave output caster text untouched. Do not remove punctuation, apply aliases,
|
|
or attempt entity resolution.
|
|
|
|
Compare source-reference sets only after the deterministic sorting and exact
|
|
de-duplication from Stage 1. Exact equality includes source identity and both
|
|
boundaries. Unknown names, empty evidence, or any invalid source reference make
|
|
a cast ineligible for duplicate grouping; the cast remains in output so the
|
|
configured normalize validators can decide its validity.
|
|
|
|
### Collapse behavior
|
|
|
|
- Retain the first input occurrence of each duplicate group and preserve stable
|
|
output order.
|
|
- The retained artifact is its normalized Stage 1 copy. Preserve that first
|
|
occurrence's caster, effect, and narrative verbatim.
|
|
- Do not union sources, combine prose, select a “better” occurrence, or use
|
|
adjacency/overlap as duplicate evidence.
|
|
- Preserve all casts that do not meet the complete duplicate identity.
|
|
|
|
Emit one `duplicate_spell_cast_collapsed` warning per collapsed group. Scope it
|
|
to the retained input cast (`spell_casts[R]`) and identify the retained input
|
|
index and removed input indices. Order group warnings by retained input index.
|
|
Display at most 20 removed indices and report the exact omitted count when more
|
|
exist.
|
|
|
|
Warning order for the complete normalizer is:
|
|
|
|
1. Walk input casts in input order. For each cast, emit its name warning, if
|
|
any, followed by its source-reference warning, if any.
|
|
2. Emit duplicate-group warnings ordered by retained input index.
|
|
|
|
Per-cast warnings remain present even when that input cast is later removed by
|
|
duplicate collapse; warning scopes deliberately refer to merged input indices.
|
|
|
|
### Stage 2 tests
|
|
|
|
Add table-driven and focused behavioral tests for:
|
|
|
|
- two- and three-member duplicate groups;
|
|
- caster case and whitespace normalization without output caster rewriting;
|
|
- first-occurrence retention, stable output ordering, and preservation of the
|
|
first occurrence's non-identity fields;
|
|
- distinct canonical spells, casters, or evidence sets remaining separate;
|
|
- exact evidence equality across differently ordered or duplicated input refs;
|
|
- unknown spells, empty evidence, and invalid refs never collapsing;
|
|
- no collapse based only on adjacent or overlapping evidence;
|
|
- no source union or prose combination;
|
|
- deterministic warning ordering and input-index semantics;
|
|
- the 20-index warning display bound and exact omitted count; and
|
|
- output/input slice independence under mutation checks.
|
|
|
|
Include an idempotence test: normalizing an already normalized successful result
|
|
does not change artifacts or emit new mutation warnings.
|
|
|
|
## Stage 3: D&D defaults and maintained configurations
|
|
|
|
Update the D&D registrar without changing global framework defaults.
|
|
|
|
### Registration and validator composition
|
|
|
|
- Add the spell normalizer package to the D&D registrar and register its
|
|
normalize-stage variant.
|
|
- Register this default validator chain for stage `normalize`, module
|
|
`dnd/spells`, in this exact order: `json`, `schema`,
|
|
`extract/dnd/spells/shape`, `extract/dnd/spells/catalog`,
|
|
`extract/dnd/spells/source_refs`, and
|
|
`extract/dnd/spells/source_relatedness`.
|
|
- Reuse the existing validator implementations and selectors. Do not rename
|
|
them merely because they are now also used after normalization.
|
|
- Leave the extractor's default validator chain unchanged.
|
|
- Preserve explicit pipeline validator overrides as authoritative; defaults are
|
|
used only when configuration does not supply an override.
|
|
- Leave the global/default `noop` normalizer behavior unchanged for all other
|
|
pipelines and lanes.
|
|
|
|
The relatedness validator may conservatively warn when a transcript uses an
|
|
alias while the artifact contains the canonical spelling. That warning is
|
|
non-fatal and does not justify changing validator behavior in this scope.
|
|
|
|
### Maintained examples
|
|
|
|
Update maintained D&D spell configurations as follows:
|
|
|
|
- The embedded-base example explicitly selects `normalize: dnd/spells` and
|
|
requires no normalize-stage catalog reference.
|
|
- The overlay example independently binds the same catalog document under
|
|
`pipelines.<pipeline>.artifacts.<lane>.normalize.references.spell_catalog` as
|
|
it does under extraction. Do not rely on implicit cross-stage reference
|
|
sharing.
|
|
|
|
Ensure both maintained examples pass strict configuration loading and effective
|
|
pipeline materialization. Do not add an LLM profile to the deterministic
|
|
normalizer.
|
|
|
|
### Stage 3 tests
|
|
|
|
Add or update registration and configuration tests to prove:
|
|
|
|
- `dnd/spells` resolves as the typed spell normalizer;
|
|
- its exact normalize default validator order is stable;
|
|
- existing extract defaults remain unchanged;
|
|
- explicit normalize validator overrides remain authoritative;
|
|
- the base example works without `spell_catalog` at normalize; and
|
|
- the overlay example binds its catalog independently to both extraction and
|
|
normalization.
|
|
|
|
Prefer asserting semantic scope and values over global literal counts that
|
|
become brittle whenever another prepared component contributes catalog
|
|
identity.
|
|
|
|
## Stage 4: Assembled execution, provenance, and checkpoint identity
|
|
|
|
Add an offline assembled-pipeline test using deterministic or fake upstream
|
|
output. The fixture must exercise records crossing chunk/merge boundaries
|
|
without an external LLM call. Verify that the normalizer:
|
|
|
|
- receives merged spell casts;
|
|
- canonicalizes a catalog alias or spelling variant;
|
|
- normalizes reference order;
|
|
- collapses only an exact supported duplicate;
|
|
- leaves different evidence as a distinct event;
|
|
- returns warnings through the existing run result and manifest path; and
|
|
- produces output accepted by the default normalize validator chain.
|
|
|
|
Add a separate assertion that an explicit normalize validator override remains
|
|
in force in an assembled pipeline.
|
|
|
|
Extend checkpoint and provenance coverage to show:
|
|
|
|
- the normalizer contributes its independently scoped `effective_catalog`
|
|
fingerprint;
|
|
- catalog-backed extractor and validators continue to contribute their own
|
|
independently scoped identities;
|
|
- all components materialized from the same reference set report the same
|
|
semantic digest;
|
|
- changing effective catalog semantics changes run/checkpoint identity and
|
|
prevents normalize-checkpoint reuse;
|
|
- semantically equivalent catalog material retains the semantic digest under
|
|
the existing catalog rules;
|
|
- raw overlay reference provenance remains present independently of the
|
|
semantic catalog fingerprint; and
|
|
- the manifest contains the normalizer's base ID, digest, and overlay IDs.
|
|
|
|
Do not change checkpoint schema, layout, compatibility, or the component
|
|
fingerprint framework. If an existing test assumes an exact total fingerprint
|
|
count, replace that brittle assertion with scoped name/value assertions while
|
|
retaining checks for LLM-profile and reference provenance.
|
|
|
|
## Stage 5: Documentation and evaluation fixtures
|
|
|
|
Update current-state documentation only as behavior becomes implemented:
|
|
|
|
- `docs/config.md`: document `dnd/spells` as a normalize selector, its optional
|
|
stage-local `spell_catalog` reference, and its default validator chain.
|
|
- `docs/internal/modules.md` and `docs/internal/overview.md`: add the typed
|
|
normalizer, deterministic behavior, catalog dependency, metadata, and
|
|
checkpoint identity where each document's current-behavior scope requires it.
|
|
- `docs/integrations/dnd-spell-artifacts.md`: document canonical-name behavior,
|
|
exact source-reference normalization, conservative duplicate identity,
|
|
preservation rules, and warning reason codes.
|
|
- Maintained examples: ensure their comments explain base-only versus
|
|
independently bound overlay behavior.
|
|
- `docs/roadmap/dnd-spell-normalization.md`: mark implemented acceptance items
|
|
complete while leaving any unevaluated human-review claims explicitly
|
|
pending.
|
|
- `docs/roadmap/future.md`: remove or revise only entries made obsolete by this
|
|
completed feature. Leave deferred LLM, fuzzy, generic-normalizer, and richer
|
|
reconciliation work in the future roadmap.
|
|
|
|
Do not create
|
|
`internal/modules/dnd/shared/assets/prompts/common-dnd-spells.md` in this
|
|
implementation. No component in scope makes an LLM call, so adding the prompt
|
|
would create an unused contract and would not improve backend cache reuse.
|
|
|
|
Add a compact deterministic fixture set covering the normalizer's accepted
|
|
input/output behavior. Do not fabricate claims about real transcript quality or
|
|
make paid or network LLM calls. If an approved human-reviewed transcript corpus
|
|
is available locally and repository policy permits its use, record aggregate
|
|
observations in the feature roadmap without committing sensitive transcript
|
|
content. Otherwise leave the qualitative evaluation item pending and state
|
|
why.
|
|
|
|
## Verification gate
|
|
|
|
At the end of every stage, run the narrowest directly affected package tests.
|
|
Before declaring the implementation complete, run:
|
|
|
|
```text
|
|
git diff --check
|
|
go test ./...
|
|
go vet ./...
|
|
go build ./cmd/notarius
|
|
```
|
|
|
|
Run targeted race detection for the affected execution surfaces:
|
|
|
|
```text
|
|
go test -race ./internal/modules/dnd/... ./internal/framework/pipeline ./internal/cli ./internal/modules/integration
|
|
```
|
|
|
|
All tests must remain offline and deterministic. This targeted command
|
|
supplements, rather than changes, the repository-wide validation contract in
|
|
`docs/development.md`.
|
|
|
|
Review the final diff for accidental framework expansion, schema changes,
|
|
prompt assets, generated output, local paths, or unrelated worktree edits.
|
|
|
|
## Open Questions
|
|
|
|
None. The feature roadmap and the decisions above are sufficient to implement
|
|
the work without further product or architecture choices.
|