// Package materializer ports scripts/distillation/transforms.ts + // build_evidence_index.ts to Go. Source rows in data/_kb/*.jsonl are // transformed into EvidenceRecord rows under data/evidence/YYYY/MM/DD/. // // Per ADR-001 #4: port LOGIC, not bit-identical reproducibility — but // on-wire JSON layout matches the TS shape so Bun and Go runs stay // interchangeable for tooling that reads either output. package materializer import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "sort" ) // CanonicalSha256 returns the hex SHA-256 of `obj` after sorting all // object keys recursively. Matches the TS canonicalSha256 in // auditor/schemas/distillation/types.ts so a row hashed by either // runtime gets the same sig_hash. // // Determinism contract: identical input → identical hash, regardless // of the producer's serialization order. func CanonicalSha256(obj any) (string, error) { ordered := orderKeys(obj) buf, err := json.Marshal(ordered) if err != nil { return "", fmt.Errorf("canonical marshal: %w", err) } sum := sha256.Sum256(buf) return hex.EncodeToString(sum[:]), nil } // orderKeys recursively sorts every map's keys. For arrays we keep the // element order (arrays are inherently ordered). Scalars pass through. func orderKeys(v any) any { switch t := v.(type) { case map[string]any: keys := make([]string, 0, len(t)) for k := range t { keys = append(keys, k) } sort.Strings(keys) out := make(orderedMap, 0, len(keys)) for _, k := range keys { out = append(out, kvPair{Key: k, Value: orderKeys(t[k])}) } return out case []any: out := make([]any, len(t)) for i, e := range t { out[i] = orderKeys(e) } return out default: return v } } // orderedMap preserves insertion order on JSON marshal. We populate it // in sorted-key order so the produced bytes are stable. type orderedMap []kvPair type kvPair struct { Key string Value any } func (om orderedMap) MarshalJSON() ([]byte, error) { if len(om) == 0 { return []byte("{}"), nil } out := []byte{'{'} for i, kv := range om { if i > 0 { out = append(out, ',') } k, err := json.Marshal(kv.Key) if err != nil { return nil, err } out = append(out, k...) out = append(out, ':') v, err := json.Marshal(kv.Value) if err != nil { return nil, err } out = append(out, v...) } out = append(out, '}') return out, nil }