// Cthulhu screen reader. // Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package parity import ( "encoding/csv" "errors" "fmt" "io" ) var ErrSchema = errors.New("parity ledger schema") const ( StatusPending = "pending" StatusInProgress = "in-progress" StatusImplemented = "implemented" StatusAuditRequired = "audit-required" StatusAcceptedOmission = "accepted-omission" ) var allowedStatuses = map[string]struct{}{ StatusPending: {}, StatusInProgress: {}, StatusImplemented: {}, StatusAuditRequired: {}, StatusAcceptedOmission: {}, } var columns = []string{ "id", "python_source", "subsystem", "behavior", "owner", "go_destination", "status", "automated_evidence", "x11_evidence", "wayland_evidence", "speech_evidence", "braille_evidence", "notes", } type Record struct { ID string PythonSource string Subsystem string Behavior string Owner string GoDestination string Status string AutomatedEvidence string X11Evidence string WaylandEvidence string SpeechEvidence string BrailleEvidence string Notes string } type ParseError struct { Row int Err error } func (parseError *ParseError) Error() string { return fmt.Sprintf("row %d: %v", parseError.Row, parseError.Err) } func (parseError *ParseError) Unwrap() error { return parseError.Err } func Load(reader io.Reader) ([]Record, error) { csvReader := csv.NewReader(reader) csvReader.FieldsPerRecord = len(columns) csvReader.TrimLeadingSpace = true header, err := csvReader.Read() if err != nil { return nil, &ParseError{Row: 1, Err: fmt.Errorf("%w: read header: %w", ErrSchema, err)} } for index, column := range header { if column != columns[index] { return nil, &ParseError{ Row: 1, Err: fmt.Errorf("%w: column %d is %q, want %q", ErrSchema, index+1, column, columns[index]), } } } ids := make(map[string]struct{}) var records []Record for row := 2; ; row++ { fields, readErr := csvReader.Read() if errors.Is(readErr, io.EOF) { break } if readErr != nil { return nil, &ParseError{Row: row, Err: fmt.Errorf("read record: %w", readErr)} } record := Record{ ID: fields[0], PythonSource: fields[1], Subsystem: fields[2], Behavior: fields[3], Owner: fields[4], GoDestination: fields[5], Status: fields[6], AutomatedEvidence: fields[7], X11Evidence: fields[8], WaylandEvidence: fields[9], SpeechEvidence: fields[10], BrailleEvidence: fields[11], Notes: fields[12], } if err := validate(record, row, ids); err != nil { return nil, err } records = append(records, record) } return records, nil } func validate(record Record, row int, ids map[string]struct{}) error { for field, value := range map[string]string{ "id": record.ID, "python_source": record.PythonSource, "subsystem": record.Subsystem, "behavior": record.Behavior, "status": record.Status, } { if value == "" { return &ParseError{Row: row, Err: fmt.Errorf("%w: %s is empty", ErrSchema, field)} } } if _, exists := allowedStatuses[record.Status]; !exists { return &ParseError{ Row: row, Err: fmt.Errorf("%w: invalid status %q", ErrSchema, record.Status), } } if _, exists := ids[record.ID]; exists { return &ParseError{ Row: row, Err: fmt.Errorf("%w: duplicate id %q", ErrSchema, record.ID), } } if record.GoDestination == "" && record.Status != StatusAcceptedOmission { return &ParseError{ Row: row, Err: fmt.Errorf("%w: go_destination is empty for status %q", ErrSchema, record.Status), } } ids[record.ID] = struct{}{} return nil } func FormatStatuses() string { return "pending, in-progress, implemented, audit-required, accepted-omission" }