Skip to content
internal/quality
On this page

Scoring a project's documentation on two independent axes: a maturity tier from selfdoc feature adoption, and a content grade from the doc-to-source ratio.

#internal/quality

#internal/quality

Package quality scores a project's documentation: a maturity tier (0-5) and a content grade (A-F).

Two independent axes describe a project's documentation:

- the tier (0-5, see [Tiers]) is a ladder of selfdoc feature adoption -- markdown exists, selfdoc.json exists, root files are generated from templates, directives connect docs to source, custom directives or blog posts are in use. Each rung requires every rung below it, so the tier is the first unmet requirement minus one, and [NextSteps] names the action that reaches the next rung. - the content grade (A-F, see [ContentGrade]) measures volume: documentation lines divided by non-test source lines.

Source lines come from the external dirstat binary (a hard requirement, see [CheckDirstat]); documentation lines, test lines, and directive usage are counted by walking the tree here. Git submodules are excluded from every count, so a project is scored on the code it actually owns.

[Run] is the entry point behind selfdoc quality.

#CodeExtensions

Go go
var CodeExtensions = map[string]bool{

CodeExtensions is every file extension this package counts as code. Markup, data and lockfiles are deliberately absent: a project is graded on documentation against source, and a lockfile is neither.

#SkipDirs

Go go
var SkipDirs = map[string]bool{

SkipDirs is every directory name the tree walks step over: version control, dependency trees, build output and tool caches.

#SkipMarkdownFiles

Go go
var SkipMarkdownFiles = map[string]bool{"CHANGELOG.md": true}

SkipMarkdownFiles is every Markdown filename that is not documentation. A generated changelog is a record of releases, not prose somebody wrote.

#Tiers

Go go
var Tiers = [6]Tier{

Tiers is the maturity ladder, indexed by tier number.

#NextSteps

Go go
var NextSteps = [5]string{

NextSteps names the concrete action that reaches the next rung, indexed by the tier the project is on. There is no entry for tier 5, where nothing is left to do.

#Adoption

Go go
type Adoption struct

Adoption describes a project's selfdoc adoption, as read from selfdoc.json. Everything but HasSelfdoc is meaningless when HasSelfdoc is false, and the machine payload omits those members exactly then.

#Tier

Go go
type Tier struct

Tier is one rung of the maturity ladder: its short label and the requirement it states.

#DirstatError

Go go
type DirstatError struct

DirstatError reports that dirstat could not be run, or answered in a shape this package cannot read.

It is the Go counterpart of the Python surface's DirstatError, and the one error type a caller needs to recognize with errors.As to print one refusal line instead of an unexpected internal failure.

#DirstatMissingError

Go go
type DirstatMissingError struct{}

DirstatMissingError reports that the dirstat binary is not installed.

It is a distinct type because it is the one condition [CheckDirstat] answers, and the message is the two lines the command prints to stderr before exiting non-zero. The Python function it replaces terminated the process from inside a library; here the library reports and the CLI decides.

#Result

Go go
type Result struct

Result is one project's score: the shape both the text report and the machine payload carry.

#MarkdownLOC

Go go
func MarkdownLOC(projectPath string, submodulePaths []string, rootFileTemplates []string) (int, int)

MarkdownLOC returns the project's Markdown line and file counts.

It walks projectPath counting lines in every .md file, skipping:

- the directories in [SkipDirs] plus todo/ (planning notes are not documentation) and every directory in submodulePaths; - the filenames in [SkipMarkdownFiles] (generated changelogs); - every path in rootFileTemplates -- the docs/_README.md style templates named by root_files in selfdoc.json. Their generated output (README.md, CLAUDE.md) sits at the project root and is counted instead, so skipping the template avoids counting the same prose twice. Paths are matched relative to projectPath, as selfdoc.json spells them.

Files that cannot be read are skipped rather than counted as empty.

#TestLOC

Go go
func TestLOC(projectPath string, submodulePaths []string) int

TestLOC returns the total line count of the project's test code.

It walks projectPath (skipping [SkipDirs], todo/ and every directory in submodulePaths) and counts lines in files that have a [CodeExtensions] extension AND look like tests -- meaning they sit under a tests/test/__tests__/testing directory at any depth, or are named conftest.py, test_.py, _test.py, _test.go, or .test./*.spec. for js/ts/jsx/tsx.

[ScoreProject] subtracts this from the dirstat code total so the doc ratio is measured against production source only, and a large test suite neither inflates nor deflates a project's grade.

#SelfdocInfo

Go go
func SelfdocInfo(projectPath string) Adoption

SelfdocInfo describes the project's selfdoc adoption, as read from selfdoc.json. A missing or unparsable selfdoc.json answers with HasSelfdoc false and nothing else.

These flags are what [ComputeTier] climbs its ladder on.

#ComputeTier

Go go
func ComputeTier(docLOC int, info Adoption) int

ComputeTier returns the maturity tier 0-5 for a project (see [Tiers]).

The rungs are cumulative and evaluated in order, so the tier is the last satisfied requirement: any Markdown at all (1), selfdoc.json present (2), a generated README template in root_files (3), at least one directive used in the docs (4), and custom directives or blog posts configured (5).

#ContentGrade

Go go
func ContentGrade(ratio *float64) string

ContentGrade grades a documentation-to-source line ratio as A-F.

Cut-offs, applied to the ratio (doc LOC / non-test source LOC): 0.30 or more is an A, 0.15 a B, 0.05 a C, 0.01 a D, and anything below that an F. A nil ratio -- meaning there was no source to compare against -- grades as "-" rather than F, so an empty project is not marked as failing.

#CheckDirstat

Go go
func CheckDirstat(h *effects.Handle) error

CheckDirstat returns a [DirstatMissingError] unless the dirstat binary is runnable.

Source-line counting has no in-tree fallback, so a missing dirstat would silently report every project as 0 source LOC. This probes it once up front (dirstat scan --help). Only absence is a refusal: a non-zero exit from the probe itself is ignored, since it still proves the binary exists.

#SubmodulePaths

Go go
func SubmodulePaths(projectPath string) []string

SubmodulePaths returns the path entries declared in the project's .gitmodules.

The paths are returned as written (repository-relative, e.g. "vendor/theme") and are used by every counter here to keep submodule content out of a project's own totals. The result is empty when there is no .gitmodules file or it cannot be read.

#CodeLOC

Go go
func CodeLOC(projectPath string, submodulePaths []string, h *effects.Handle) (int, int, error)

CodeLOC returns the project's code LOC and file count, submodules excluded.

It runs dirstat scan over projectPath and keeps only the file-format groups whose extension appears in [CodeExtensions] -- markup, data and lockfiles are therefore not code. Because dirstat scans the whole tree, each path in submodulePaths is scanned separately and subtracted; overlapping entries (a submodule nested inside another submodule) are subtracted once each, so the totals can go negative.

It returns a [DirstatError] when any scan fails, times out (60s per scan), or answers in a shape this package cannot read -- the submodule scans included, since a subtraction that silently did not happen inflates the total it was there to correct.

#ScoreProject

Go go
func ScoreProject(projectPath string, h *effects.Handle) (Result, error)

ScoreProject scores one project directory.

It runs every counter in this package against projectPath and combines them. SourceLOC is the dirstat code total minus test LOC, floored at zero; DocRatio is doc LOC over SourceLOC rounded to four decimals, or nil when there is no source to divide by.

#Run

Go go
func Run(dir string, h *effects.Handle) (Result, error)

Run runs selfdoc quality: score the project rooted at dir.

It verifies dirstat is installed first ([CheckDirstat]), then scores dir as an absolute path -- the Python function this replaces always scored the process's own working directory, so the project's reported name is the directory's name rather than the empty name a relative "." would carry.

It returns a [DirstatError] when the scan the score rests on could not be performed or understood. Scoring itself never fails: quality is a report, not a blocker, so a low tier or a failing grade is an answer, not an error.

#FormatSingleText

Go go
func FormatSingleText(result Result) string

FormatSingleText renders a [ScoreProject] result as the human-readable report.

The report is a headline (project, tier, tier name), a one-line metrics summary (source LOC, test LOC when non-zero, doc LOC with the ratio as a percentage, file count, grade), the selfdoc adoption block or a "not configured" line, the tiers already completed, and the tiers still to do -- where the immediate next tier is stated as the concrete action from [NextSteps] rather than as a requirement.

The returned report carries no trailing newline.

#DirstatError.Error

Go go
func (e *DirstatError) Error() string { return e.Message }

Error returns the diagnostic.

#DirstatMissingError.Error

Go go
func (e *DirstatMissingError) Error() string

Error returns the refusal and the install command, one per line.

#Result.Payload

Go go
func (r Result) Payload() map[string]any

Payload returns the result as the QUALITY machine payload: the same members under the same names, with the selfdoc block carrying only has_selfdoc when the project has no selfdoc.json.

Search