Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ A new `lib/<m>` is never enough on its own; wire the whole ring or the build/tes
- Go values enter scripts via the `predecl` parameter (see `lib/serial`'s use of `startime.Time` / `dataconv.ConvertStruct`).
- The default suite must stay **hermetic** (no real network/DNS; `lib/net` uses local stubs) — real-network tests go behind `//go:build integration`.

## Documentation standard for `lib/*` READMEs

Module READMEs are read by humans skimming and by AI agents parsing — optimize for both: a scannable, complete surface table up front, runnable examples, explicit boundaries. Every script-visible member (function, constant, type) must be documented, and `TestDocCoverage` enforces it (see below).

Required structure, in order:

1. **Title + purpose** — `# <module>` then 1–2 sentences: what it does; what it mirrors or succeeds (e.g. "a subset of Python's `re`"); the capability profile (pure / filesystem / network / process / log), so a reader knows the side effects without reading code.
2. **Functions** — a single scannable table listing **every** function, grouped if large: `| function | description |`, with the signature in the function cell as `` `name(args) → result` ``. `try_*` variants may share a row but each name must still appear as a backtick token (`` `try_get` ``) so coverage passes. This table is the contract `TestDocCoverage` checks.
3. **Constants** (if any) — `| constant | meaning |`, every one present.
4. **Types** (if any, e.g. `Pattern`, `Match`) — a subsection per type with a methods/attributes table.
5. **Details & examples** — per function or group: the signature, parameters only where non-obvious, the return, **what it errors on** (the honest-boundary principle), and at least one **runnable example ending in `# Output:`**.
6. **Notes / boundaries** — engine, determinism, limits, differences from the mirrored API.

Style: names always in backticks; lead with the table before prose; examples real, minimal, runnable; drop framework boilerplate (no empty `#### Parameters` table for a one-arg function — fold into a sentence); state errors and edge behavior explicitly; flag any non-snake_case name (e.g. http's `postForm`).

**Doc coverage check** — `tools/doccov/coverage.star` + `TestDocCoverage` enforce that every member of every `lib/*` module appears in its README (matched as a backtick-quoted identifier). The Go test enumerates the authoritative surface (each module's registered members across the Module/Struct/flat shapes) and runs the `.star` matcher through a Machine, so the check **dogfoods the `regex` module**. Run it with `go test -run TestDocCoverage -v .` — the report lists any undocumented members. (Scope: `lib/*` modules; the go.starlark.net-backed `math`/`struct`/`time` have no lib README and are skipped. Type methods are a standard requirement verified by review, not by the automated member check.)

## Release discipline

- **Never tag or publish autonomously.** Draft the release title + notes, show the user, and tag only after explicit approval. Patch bump by default. A published tag is immutable in the Go module proxy.
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export PACK=main
export FLAGS="-s -w -X '$(PACK).AppName=$(BINARY)' -X '$(PACK).BuildDate=`date '+%Y-%m-%dT%T%z'`' -X '$(PACK).BuildHost=`hostname`' -X '$(PACK).GoVersion=`go version`' -X '$(PACK).GitBranch=`git symbolic-ref -q --short HEAD`' -X '$(PACK).GitCommit=`git rev-parse --short HEAD`' -X '$(PACK).GitSummary=`git describe --tags --dirty --always`' -X '$(PACK).CIBuildNum=${BUILD_NUM}'"

# commands
.PHONY: default ci test test_loop bench build
.PHONY: default ci test test_loop bench build doc-check
default:
@echo "build target is required for $(BINARY)"
@exit 1
Expand All @@ -39,6 +39,13 @@ ci:
$(GOTEST) -v -race -cover -covermode=atomic -coverprofile=coverage.txt -count 1 ./...
$(GOTEST) -v -parallel=4 -run="none" -benchtime="2s" -benchmem -bench=.

# Documentation coverage gate: every script-visible member of every lib/*
# module must be documented in its README. Runs tools/doccov/coverage.star
# through a Machine (dogfoods the regex module). Already part of `make ci` via
# ./... ; this target runs just it, with the undocumented-member report.
doc-check:
$(GOTEST) -v -run TestDocCoverage -count 1 .

build:
make -C cmd/starlet build

Expand Down
94 changes: 94 additions & 0 deletions doc_coverage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package starlet_test

import (
"os"
"path/filepath"
"sort"
"strings"
"testing"

"github.com/1set/starlet"
"go.starlark.net/starlarkstruct"
)

// libReadmeDir maps a builtin module name to its lib/<dir> documentation
// directory. The only non-identity case is go_idiomatic -> goidiomatic;
// modules backed by go.starlark.net (math, struct, time) have no lib README
// and are skipped below.
func libReadmeDir(module string) string {
return strings.ReplaceAll(module, "_", "")
}

// moduleSurface enumerates the script-visible names a module exports, across
// the three registration shapes: a starlarkstruct.Module (its Members), a
// starlarkstruct.Struct (its AttrNames), or a flat StringDict (its keys).
func moduleSurface(t *testing.T, name string) []string {
loader := starlet.GetBuiltinModule(name)
if loader == nil {
return nil
}
sd, err := loader()
if err != nil {
t.Fatalf("load module %q: %v", name, err)
}
var out []string
for k, v := range sd {
switch m := v.(type) {
case *starlarkstruct.Module:
for mk := range m.Members {
out = append(out, mk)
}
case *starlarkstruct.Struct:
out = append(out, m.AttrNames()...)
default:
out = append(out, k)
}
}
sort.Strings(out)
return out
}

// TestDocCoverage asserts that every script-visible member of every lib/*
// module is documented in that module's README. The matching logic lives in
// tools/doccov/coverage.star and runs through a starlet Machine, so the check
// dogfoods the regex module.
func TestDocCoverage(t *testing.T) {
script, err := os.ReadFile(filepath.Join("tools", "doccov", "coverage.star"))
if err != nil {
t.Fatalf("read coverage script: %v", err)
}

surface := map[string]interface{}{}
docs := map[string]interface{}{}
var skipped []string
for _, name := range starlet.GetAllBuiltinModuleNames() {
readme, err := os.ReadFile(filepath.Join("lib", libReadmeDir(name), "README.md"))
if err != nil {
skipped = append(skipped, name) // external module without a lib README
continue
}
docs[name] = string(readme)
names := moduleSurface(t, name)
members := make([]interface{}, len(names))
for i, n := range names {
members[i] = n
}
surface[name] = members
}

m := starlet.NewWithNames(starlet.StringAnyMap{"surface": surface, "docs": docs}, nil, []string{"regex"})
m.SetScriptContent(script)
out, err := m.Run()
if err != nil {
t.Fatalf("doc coverage script failed: %v", err)
}
if report, ok := out["report"].(string); ok {
t.Log("\n" + report)
}
sort.Strings(skipped)
t.Logf("skipped (go.starlark.net modules, no lib README): %v", skipped)

if missing, ok := out["missing"].([]interface{}); ok && len(missing) > 0 {
t.Errorf("%d module member(s) are not documented in their README — see the report above", len(missing))
}
}
Loading
Loading