cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Merge branch 'main' into worktree-agent-aa352c3e491997bf4

Dami committed d9b2c7e
commit
d9b2c7e181e41b023880b0dc0fa34cf9d7d39a8b
parent
909d761740b4ca7998b1321f25168282cca96bfe
parent
23151feaef383a3a622c34c5223c43d5e606754b
author
Dami <dami.etoile@gmail.com>
20 files changed, +232 -69
ACLAUDE.md23++++
Mcmd/012/main.go4+-
Mcmd/012/telemetry.go16---
Mdocs/architecture.md2+-
Mdocs/extending.md7+-
Mdocs/jev.md2+-
Mdocs/testing.md6+
Minternal/fileio/wk1.go1-
Minternal/fileio/xlsxwrite.go10--
Minternal/functions/reader.go4+-
Minternal/jev/apikey.go14---
Minternal/jev/key_test.go3+-
Minternal/sheet/occupancy.go8-
Minternal/sheet/values.go9+-
Minternal/ui/charts_test.go2+-
Minternal/ui/graphics.go2+-
Minternal/ui/pivoteditor.go2+-
Ascripts/doclint/main.go163++++++++++++++++++++++++++++++
Mscripts/lint.sh19+++-
Astaticcheck.conf4+
A CLAUDE.md +23 -0
M cmd/012/main.go +2 -2
M cmd/012/telemetry.go +0 -16
M docs/architecture.md +1 -1
M docs/extending.md +4 -3
M docs/jev.md +1 -1
M docs/testing.md +6 -0
M internal/fileio/wk1.go +0 -1
M internal/fileio/xlsxwrite.go +0 -10
M internal/functions/reader.go +2 -2
M internal/jev/apikey.go +0 -14
M internal/jev/key_test.go +2 -1
M internal/sheet/occupancy.go +0 -8
M internal/sheet/values.go +4 -5
M internal/ui/charts_test.go +1 -1
M internal/ui/graphics.go +1 -1
M internal/ui/pivoteditor.go +1 -1
A scripts/doclint/main.go +163 -0
@@ -0,0 +1,163 @@
1+// Command doclint keeps documentation about the code as it is. It reads2+// Go comments and Markdown prose (not code, strings or code blocks) and3+// reports wording that narrates how the code got here: what changed,4+// what it did before, what's temporary, who did it and when. History5+// belongs in commit messages; docs and comments say what is and why.6+//7+// Saying why something stays compatible with a planned change is fine8+// ("kept a pointer so the store can change shape"), since it explains9+// the code today. A line that must use a flagged word says so with10+// "doclint:allow" and a reason, in a comment on that line (or an HTML11+// comment in Markdown).12+//13+// Usage: go run ./scripts/doclint [dirs or files...]14+package main15+16+import (17+ "bufio"18+ "fmt"19+ "go/parser"20+ "go/token"21+ "io/fs"22+ "os"23+ "path/filepath"24+ "regexp"25+ "strings"26+)27+28+// rules are the phrasings that describe development rather than the code.29+var rules = []struct {30+ re *regexp.Regexp31+ why string32+ markdown bool // only in Markdown, where the phrase can't be about code33+ notTests bool // not in tests, which narrate the steps they take34+}{35+ {re: re(`\bno longer (read|supported|used|needed|exists?|works?|possible|available|goes|runs)\b`), why: "describes a past state"},36+ {re: re(`\bused to (be|have|take|go|cost|read|write|live|run|do|make|scan|walk|say|show)\b|\bpreviously\b|\bformerly\b|\boriginally\b`), why: "describes a past state"},37+ {re: regexp.MustCompile(`\b(it|this|that|they|which|012|[a-z]+s) now (uses?|takes?|goes|lives?|sits?|keeps?|returns?|comes?|reads?|writes?|has|have|is|are|gets?|works?)\b|\b(is|are) now\b`), why: `"now" narrates a change`, notTests: true},38+ {re: re(`\b(was|were|has been|have been) (moved|renamed|replaced|rewritten|split|merged)\b|\b(moved|renamed) (from|out of|into)\b`), why: "narrates a change"},39+ {re: re(`\bfor now\b|\btemporarily\b|\bat the moment\b|\bfor the time being\b|\buntil (it|this|that) lands\b`), why: "describes a transient state"},40+ {re: regexp.MustCompile(`\b(TODO|FIXME|XXX|HACK)\b`), why: "a work note; put it in ROADMAP.md instead"},41+ {re: regexp.MustCompile(`\(new\)|\bnew in v?\d|\bas of v\d|\bsince v\d`), why: "dates the text"},42+ {re: re(`\bbefore (and|->|→|/) after\b|\bthis (commit|change|PR)\b|\bin the commit that\b`), why: "narrates a change", markdown: true},43+ {re: re(`\b(the lead|an? agent|merged with main|merge of main)\b`), why: "narrates who changed it"},44+}45+46+func re(s string) *regexp.Regexp { return regexp.MustCompile(`(?i)` + s) }47+48+// skip lists paths whose purpose is history or plans: the roadmap tracks49+// what's done and ahead, NOTICE and LICENSE are legal text.50+var skip = map[string]bool{"ROADMAP.md": true, "NOTICE": true, "LICENSE": true}51+52+type finding struct {53+ file string54+ line int55+ text string56+ why string57+}58+59+func main() {60+ roots := os.Args[1:]61+ if len(roots) == 0 {62+ roots = []string{"."}63+ }64+ var found []finding65+ for _, root := range roots {66+ filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {67+ if err != nil {68+ return err69+ }70+ name := d.Name()71+ if d.IsDir() {72+ if path != root && (strings.HasPrefix(name, ".") || name == "testdata" || name == "node_modules") {73+ return filepath.SkipDir74+ }75+ return nil76+ }77+ if skip[filepath.ToSlash(path)] || skip[name] {78+ return nil79+ }80+ switch filepath.Ext(name) {81+ case ".go":82+ found = append(found, goComments(path)...)83+ case ".md":84+ found = append(found, markdown(path)...)85+ }86+ return nil87+ })88+ }89+ for _, f := range found {90+ fmt.Printf("%s:%d: %s: %s\n", f.file, f.line, f.why, strings.TrimSpace(f.text))91+ }92+ if len(found) > 0 {93+ fmt.Printf("doclint: %d lines describe development rather than the code\n", len(found))94+ os.Exit(1)95+ }96+}97+98+// check reports the rules text breaks, unless it's allowed.99+func check(file string, line int, text string) []finding {100+ if strings.Contains(text, "doclint:allow") {101+ return nil102+ }103+ md := strings.HasSuffix(file, ".md")104+ test := strings.HasSuffix(file, "_test.go")105+ var out []finding106+ for _, r := range rules {107+ if r.markdown && !md || r.notTests && test {108+ continue109+ }110+ if r.re.MatchString(text) {111+ out = append(out, finding{file, line, text, r.why})112+ break113+ }114+ }115+ return out116+}117+118+func goComments(path string) []finding {119+ fset := token.NewFileSet()120+ f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)121+ if err != nil {122+ return nil // not ours to judge; the compiler reports it123+ }124+ var out []finding125+ for _, g := range f.Comments {126+ for _, c := range g.List {127+ line := fset.Position(c.Pos()).Line128+ for i, l := range strings.Split(c.Text, "\n") {129+ out = append(out, check(path, line+i, l)...)130+ }131+ }132+ }133+ return out134+}135+136+func markdown(path string) []finding {137+ f, err := os.Open(path)138+ if err != nil {139+ return nil140+ }141+ defer f.Close()142+ var out []finding143+ fenced := false144+ sc := bufio.NewScanner(f)145+ sc.Buffer(make([]byte, 1<<20), 1<<20)146+ for n := 1; sc.Scan(); n++ {147+ l := sc.Text()148+ if strings.HasPrefix(strings.TrimSpace(l), "```") {149+ fenced = !fenced150+ continue151+ }152+ if fenced {153+ continue154+ }155+ out = append(out, check(path, n, stripCode(l))...)156+ }157+ return out158+}159+160+var inlineCode = regexp.MustCompile("`[^`]*`")161+162+// stripCode drops inline code spans, which quote code rather than narrate.163+func stripCode(l string) string { return inlineCode.ReplaceAllString(l, "``") }
M scripts/lint.sh +17 -2
A staticcheck.conf +4 -0