# Scrum review — c3_materializer — opus (opencode/claude-opus-4-7)

**Latency:** 21064ms · **Tokens:** 6 in / 1133 out · **Date:** 2026-05-02

---

SEVERITY: BLOCK
WHERE:    internal/materializer/materializer.go:processSource
WHAT:     loadSeenHashes is called BEFORE the output directory is created, but the seen-hash dedup must read outPath which lives in outDir; on first run this is fine (file doesn't exist), but skipsToWrite/rowsToWrite are accumulated in memory and flushed only at end — meaning a mid-run crash loses all the writes AND the next run re-processes them as new rather than dedup. Actually re-reading: writes are buffered then appended atomically; OK. Retracting. (No finding.)

SEVERITY: WARN
WHERE:    internal/materializer/materializer.go:178
WHAT:     `strings.TrimPrefix(p, opts.Root+string(os.PathSeparator))` won't strip when Root has a trailing slash or differs in cleanliness, leaking absolute paths into receipt's output_files.
WHY:      filepath.Join may not preserve a user-supplied trailing separator; safer to use filepath.Rel(opts.Root, p).

SEVERITY: WARN
WHERE:    internal/materializer/materializer.go:loadSeenHashes
WHAT:     Dedup only loads sig_hashes from today's partition file; identical rows reprocessed on a different RecordedAt date will be written again rather than deduped.
WHY:      isoDatePartition derives outDir from RecordedAt, so cross-day re-runs produce duplicate evidence rows for the same source line — breaks the idempotency contract claimed in the commit message.

SEVERITY: WARN
WHERE:    internal/materializer/materializer.go:processSource
WHAT:     `lineOffset := int64(-1)` then `lineOffset++` before the empty-line `continue` means RowsRead matches non-empty lines but offsets in skip records skip empty lines too, while the TS port likely tracks raw line numbers.
WHY:      Operators correlating skip line_offset to source files will be off by the count of preceding blank lines.

SEVERITY: WARN
WHERE:    internal/materializer/transforms.go:outcomesTransform
WHAT:     Test asserts `rec.ValidationResults["gap_signals"].(int64)` but the map is built with `numInt` returning int64 as `any`; works in-process, but JSON round-trip will produce float64 — fragile contract.
WHY:      ValidationResults is `map[string]any`; downstream consumers reading the marshaled JSON will see float64, mismatching the test's type assertion expectations.

SEVERITY: INFO
WHERE:    internal/materializer/transforms.go:buildRetrievedContext
WHAT:     Local variable shadows builtin `any` type alias.
WHY:      `any := false` shadows the universe-scope `any` (alias for interface{}); compiles but confuses readers and tooling.

SEVERITY: INFO
WHERE:    scripts/materializer_smoke.sh:34
WHAT:     `date -u +'%Y/%m/%d'` race: smoke test could straddle UTC midnight between materializer run and assertion.
WHY:      Flake risk; pin RecordedAt via env or compute once.

VERDICT: ship-with-fixes — cross-day dedup gap and absolute-path leak warrant follow-up.
