Complete the Stage 23 refactoring audit

This commit is contained in:
2026-08-12 17:43:35 +00:00
parent 91e7e5f321
commit fa19452dec

View File

@@ -1,6 +1,6 @@
# Repository Audit Ledger
Status: In progress; Stages 1-21 complete.
Status: In progress; Stages 1-23 complete.
This temporary roadmap document is the evidence ledger for the staged audit
defined by [the audit plan](audit-plan.md). It records audit evidence and
@@ -177,6 +177,25 @@ semantics; dense configuration/domain tables, realistic filesystem tests,
small external-boundary fakes, and representative app/CLI workflows earn their
cost. No complete-output golden snapshots or oversized fixture framework was
found.
Stage 22 found one medium-severity efficiency defect because every Weather API
collection discards a current-conditions warmup response, fetches that source
again, and serializes all eight otherwise independent source requests. It also
found one low-severity replacement cost because an existing comparison bundle
is fully recognized four times, causing `4N+16` complete reads for `N`
successful reports. Service-sized derivation, fixed registries, template
parsing, fixed batch sequencing, and ordinary profile fan-out are otherwise
proportionate to their current workloads.
Stage 23 found seven low-severity structural issues: daypart-key normalization and
Distributor template parsing each have two policy implementations; report
metadata and render inputs retain parallel authorities and unused template
surfaces; fact requirements span string constants, registry declarations, and
availability switches; generate and comparison repeat report-date policy at
four sites; several internal compatibility surfaces have no production
consumer; and one duplicate capitalization path corrupts non-ASCII daypart
labels. Report-specific generated types/builders, explicit prompt allowlist
projections, package-local safety copies and path diagnostics, config-to-
briefing validation, top-level workflow orchestration, and the current narrow
resolved-value clone remain intentionally explicit.
Subsystem conclusions and final disposition remain pending the later stages.
## Baseline Metadata
@@ -326,7 +345,7 @@ inventory commands, graph index refresh, and graph architecture inspection.
| 20 | Audit test risk coverage and ownership | Complete |
| 21 | Audit test durability, duplication, and maintenance cost | Complete |
| 22 | Audit cross-cutting efficiency and complexity | Complete |
| 23 | Audit cross-cutting refactoring and deduplication opportunities | Pending |
| 23 | Audit cross-cutting refactoring and deduplication opportunities | Complete |
| 24 | Audit documentation coherence and executable contracts | Pending |
| 25 | Run dynamic robustness and final diagnostic validation | Pending |
| 26 | Verify, consolidate, and triage findings | Pending |
@@ -360,7 +379,7 @@ with evidence about meaningful risks, test ownership, gaps, and duplication.
| Test risk coverage and ownership | Repository-wide suite | 20 | Partly insufficient. Consequential contracts have narrow owners and representative cross-boundary workflows; every identified missing regression is already attached to the production or test-gap finding for the defect it would catch. Coverage diagnostics found no additional unowned critical branch. Accepted omissions are thin entry-point/delegation wrappers, defensive accessors, and dormant helpers already routed under `AUD-001`; broad duplication and durability remain Stage 21. |
| Test durability, duplication, and maintenance cost | Repository-wide suite | 21 | Partly insufficient. Most tests assert stable behavior with direct fixtures and appropriately narrow fakes. `AUD-039`, `AUD-061`, and `AUD-063` identify duplicated or implementation-coupled assertions; `AUD-062` identifies avoidable production waits; `AUD-064` identifies incomplete failure cleanup. Each consolidation retains a named behavioral owner. |
| Cross-cutting efficiency | Graph metrics, workflow traces, and focused latency/allocation diagnostics | 22 | Partly insufficient. Fixed-size registries, service-sized derivation, template parsing, batch composition, and ordinary profile fan-out are proportionate. `AUD-065` records one redundant Weather API request plus serial independent source fetches; `AUD-066` records repeated full comparison-bundle reads during replacement. |
| Cross-cutting maintainability | Graph metrics plus focused structural review | 23 | Pending |
| Cross-cutting maintainability | Graph similarity, change history, caller traces, and focused structural review | 23 | Partly insufficient. `AUD-067` through `AUD-073` identify duplicated canonicalization/template/date/requirement policy, parallel metadata authority, dormant internal surfaces, and unsafe duplicate capitalization. Important report, prompt-curation, safety-copy, validation, and orchestration similarities remain explicit under `RET-053` through `RET-058`. |
| Documentation and executable-contract coherence | Canonical documents, code, schemas, templates, examples | 24 | Pending |
| Dynamic robustness and diagnostic checks | Repository-wide deterministic checks | 25 | Pending |
@@ -2897,6 +2916,298 @@ with evidence about meaningful risks, test ownership, gaps, and duplication.
- Related findings: `AUD-038`, `AUD-053`, `AUD-055`
- Remediation reference: pending
### AUD-067: Daypart key canonicalization has two independent owners
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: duplication
- Area: `internal/briefing.normalizedKey` and
`internal/generatedtext.normalizedDaypartKey`
- Evidence: Graph fingerprint comparison found two exact 17-line production
implementations: trim and lowercase the name, preserve Unicode letters and
digits, collapse every other run to one underscore, and trim edge
underscores. Briefing introduced its copy in `7b760a0`; generated-text added
a separately named copy later in `ddda424`. Briefing uses the key to store
daypart module values, while generated-text recomputes it from derived facts
to recover configured order. The same identity is also the collision domain
whose missing validation causes `AUD-024`.
- Contract at risk: One accepted daypart name must have one canonical identity
from configuration through module storage and render ordering.
- Impact: A future Unicode, punctuation, casing, or collision-policy change
must update two packages identically. Drift can leave a module entry present
under one key but unrecognized by ordered rendering under the other, causing
misplaced or omitted daypart output even after collision validation is
added.
- Recommendation: Give canonical daypart identity to the existing daypart
domain owner, preferably beside `forecast.DaypartDefinition`, and make
configuration collision checks, briefing keys, and generated-text ordering
call that one helper. Keep display labels separate from canonical roles as
required by `AUD-025`; do not introduce a generic string-normalization
package.
- Test implications: One focused domain table should own blank edges,
punctuation runs, casing, Unicode, and collision equivalence. Config should
test rejection consequences, while briefing/generated-text retain one
composition case each rather than repeating the normalization table.
- Validation: Every accepted name produces the same key at validation,
snapshot, prompt, and render boundaries; current default keys and rendered
ordering remain unchanged.
- Related findings: `AUD-024`, `AUD-025`
- Remediation reference: pending
### AUD-068: Distributor single and batch templates duplicate one parser
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: duplication
- Area: `internal/config.renderDistributorTemplate` and
`renderDistributorBatchTemplate`
- Evidence: The two 27-line functions are fingerprint-identical except for
their value type and variable-to-value callback. Both independently own the
brace scanner and the exact unclosed, empty, unknown, and unopened-variable
diagnostics. Batch support copied the second grammar in `32060bd`; the file
has seven historical changes, and config tests repeat malformed/unknown
template cases for the two families. Typed public wrappers and allowed
variable sets are legitimately distinct, but syntax is not.
- Contract at risk: Single-report and batch notification templates use one
interpolation grammar and must reject malformed syntax consistently.
- Impact: Any escaping, validation, diagnostic, or parser correction requires
synchronized edits and duplicated tests. Drift would make configuration
accept a template for one notification mode and reject the same syntax for
the other without a domain reason.
- Recommendation: Extract one private parser that accepts the diagnostic name
and a typed-family resolver callback returning a value and whether the
variable is allowed. Retain distinct value structs, allowed-variable sets,
nonempty-result rules, and public rendering functions; no general template
framework or new package is warranted.
- Test implications: Give the shared parser one malformed-syntax table. Keep
small single/batch tests for their variable vocabularies, value mappings,
nonempty requirements, report-path validation, and public error context.
- Validation: Every current valid template renders identically, current
malformed inputs retain their actionable field names, and a parser mutation
is detected once rather than by copied syntax suites.
- Related findings: none
- Remediation reference: pending
### AUD-069: Preparation retains parallel metadata authorities and unused render surfaces
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: architecture
- Area: `internal/briefing.BuildMetadata`, `buildMetadataModule`,
`internal/app.preparedReport`, and
`internal/generatedtext/*RenderContext`/`*TemplateModules`
- Evidence: `BuildMetadata` and `buildMetadataModule` independently project the
same run ID, report ID, variant, prompt ID, generation time, units, timezone,
valid period, location, and source-warning facts. Preparation retains the
broader `briefing.Metadata` beside a snapshot containing `MetadataModule`,
then projects the former again into `promptinput.Metadata`. Render contexts
receive the outer metadata for display identity while also exposing the
decoded module metadata. All four contexts additionally expose complete
`Collected` and `Derived` values. Repository template search found no use of
`.Collected`, `.Derived`, or `.Modules.Metadata`; the duplicated identity
paths already permit the conflicts in `AUD-040`. The broad metadata path
predates the later module and render-context paths, rather than resulting
from one shared projection.
- Contract at risk: Prompt metadata, metadata stanza content, and displayed
report timing must derive from one prepared identity, while templates should
receive only fields they are authorized to render.
- Impact: A metadata addition or correction can require parallel builders,
application projection, snapshot fixtures, and context fixtures. More
importantly, callers can supply conflicting outer and stanza identities and
templates gain accidental access to raw deterministic facts that current
assets deliberately do not use.
- Recommendation: After resolving `AUD-040`, establish one smallest prepared
metadata value and derive prompt-package metadata, the metadata stanza, and
report display context from it through named projections. Remove unused
`Collected`/`Derived` fields and the unused template-module metadata field;
pass derived facts privately only while constructing ordered daypart views.
Keep prompt-package ownership in `promptinput` and module-value ownership in
`briefing` rather than replacing them with one cross-layer mega-struct.
- Test implications: Preserve one preparation test proving all three outputs
share identity and source-warning values, module tests for prompt-safe stanza
content, generated-text tests for report display fields and daypart order,
and template tests for unchanged Markdown. Delete fixture data that protects
only unreachable template fields.
- Validation: Current prompt YAML and rendered Markdown remain semantically or
byte equivalent as contractually required, conflicting identity inputs are
impossible or rejected, and template contexts contain no unused complete
fact bundle.
- Related findings: `AUD-029`, `AUD-040`, `AUD-063`
- Remediation reference: pending
### AUD-070: Fact requirement policy spans constants, declarations, and switches
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: architecture
- Area: `internal/module.FactRequirement`,
`internal/briefing.defaultModuleDefinitions`, `collectedFactAvailable`, and
`derivedFactAvailable`
- Evidence: Adding a module fact prerequisite requires a string constant in
`module`, use in one or more registry definitions, and a matching branch in
one of two availability switches in `briefing`. `FactRequirement` is a
string, and `NewModuleRegistry` validates neither known requirement names nor
collected-versus-derived placement; an unknown value is accepted and later
silently treated as unavailable. `modules.go` has changed 21 times as source,
derived, planning, SPC, and prompt-export modules were added, demonstrating
continuing evolution of this three-part policy. Focused default-composition
tests currently keep built-ins aligned but do not make one representation
authoritative.
- Contract at risk: Every declared prerequisite must have one known
availability predicate, and registry construction should reject rather than
defer unknown policy vocabulary.
- Impact: A new or renamed requirement can compile and validate into a registry
yet make its module always missing at runtime. The separate collected and
derived slices also allow a valid constant in the wrong category, producing
the same misleading absence without identifying the declaration error.
- Recommendation: Move requirement descriptors to `briefing`, where facts are
visible, and define each name, fact category, and availability predicate
once. Let definitions refer to those descriptors or validate identifiers
through one registry-owned map during `NewModuleRegistry`. Keep each
module's actual requirement list and missing-data behavior explicit in the
definition table.
- Test implications: One descriptor table should prove every identifier has a
predicate and correct category; constructor tests should reject unknown or
misplaced requirements. Retain behavior tests for materially distinct
missing-source policies, not one availability test per constant.
- Validation: Every default definition constructs, unknown/miscategorized
requirements fail at registry creation, and existing omit/empty/error results
remain unchanged for all reports.
- Related findings: none
- Remediation reference: pending
### AUD-071: Generate and comparison repeat report-date policy at four sites
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: duplication
- Area: `internal/cli.parseGenerateFlags`, `parseComparisonFlags`,
`Runner.resolveGenerateAction`, and `Runner.resolveComparisonAction`
- Evidence: Both flag parsers independently decide that Daily and Today accept
`--date`. Both action resolvers then independently require it for Daily,
default it to the local current date for Today, parse it in the configured
timezone, and leave Tomorrow/Hourly without it. Generate acquired Today
policy in `4a02389`; comparison copied the action policy later in `b489c56`.
`root.go` has 36 historical changes, and
`TestResolveComparisonActionMatchesReportDatePolicies` exists specifically to
detect disagreement after the fact. Action-specific request construction and
diagnostics differ, but report/date capability does not.
- Contract at risk: The same report accepts, requires, defaults, and parses the
same date semantics regardless of whether one profile or several profiles
execute it.
- Impact: Adding a report variant or changing date semantics requires four
coordinated branches plus agreement tests. A missed site can expose a flag
that later fails, reject a valid comparison only, or choose different civil
dates for otherwise equivalent actions.
- Recommendation: Add one private CLI report-date policy descriptor and one
resolver that returns the effective local date from report kind, supplied
text, timezone, and clock. Let each parser consult `AcceptsDate` and each
action wrap missing-date diagnostics with its command name. Keep flag parsing
and request construction in CLI rather than moving them into the report
registry.
- Test implications: Give the shared policy a four-report table for acceptance,
required/default/forbidden behavior, invalid dates, and timezone-relative
Today defaults. Retain one thin generate and comparison wiring case each.
- Validation: Generate and compare preserve current accepted flags, errors, and
resolved dates for all four reports, and no duplicated report-kind switch
remains in their date path.
- Related findings: `AUD-009`
- Remediation reference: pending
### AUD-072: Dormant internal compatibility surfaces have no workflow owner
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: architecture
- Area: `internal/promptinput.LoadYAML`,
`internal/forecast.{DifferenceAtLeast,CrossesAtOrAbove,CrossesBelow}`,
`internal/weatherdata.AlertRun.Raw`, and the normalized-JSON return from
`internal/generatedtext` validators
- Evidence: Production caller traces find no caller for `LoadYAML`; its
round-trip tests are the only consumer, while normal execution builds a
package and marshals YAML directly. The three exported forecast threshold
helpers are called only by `TestThresholdHelpers`. The Weather API adapter
copies every full alerts response into `AlertRun.Raw`, but no production or
test code reads that field; individual raw alert items and source hashes own
the needed payload/provenance. All four generated-text validators marshal and
return canonical normalized JSON, but the sole production handler caller
assigns that result to `_`; normalization behavior is otherwise consumed
through the typed value used for rendering. The persistence half of the same
obsolete prompt-input surface is already `AUD-001`, and hardening the unused
loader alone is `AUD-032`.
- Contract at risk: Internal package APIs and stored values should describe the
active stateless pipeline, not imply compatibility or persistence contracts
with no workflow owner.
- Impact: These surfaces retain tests, error branches, allocations, JSON shape,
and future design constraints without protecting current output. They can
invite a new caller to rely on accidental semantics such as YAML round-trip
loading, threshold helpers with no domain owner, a duplicate whole alert
envelope, or canonical JSON that the renderer never observes.
- Recommendation: Retire `LoadYAML` with `promptinput.Save`; remove the unused
threshold helpers and their representation-only test; remove `AlertRun.Raw`
and adapter assignments; and simplify the internal validator/handler result
to the typed value plus error. If a concrete tooling consumer is identified
before remediation, document and test its narrow contract instead of
retaining all surfaces speculatively.
- Test implications: Preserve Build/Validate/MarshalYAML execution tests,
forecast behavior that uses real threshold decisions, alert item parsing and
provenance hashes, typed normalization semantics, schema agreement, and all
normal generate/batch/comparison workflows. Remove only tests whose sole
subject is the retired surface; `AUD-039` still owns duplicated validator
semantics.
- Validation: Graph and text searches show no remaining caller or field
reference, prompt YAML and rendered reports remain unchanged, alert source
hashes still cover accepted payloads, and the repository suite passes.
- Related findings: `AUD-001`, `AUD-032`, `AUD-038`, `AUD-039`
- Remediation reference: pending
### AUD-073: Duplicate capitalization helpers disagree on Unicode safety
- Stage: 23
- Status: candidate
- Severity: low
- Confidence: high
- Category: duplication
- Area: `internal/briefing.titleWord` and `sentenceCase`
- Evidence: `sentenceCase` trims, converts the string to runes, and uppercases
the first rune. `titleWord` instead uppercases `value[:1]` and appends
`value[1:]`, splitting any multibyte first rune. Its four production callers
place configured daypart names into derived summaries, workday concerns,
outdoor planning notes, and late-day watch prose. Configuration accepts
Unicode nonblank daypart names, so a name beginning with `é`, `ø`, or another
multibyte letter reaches this path and produces replacement/invalid text.
Existing tests use ASCII default names only.
- Contract at risk: Accepted Unicode display labels must remain valid and
readable when interpolated into deterministic briefing prose.
- Impact: A valid localized or operator-chosen daypart name can be corrupted in
several prompt-facing planning fields. Two helpers that express the same
first-character presentation policy also invite future call sites to choose
the unsafe implementation accidentally.
- Recommendation: Replace both with one private rune-safe first-letter helper,
choosing trimming explicitly at each call boundary if their whitespace
contracts differ. Keep canonical identity normalization separate under
`AUD-067`; this helper is display-only and must not assign semantic roles.
- Test implications: Add one small table for empty, ASCII, multibyte lowercase,
already-uppercase, and leading-space behavior. Retain one assembled planning
assertion proving a Unicode daypart label survives in prompt-facing prose.
- Validation: All current ASCII output is unchanged, every accepted Unicode
label remains valid UTF-8 with its first rune capitalized, and only one
capitalization implementation remains.
- Related findings: `AUD-024`, `AUD-025`, `AUD-067`
- Remediation reference: pending
## Retained Decisions
### RET-001: Keep the application package as the explicit composition owner
@@ -3515,6 +3826,77 @@ observable clone contention, or a larger dynamic batch as triggers for a
bounded worker design rather than inferring a defect from goroutine or graph
counts alone.
### RET-053: Keep report-specific generated types and thin builders explicit
Fingerprint analysis found exact or near-exact Daily/Tomorrow context builders,
four catalog type-dispatch adapters, three planning-module wrappers, and
parallel report definitions. Their similarity follows four intentionally
distinct report contracts: each public generated type, schema/template pair,
planning stanza, report ID, and typed context must remain independently
selectable and type-checked. Existing private day-style helpers already share
the mechanics that have one policy owner. Retain the thin variants rather than
introducing reflection, a generic report mega-type, or callbacks that obscure
which planning fields each report supports. `AUD-039` and `AUD-063` own test
duplication without erasing these production distinctions.
### RET-054: Keep rich-to-prompt projections as explicit allowlists
Current conditions, hourly periods, and derived dayparts copy many fields from
rich module values into narrower prompt structs. That field-by-field code is
deliberate curation: omission of template-only helpers, source detail, and
unneeded values is part of the prompt security and authority boundary under
`RET-017`. Code generation, reflection, embedding, or serialization through
the rich struct would make accidental prompt expansion harder to review.
Retain named projection functions and focused omission tests. Extract only
small semantic subprojections when multiple prompt types genuinely share the
same allowlist and change history.
### RET-055: Keep tiny safety copies and path checks package-local
Exact fingerprints found three ten-line string-map copy helpers in Promptkit,
prompt-debug, and neutral execution packages, plus similar absolute-path
checks in app and comparison. The copies sit at separate mutation/security
boundaries and their names state which owner is taking a copy. The path checks
attach different diagnostics and feed substantially different publication
policies. A generic utility package would save little code while coupling
unrelated contracts. Retain these local helpers; share only if a common type or
policy owner emerges, not because the mechanics happen to match.
### RET-056: Keep configuration validation against the briefing registry
`internal/config` imports `internal/briefing` to normalize typed module options
and validate report compositions against the same definitions used at runtime.
This edge prevents configuration from copying module IDs, option shapes,
supported-report rules, and stanza policy. Moving a second descriptor table
into config would create the duplication Stage 23 is meant to remove, while a
new abstraction package would split the current owner without another
consumer. Retain the dependency; narrow the requirement vocabulary within
briefing under `AUD-070` without moving runtime facts into config.
### RET-057: Keep workflow orchestration visibly distinct
Generate, fixed batch, and comparison share phases such as resolution,
inspection, collection, preparation, execution, and publication, but the
ordering and failure contracts differ materially. Batch validates all outputs,
continues independent report failures, publishes sequentially, and gates one
notification. Comparison validates explicit profiles, prepares once, executes
concurrently, and publishes one partial-capable transaction without
notification. Extracting a common workflow framework would hide those state
machines. Retain top-level guarded pipelines and consider only phase-local
helpers after `AUD-051`, `AUD-058`, `AUD-065`, and `AUD-066` settle their
behavior.
### RET-058: Keep the resolved-value clone explicit for current option shapes
`cloneResolved` copies report slices and the only reference-bearing module
option, AFD sections. Every other accepted option is presently value-only, and
mutation-isolation tests cover the complete prepared boundary. Reflection or
JSON cloning would add runtime failure modes and obscure ownership to solve a
hypothetical future shape. Retain the explicit switch now. If a second
reference-bearing option is introduced, give module options an owner-defined
clone contract and make the registry/construction test prove every registered
shape participates.
## Open Questions
No Stage 1 open questions or unexplained baseline failures remain.
@@ -3909,6 +4291,25 @@ Stage 22 routed these investigation leads to their assigned later stages:
Stage 26 may consider a limit or worker bound only if operational evidence
establishes a larger supported workload.
Stage 23 routed these investigation leads to their assigned later stages:
- Stage 24 should reconcile docs that present `promptinput.Save`/`LoadYAML`,
normalized generated JSON, complete render-context facts, or template module
metadata as maintained surfaces if Stage 26 selects the retirement/narrowing
direction in `AUD-069` and `AUD-072`. Current-state documentation should not
be changed before remediation.
- Stage 25 should include a Unicode-leading daypart label in its broader
parser/configuration corpus and preserve the existing destination-race cases;
it should validate the findings rather than implement shared helpers.
- Stage 26 should consolidate `AUD-001`, `AUD-032`, and `AUD-072` into one
coherent dormant-surface decision; `AUD-024`, `AUD-025`, `AUD-067`, and
`AUD-073` into one daypart identity/display sequence; and `AUD-040` with
`AUD-069` so canonical metadata ownership precedes context narrowing.
- Refactoring for `AUD-065` should keep source-local collection results and one
deterministic merge point; refactoring for `AUD-066` should keep initial
rejection and commit-time authorization in their current safety owners.
Stage 26 must sequence those behavior fixes before extracting helpers.
## Stage Log
### Stage 1: Establish The Baseline And Audit Ledger
@@ -5136,3 +5537,65 @@ Stage 22 routed these investigation leads to their assigned later stages:
- Retained decisions: `RET-050` through `RET-052`.
- Open questions: the four leads recorded above are routed to their assigned
later stages.
### Stage 23: Audit Cross-Cutting Refactoring And Deduplication Opportunities
- Status: Complete.
- Scope reviewed: graph exact-fingerprint and semantic-similarity candidates;
repeated report/date policies, mappings, validation, path handling, result
and error projections, report and module variants, workflow stages, template
parsing, canonicalization, value copying, prompt curation, generated-text
APIs, render contexts, registry construction, fact requirements, dormant
compatibility surfaces, test ownership already established in Stage 21, and
production/test-file change history.
- Exclusions: no structural remediation was implemented; documentation
coherence remains Stage 24; adversarial/dynamic validation remains Stage 25;
consolidation, priority, and remediation sequencing remain Stages 26-27. No
production code or tests were changed.
#### Refactoring And Duplication Accounting
| Candidate | Shared behavior, drift/change evidence, and required stable boundary | Disposition |
| --- | --- | --- |
| Daypart canonical keys | Briefing and generated-text contain exact 17-line normalizers introduced in different commits. Both must map accepted configuration names to the same snapshot/render key; collisions are already consequential under `AUD-024`. | One domain owner under `AUD-067`; preserve display labels and semantic roles separately. |
| Distributor template grammar | Single and batch rendering contain exact 27-line brace parsers and duplicated malformed-input tests. Batch copied the grammar later; typed values and variable sets differ, syntax does not. | One private parser callback under `AUD-068`; retain typed public wrappers and family policies. |
| Prepared/report metadata | Two briefing builders repeat ten identity/context fields; app retains outer metadata plus a metadata stanza and projects prompt metadata again. Templates use neither complete facts nor module metadata exposed by all contexts. Existing conflict is `AUD-040`. | Establish one prepared identity and narrow projections under `AUD-069`; preserve prompt YAML and Markdown contracts. |
| Fact requirements | Names live in `module`, lists in briefing definitions, and availability in two switches; constructor accepts unknown or miscategorized strings. `modules.go` has 21 revisions across continued module growth. | Define descriptor and predicate once under `AUD-070`; keep per-module lists/missing policy explicit. |
| CLI report dates | Generate and comparison each repeat flag eligibility plus required/default/parse logic. Policies arrived in different commits, and a cross-action test now guards agreement in a 36-change file. | One private CLI descriptor/resolver under `AUD-071`; preserve action-specific errors and request types. |
| Dormant compatibility surfaces | YAML loading, three forecast threshold exports, complete alert envelopes, and canonical normalized generated JSON have no production consumer; some retain tests or runtime copies. | Retire together with related `AUD-001`/`AUD-032` work under `AUD-072`, unless Stage 26 identifies a concrete owner. |
| Capitalization | Rune-safe `sentenceCase` and byte-slicing `titleWord` express the same presentation mechanic; the latter has four prompt-facing callers and corrupts accepted multibyte labels. | One display-only rune-safe helper under `AUD-073`; canonical identity remains `AUD-067`. |
| Report variants | Daily/Tomorrow context builders are fingerprint-identical and catalog/planning wrappers are similar, but distinct types, schema/template pairs, IDs, and planning fields are intentional contracts. | Retain thin explicit variants under `RET-053`; existing private day-style helpers own shared mechanics. |
| Prompt projections | Rich current/hourly/daypart values require lengthy field copies into narrower prompt types. Omitted fields are an authority allowlist, not incidental mapping repetition. | Retain reviewable projections under `RET-054`; no reflection, embedding, or generated mega-mapper. |
| Maps and absolute paths | Three packages locally copy string maps; app/comparison perform similar absolute-clean checks. Copies protect separate mutation boundaries, while path errors and downstream policy differ. | Retain tiny local helpers under `RET-055`; no generic utility package. |
| Config and briefing registry | Config calls the runtime briefing registry for module IDs, option types, supported reports, and composition validation. | Retain under `RET-056`; duplicating a config descriptor would increase drift. |
| Workflow and result translation | Generate, batch, and comparison share phase names but differ in preflight scope, continuation, concurrency, transaction, notification, partial results, and safe CLI projection. | Retain visible state machines and boundary-specific result/error projections under `RET-057`; extract only local phases after open correctness findings settle. |
| Prepared clone | `cloneResolved` manually copies slices and the sole reference-bearing option; current mutation tests cover it. | Retain under `RET-058`; introduce an option-owned clone contract only when another reference-bearing shape appears. |
| Test fixtures and duplicate assertions | Stage 21 already identified the concrete cross-owner and implementation-coupled cases in `AUD-039`, `AUD-061`, and `AUD-063`; remaining local fixtures/tables protect distinct contracts. | Do not create a shared fixture framework or duplicate Stage 21 findings. |
#### Commands And Evidence
- Enumerated all indexed function nodes and grouped exact fingerprints, then
manually separated tests, thin report variants, local safety helpers, and
genuine shared policy. Used semantic graph search, symbol snippets, and
inbound traces for metadata, requirements, projections, date handling,
workflows, and dormant APIs.
- Used targeted text search where assets and field references are outside graph
coverage. Confirmed no template references `.Collected`, `.Derived`, or
`.Modules.Metadata`; no production caller consumes validator normalized JSON,
`AlertRun.Raw`, the three forecast thresholds, or `LoadYAML`.
- Reviewed history rather than inferring burden from line similarity alone.
`root.go` has 36 changes, `briefing/modules.go` 21, and
`config/notify_templates.go` seven. The two daypart normalizers and the two
CLI action policies were introduced in distinct commits; batch notification
copied the existing template grammar when that feature arrived.
- Reconciled every earlier Stage 23 lead: unused persistence/loader surfaces
join `AUD-072`; metadata and render-context breadth become `AUD-069`;
requirements become `AUD-070`; date policy becomes `AUD-071`; Unicode and
daypart identity become `AUD-067`/`AUD-073`; projections, config coupling,
workflow metrics, and the current clone remain retained decisions.
- Ran `go test ./...`, `go vet ./...`,
`go run ./cmd/weatherreporter --help`, and `git diff --check`; all passed.
- Findings: `AUD-067` through `AUD-073`.
- Retained decisions: `RET-053` through `RET-058`.
- Open questions: the four leads recorded above are routed to their assigned
later stages.