diff --git a/docs/development.md b/docs/development.md index 6c07248..0971559 100644 --- a/docs/development.md +++ b/docs/development.md @@ -44,3 +44,190 @@ Start with: For cross-cutting changes, follow every applicable row. Do not create placeholder documents for packages, APIs, or integrations that do not yet exist. + +## Maintainer Validation + +This section is the canonical local validation workflow for Promptkit. Run +every command from the repository root before accepting a change. The test +suite and maintained examples are deterministic, offline, and require no real +provider credentials. + +### Tests, Analysis, Build, And Examples + +Run the ordinary and race-enabled suites, static analysis, the build, and both +maintained consumer examples: + +```sh +go test ./... +go test -race ./... +go vet ./... +go build ./... +go run ./examples/go-library/prepare +go run ./examples/go-library/run +``` + +Both examples must exit successfully. Review their JSON output: preparation +must report the selected offline prompt, profile, model, and message count; +execution must report the deterministic generated output, successful +validation, selected offline model, and usage. Neither command may contact a +provider or require credentials. + +### Go Formatting + +Check every tracked Go file. The final command must succeed and the captured +list must be empty: + +```sh +unformatted=$( + git ls-files '*.go' | + while IFS= read -r go_file + do + gofmt -l "$go_file" + done +) +test -z "$unformatted" +``` + +### Local Markdown Links + +Use the Python standard library to verify every repository-relative Markdown +target and local heading fragment. The check is offline and prints nothing on +success: + +```sh +python3 - <<'PY' +from pathlib import Path +import re +import subprocess +import sys +from urllib.parse import unquote + +root = Path.cwd().resolve() +markdown_files = [ + root / name + for name in subprocess.check_output( + ["git", "ls-files", "*.md"], text=True + ).splitlines() +] +link_pattern = re.compile(r"!?\[[^]]*\]\(([^)]+)\)") +heading_pattern = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$") +scheme_pattern = re.compile(r"^[a-z][a-z0-9+.-]*:", re.IGNORECASE) + + +def markdown_lines(path): + in_fence = False + fence = "" + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.lstrip() + marker = stripped[:3] + if marker in {"```", "~~~"}: + if not in_fence: + in_fence = True + fence = marker + elif marker == fence: + in_fence = False + fence = "" + continue + if not in_fence: + yield line + + +anchor_cache = {} + + +def anchors(path): + if path in anchor_cache: + return anchor_cache[path] + found = set() + counts = {} + for line in markdown_lines(path): + match = heading_pattern.match(line) + if not match: + continue + heading = re.sub(r"<[^>]+>", "", match.group(1)).replace("`", "") + base = re.sub(r"[^\w\- ]", "", heading.lower()).replace(" ", "-") + count = counts.get(base, 0) + counts[base] = count + 1 + found.add(base if count == 0 else f"{base}-{count}") + anchor_cache[path] = found + return found + + +failures = [] +for source in markdown_files: + text = "\n".join(markdown_lines(source)) + for match in link_pattern.finditer(text): + target = match.group(1).strip() + if target.startswith("<") and target.endswith(">"): + target = target[1:-1] + if scheme_pattern.match(target) or target.startswith("//"): + continue + path_text, separator, fragment = target.partition("#") + destination = source if not path_text else source.parent / unquote(path_text) + try: + destination = destination.resolve() + destination.relative_to(root) + except ValueError: + failures.append(f"{source.relative_to(root)}: escapes repository: {target}") + continue + if not destination.exists(): + failures.append(f"{source.relative_to(root)}: missing target: {target}") + continue + if separator and destination.suffix.lower() == ".md": + fragment = unquote(fragment).lower() + if fragment not in anchors(destination): + failures.append(f"{source.relative_to(root)}: missing anchor: {target}") + +if failures: + print("\n".join(failures), file=sys.stderr) + raise SystemExit(1) +PY +``` + +### Repository Hygiene And Review + +Reject an active Go workspace, tracked workspace files, a vendor tree, or a +module replacement: + +```sh +case "$(go env GOWORK)" in + ''|off) ;; + *) printf '%s\n' 'an active Go workspace is not allowed' >&2; exit 1 ;; +esac +test -z "$(git ls-files go.work go.work.sum)" +test ! -e vendor +if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod +then + printf '%s\n' 'go.mod contains a replacement' >&2 + exit 1 +fi +``` + +Check whitespace in both unstaged and staged changes. List ignored files and +scan tracked content for common credential forms: + +```sh +git diff --check +git diff --cached --check +test -z "$(git ls-files --others --ignored --exclude-standard)" +credential_pattern='-----BEGIN ([A-Z0-9]+ )?PRIV''ATE KEY-----|AKI''A[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36,}|sk-[A-Za-z0-9]{32,}' +if git grep -nEI -e "$credential_pattern" -- . +then + printf '%s\n' 'possible credential found' >&2 + exit 1 +fi +``` + +Inspect `git status --short --untracked-files=all` and the complete diff before +accepting a change. The status may contain only the intended source changes +during development. Reject credentials, private keys, environment files, +generated binaries, test or coverage output, downloaded assets, template +residue, and any other artifact that does not belong in source control. The +credential scan catches common forms but does not replace inspection of the +actual change. + +After committing the accepted change, require a clean candidate: + +```sh +test -z "$(git status --porcelain)" +``` diff --git a/docs/policy/testing.md b/docs/policy/testing.md index bd7cf8d..ed62030 100644 --- a/docs/policy/testing.md +++ b/docs/policy/testing.md @@ -50,19 +50,18 @@ Examples of appropriate seams include clocks, randomness, subprocesses, remote A ## Test execution requirements Promptkit currently uses maintainer-run validation rather than hosted CI. -Maintainers run the repository-documented test, vet, build, formatting, -documentation-link, and repository-hygiene checks before accepting changes. +Maintainers run the complete local workflow in the +[development guide](../development.md#maintainer-validation) before accepting +changes. That guide is the canonical owner of exact commands, formatting, +documentation-link validation, and repository-hygiene checks. Introducing hosted CI later would supplement, not silently redefine, this documented validation model. -The complete test sequence includes ordinary and race-enabled package tests. -The maintained offline consumer workflow is also run from the repository root: - -```sh -go test ./... -go test -race ./... -go run ./examples/go-library/prepare -``` +Maintainer validation must include ordinary and race-enabled package tests, +static analysis, a complete build, and execution of both maintained offline +consumer examples. The preparation example protects assembled preparation and +inspection behavior. The execution example separately protects assembled +`Run`, injected-client, validation, usage, and result behavior. Tests in the default suite must be deterministic, offline, and independent of real credentials. They must not invoke paid APIs, use live network @@ -88,8 +87,9 @@ Use each test type where it protects a distinct risk: interaction, while replacing live or nondeterministic external boundaries. - External-package root tests exercise the public facade as a Go consumer, while internal package tests own focused implementation behavior. -- The maintained offline preparation example protects one representative - assembled consumer workflow without contacting a model provider. +- The maintained offline preparation and execution examples protect distinct + representative assembled consumer workflows without contacting a model + provider. - Fixtures should be minimal, synthetic, versioned with the behavior they exercise, and free of credentials or private data. - Golden files are appropriate only when the complete output is intentionally diff --git a/docs/release.md b/docs/release.md index 6b15cab..ec6823e 100644 --- a/docs/release.md +++ b/docs/release.md @@ -106,48 +106,12 @@ gitea.maximumdirect.net/eric/promptkit 1.25.5 promptkit gitea.maximumdirect.net/eric/promptkit ``` -Run the complete maintainer validation required by the -[development guide](development.md): - -```sh -go test ./... -go test -race ./... -go vet ./... -go build ./... -go run ./examples/go-library/prepare -``` - -Check every tracked Go file. This command must produce no output: - -```sh -unformatted=$( - git ls-files '*.go' | - while IFS= read -r go_file - do - gofmt -l "$go_file" - done -) -test -z "$unformatted" -``` - -Follow every maintained Markdown link and confirm that its local or published -target exists. Review the repository for generated binaries, test or coverage -output, credentials, template residue, downloaded assets, and other files that -do not belong in source control. - -Recheck module and repository hygiene, whitespace, and the clean checkout: - -```sh -test -z "$(git ls-files go.work go.work.sum)" -test ! -e vendor -if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod -then - printf '%s\n' 'go.mod contains a replacement' >&2 - exit 1 -fi -git diff --check -test -z "$(git status --porcelain)" -``` +As a release prerequisite, run the complete +[maintainer validation workflow](development.md#maintainer-validation) against +the clean candidate. Do not substitute a partial command list: the development +guide owns the tests, race checks, analysis, build, both offline examples, +formatting, Markdown links, generated-output and credential review, and +repository hygiene. Record the successful workflow result with the candidate. ## Write The Release Note