diff --git a/.claude/README.md b/.claude/README.md deleted file mode 100644 index 2b47fd1..0000000 --- a/.claude/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# `.claude/` helper set - -Productivity helpers for roadmap/spec-driven work in `living-doc-toolkit`. Copied and -adapted from the ecosystem reference copy in `living-doc-collector-gh/.claude/` (the -repo-specific parts here are the monorepo `make` vocabulary, the mock/fixture table, and -the doc paths). - -These helpers are **acceleration, not dependency**. Every task they describe can be done by -hand, just slower. Nothing in CI or the release path requires them. - -## What's here - -| File | Kind | Purpose | -|---|---|---| -| `commands/implement-task.md` | slash command | Drive one roadmap task from its spec to a PR-ready description. | -| `commands/verify-pr-ready.md` | slash command | Verify-only gate: is this branch PR-ready? one yes/no. | -| `agents/test-author.md` | subagent | Write deterministic pytest tests using this repo's real mock/fixture surface. | -| `rules/docs-lifecycle.md` | rule | Implemented `SPEC.md` sections **move** into the live docs. | - -## How the two commands relate - -Both check **acceptance criteria against the actual code** — reading the return annotation, -the sort call, the guard position, the literal `pyproject.toml` value — never "a test with -that name is green". - -- **`/implement-task `** is the full lifecycle: read the task → read every referenced - file → implement → `make qa` (or `make qa-`) until green → verify each acceptance - criterion against code → write the `## Overview` / `## Release Notes` / `## Related` PR - description → stop. It collapses the `specification-master` → `senior-developer` → - `sdet` → `reviewer` agent sequence into one driven command. - -- **`/verify-pr-ready `** is the lighter companion for right before opening a PR — - useful when a human did most of the implementation. It re-checks every acceptance - criterion against code, runs `make qa`, runs the `reviewer` agent, and outputs a single - `PR-READY: yes/no` with a specific fix list when the answer is no. It does not implement - anything. - -Typical flow: `/implement-task` to build it, then `/verify-pr-ready` as the final gate -before opening the PR. - -## Relationship to `.github/agents/` - -The `.github/agents/` board (`specification-master`, `senior-developer`, `sdet`, -`reviewer`, `devops-engineer`) stays the source of truth for role behavior. `test-author` -here is `sdet` plus the concrete repo mock table; `/verify-pr-ready` invokes `reviewer`. - -The concrete mock/fixture cheat-table that drives test implementation lives in -`agents/test-author.md` — this is the single source of truth for which boundary to -isolate and how, updated whenever a contract or fixture changes. diff --git a/.claude/agents/test-author.md b/.claude/agents/test-author.md deleted file mode 100644 index 06d0e72..0000000 --- a/.claude/agents/test-author.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: test-author -description: Writes deterministic pytest tests for living-doc-toolkit, using this repo's real mock and fixture surface. -tools: Read, Grep, Glob, Edit, Write, Bash ---- - -You write tests for `living-doc-toolkit`. You are the `sdet` agent's principles -(determinism, fast feedback, success + failure coverage) plus this repo's **concrete mock -and fixture surface** — so you isolate the right boundary on the first try instead of -guessing. - -## Rules - -- Must use `pytest` + `pytest-cov`. Tests live in each package's own `tests/` directory, - mirroring its `src/` layout; the action's entry point and venv step are tested in the - repo-root `tests/`. -- The pipeline makes no network calls — there is nothing external to stub. Must isolate - file I/O to `tmp_path` and fixtures, never the ambient filesystem. -- Must read contract inputs from the one fixture copy under the repo-root `tests/fixtures/`, - or write a changed copy of it under `tmp_path`; never hand-write a whole contract file. -- Must assert on behavior — return values, the contract model a read returns, raised errors, - each `InputProblem`'s code, location and message, exit codes and error lines. -- Prefer adding to each package's `tests/conftest.py` over duplicating setup. -- Must keep the suite green under `make test-` / `make coverage-` (≥ 80% per - package). - -## Mock / fixture cheat-table (sourced from what already exists in `tests/`) - -| What you need to fake | Pattern used in this repo | Where to copy it from | -|---|---|---| -| A valid contract input | `living-doc`'s example `doc-source.json` / `ui-tests.json`, via the `doc_source` / `ui_tests` fixtures | `tests/fixtures/collector-gh/`; `packages/core/tests/conftest.py` | -| An input that fails one validation step | `changed_copy(fixture, change)` writes the fixture under `tmp_path` with `change` applied to its parsed JSON | `packages/core/tests/conftest.py`, `test_input_validation.py` | -| Every optional field of a contract filled | `living_doc_utilities.contracts.testing.full_sample(contract_id)` | `living-doc-utilities` | -| A read through `read_artifact` | `monkeypatch.setattr(readers, "read_artifact", ...)` around the real one, to record the call | `packages/core/tests/test_readers.py` | -| The CLI the action runs | `_FakeCli` replaces `subprocess.run` to test the run itself | `tests/test_main.py` | -| Action inputs | `INPUT_` environment variables via `monkeypatch.setenv`; `tests/conftest.py` clears inherited ones | `tests/test_main.py`, `tests/conftest.py` | -| The action's venv step | a fake interpreter and pip, run through bash | `tests/test_action_yml.py` | -| CLI invocation + exit code + output | `click.testing.CliRunner().invoke(cli, [...])`, assert `result.exit_code` and `result.output` | `apps/cli/tests/test_cli.py` | -| Logging assertions | `caplog` / `capsys` on the `[LEVEL] message` lines | `tests/test_main.py`, `packages/core/tests/test_input_validation.py` | - -**No HTTP surface:** the toolkit never calls a network service. If a change ever -introduces one, that is a design red flag — the pipeline is offline and AI-free by -contract. Raise it rather than adding an HTTP mock. - -## Output - -- The test files/additions themselves. -- A recap ≤ 10 lines: what is covered (success + failure paths), how to run it - (`make test-` / `make coverage-`), any coverage gap and why. diff --git a/.claude/commands/implement-task.md b/.claude/commands/implement-task.md deleted file mode 100644 index 7cc16e4..0000000 --- a/.claude/commands/implement-task.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -description: Implement a Phase 0 / roadmap task end to end, from its spec to a PR-ready description. -argument-hint: (e.g. P0-TK-3) [path-or-URL to the task list, if not the default] ---- - -You are driving a single roadmap task to completion in `living-doc-toolkit`. -Follow these steps **in this exact order**. Do not skip or reorder them. - -Task ID: `$1` -Task-list source (optional override): `$2` - -## 1. Read the task - -- Locate task `$1` in the Phase 0 task list (`living-doc/docs/specs/phase-0-tasklist.md`) - or the roadmap (`living-doc/docs/specs/roadmap.md`). If `$2` is given, read it there. -- Read the task's **prompt** and its **acceptance criteria** in full. Also read the - linked GitHub issue if the task names one. -- Write the acceptance criteria out as a numbered checklist you will verify against code - in step 5. Do not start work until this list is explicit. - -## 2. Read every file the task references — before writing any code - -- Open every spec, doc, config, and source file named in the task prompt, its "Spec refs" - line, and its acceptance criteria. Follow one hop of cross-references (a spec section that - points at another spec section). -- Read the existing code the change touches and the tests that already cover it, so the - implementation matches repo patterns (`.github/copilot-instructions.md`, - `.github/copilot-review-rules.md`). -- For a monorepo change, identify which packages the task touches — `core`, `apps/cli`, or a - new one — and respect the dependency rules. -- If the task is ambiguous after reading, stop and ask — do not guess. - -## 3. Implement to the spec exactly - -- Make the smallest change that satisfies the spec. Match existing structure, naming, - logging style (lazy `%` formatting, no `print`), and import conventions. -- Prefer a change that touches one package. Split a cross-package change unless the parts - must change together. -- Add or update tests for every new or changed code path (success + failure). Use the - `test-author` agent for the test surface. -- Do not change externally-visible contracts (CLI argument names/defaults, action inputs and - outputs, exit codes, error lines) unless the task explicitly calls for it. The artifact - contracts are `living-doc-utilities`'; the toolkit never redefines one. -- If a spec section is being implemented, apply `.claude/rules/docs-lifecycle.md` in this - same change: move that section's content out of `SPEC.md` into the live docs. - -## 4. Run the `make qa` loop until green - -- Run `make qa` for a full-repo change, or `make qa-` for the package you touched - (`` ∈ `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix`, `cli`) — the same targets CI runs. -- Fix every failure and re-run. Repeat until QA exits clean per package: - Pylint ≥ 9.5, Black clean, mypy clean, deptry clean, `pytest --cov-fail-under=80` passing, - and `make no-vendored-schemas` clean. -- Do not lower a threshold, add an inline lint disable, or `# type: ignore` to get past a - gate unless the repo documents that exception. - -## 5. Verify each acceptance criterion against the actual code - -For every criterion on your step 1 checklist, confirm the **literal claim** by reading the -code / config that now exists — **not** by checking that a same-named test is green. - -- "returns a typed `X`" → read the function's real return annotation. -- "sorted descending" → read the actual sort call and its `reverse=`/key. -- "reports only the first failing step" → read the step order in `validate_inputs` and - confirm each step stops before the next. -- "`requires-python` is `>=3.10`" → open each `pyproject.toml` and read the literal value. -- "file X exists / contains Y" → open X and confirm Y is literally present. - -Record each criterion as met (with the `file:line` that proves it) or not met. If any -criterion is not met, return to step 3. - -## 6. Write the PR description - -Write a PR description (to `pr.md` in the repo root unless told otherwise) with exactly -these sections, per `CONTRIBUTING.md`: - -- `## Overview` — what changed and why. -- `## Release Notes` — at least one real user-facing bullet (no `TBD`). -- `## Related` — `Closes #` for the task's issue. - -Include a short "Acceptance criteria" checklist mapping each criterion to the `file:line` -that satisfies it (from step 5). - -## 7. Stop - -Do not open the PR, push, or commit. Report: what changed, the `make qa` result, and the -acceptance-criteria verification table. Then stop. diff --git a/.claude/commands/verify-pr-ready.md b/.claude/commands/verify-pr-ready.md deleted file mode 100644 index cb3515b..0000000 --- a/.claude/commands/verify-pr-ready.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -description: Verify a branch is PR-ready — acceptance criteria checked against code, QA gates green, reviewer pass. Outputs one yes/no. -argument-hint: (e.g. P0-TK-3) [path-or-URL to the task list, if not the default] ---- - -You are the final gate before a PR is opened. This is the **verify-only** companion to -`/implement-task` — assume a human (or a prior run) did most of the implementation and you -are applying the same rigor as a last check. Do not implement features here; only verify, -and list fixes if it is not ready. - -Task ID: `$1` -Task-list source (optional override): `$2` - -## 1. Re-check every acceptance criterion against code - -- Read task `$1`'s acceptance criteria from the Phase 0 task list or roadmap (or `$2`). -- For each criterion, confirm the **literal claim** against the code / config that - currently exists on this branch — read the return annotation, the sort call, the guard - position, the `pyproject.toml` value, the actual file contents. **Do not** accept "a test - with that name passes" as evidence. -- Produce a table: criterion → met / not met → the `file:line` that proves it (or the gap). - -## 2. Run the QA gates - -- Run `make qa` (or `make qa-` for each package the branch touches). Record the - result: Pylint score, Black, mypy, coverage % per package. -- Any red gate ⇒ not PR-ready. - -## 3. Run the reviewer agent - -- Run the `reviewer` agent (`.github/agents/reviewer.agent.md`) over the branch diff. -- Fold its Blocker / Important findings into the verdict. Nits are noted, not blocking. - -## 4. Output a single verdict - -Print exactly one of: - -- `PR-READY: yes` — all acceptance criteria met against code, `make qa` green, no reviewer - blockers. Follow with the criterion→`file:line` table. -- `PR-READY: no` — followed by a specific, ordered fix list: each item names the failing - criterion or gate, the `file:line`, and the concrete change needed. - -Do not open the PR, push, or commit. diff --git a/.claude/rules/docs-lifecycle.md b/.claude/rules/docs-lifecycle.md index fd9af4a..345c169 100644 --- a/.claude/rules/docs-lifecycle.md +++ b/.claude/rules/docs-lifecycle.md @@ -11,10 +11,15 @@ When a PR implements a `SPEC.md` section, that same PR must: 1. **Delete** the implemented content from `SPEC.md` (the whole section, or the specific subsections that are now shipped). 2. **Add** the equivalent, "what actually exists" description to the live docs: - - `README.md` for user-facing usage and examples: the action's commands, inputs and output; - - `DEVELOPER.md` for local-dev workflow, the quality gates and CI; - - the package's own `README.md` (`packages/core/README.md`, `apps/cli/README.md`, or a new - package's) for package-specific behaviour. + - `docs/action.md` for the action's commands, inputs, output and run-level errors; + `README.md` only lists them at a glance and links there; + - the package's own `README.md` (`packages/core/README.md`, `apps/cli/README.md`, a + command's `packages/services//README.md`, or a new package's) for + package-specific behaviour; + - `DEVELOPER.md` for local-dev workflow, the quality gates and CI. + + Write the new text to the page rules in `DEVELOPER.md`, "Writing documentation", and add each new + fact's `path::symbol` to the page's code map, the `## Code map` of the `CONTEXT.md` beside its code. This is a **move**, not a copy. After the PR, the information exists in exactly one place — the live docs — and `SPEC.md` is smaller. `SPEC.md` trends toward its process-only skeleton diff --git a/.github/agents/devops-engineer.agent.md b/.github/agents/devops-engineer.agent.md deleted file mode 100644 index 9da3ec1..0000000 --- a/.github/agents/devops-engineer.agent.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -name: DevOps Engineer -description: Keeps CI/CD fast, reliable, and deterministic while enforcing repo quality gates. ---- - -DevOps Engineer - -Purpose - -- Define the agent’s operating contract: mission, inputs/outputs, constraints, and quality bar. - -Writing style - -- Must use short headings and bullet lists. -- Must write rules as constraints — `Must` / `Must not` / `Prefer` / `Avoid`, sentence-leading, no trailing colons. -- Prefer constraints over prose. - -Mission - -- Deliver CI/CD workflows that are fast, reliable, and deterministic while enforcing required quality gates. - -Operating principles - -- Must keep changes small, explicit, and reviewable. -- Prefer correctness and reliability over speed. -- Must avoid nondeterminism and hidden side effects. -- Must keep externally-visible behavior stable unless a contract update is intended. - -Inputs - -- Task description / issue / spec. -- Acceptance criteria. -- Test plan. -- Reviewer feedback / PR comments. -- Repo constraints (linting, style, release process). - -Outputs - -- CI/CD workflow changes (build/test/lint/type/coverage). -- Caching and environment setup improvements. -- Reports/badges when they reduce review or triage time. -- Short final recap (What changed / Why / How to verify). - -Output discipline (reduce review time) - -- Prefer concrete changes over long explanations. -- Prefer linking to workflow files over pasting large YAML blocks. -- Prefer summarizing: goal, diff summary, expected runtime impact (≤ 8 bullets). - -Responsibilities - -- Implementation - - Must keep pipelines deterministic (pin versions where required; avoid flaky steps). - - Prefer incremental improvements (one optimization or guardrail per change). - - Must handle secrets safely; avoid printing credentials or tokens. -- Quality - - Must enforce the repo’s quality gates (format/lint/type/tests/coverage). - - Prefer fast feedback (parallelize where safe; cache dependencies). - - Prefer reducing flakiness before adding more checks. -- Compatibility & contracts - - Must not change externally-visible action outputs or exit codes via CI changes. -- Security & reliability - - Must validate failure modes (timeouts, retries, rate limits) for external calls. - -Collaboration - -- Prefer clarifying acceptance criteria before changing workflows. -- Prefer coordinating with SDET on test execution strategy and flake triage. -- Prefer notifying Reviewer/spec owner when CI changes could affect contracts. - -Definition of Done - -- Acceptance criteria met. -- CI is consistently green, fast, and yields actionable logs. -- Pipelines are faster or more reliable without reducing gate coverage. -- Final recap provided in required format. - -Non-goals - -- Must not redesign CI architecture unless explicitly requested. -- Avoid introducing new tools or dependencies without justification. -- Must not broaden scope beyond the task. - -Repo specifics - -- Runtime/toolchain targets - - Python 3.10+ (supported floor); the CI matrix runs `3.10` through `3.14`. -- Quality gates (use the `Makefile` targets — `.github/workflows/test.yml` and `integration.yml` run the same) - - Full gate, all packages: `make qa` - - Per package: `make qa-` (`` ∈ `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix`, `cli`) - - Individual: `make lint-` (Pylint ≥ 9.5), `make format-check-` (Black, line length 120), `make types-` (mypy), `make deptry-` (deptry), `make test-`, `make coverage-` (`--cov-fail-under=80`) - - Whole repository: `make no-vendored-schemas` (R12 check 1, in `make qa`) - - Must keep the per-package invocation (each gate runs from inside the package directory) — do not flatten it to a repo-root command. -- Workflow set - - `test.yml` (per-package QA), `integration.yml` (the action's venv jobs, run through `uses: ./`), `release_draft.yml`, `check_pr_release_notes.yml`, `dependabot.yml` (auto-merge). - - Must pin every `uses:` to a full commit SHA with a trailing `# vX.Y.Z` comment, and Must keep one SHA per action across all workflow files. -- Dependencies - - Editable install order matters — `core` → `packages/services/normalize_docs` → `apps/cli`; `make install` and the CI install step follow it. - - Runtime dependencies are declared in each package's `pyproject.toml`; the root `requirements.txt` lists the package directories, which the action installs, and `requirements-dev.txt` adds the pinned development tools. -- Contract-sensitive outputs - - CLI exit codes and error-message prefixes; the per-package `--cov-fail-under=80` gate. diff --git a/.github/agents/reviewer.agent.md b/.github/agents/reviewer.agent.md deleted file mode 100644 index 5e1bf9a..0000000 --- a/.github/agents/reviewer.agent.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: Reviewer -description: Guards correctness, performance, and contract stability; approves only when all gates pass. ---- - -Reviewer - -Purpose - -- Define the agent’s operating contract: mission, inputs/outputs, constraints, and quality bar. - -Writing style - -- Must use short headings and bullet lists. -- Must write rules as constraints — `Must` / `Must not` / `Prefer` / `Avoid`, sentence-leading, no trailing colons. -- Prefer constraints over prose. - -Mission - -- Deliver concise, high-signal PR reviews that protect correctness, security, tests, maintainability, and contracts. - -Operating principles - -- Must keep feedback small, explicit, and reviewable. -- Prefer correctness and maintainability over speed. -- Must avoid nondeterminism and hidden side effects. -- Must keep externally-visible behavior stable unless a contract update is intended. - -Inputs - -- Task description / issue / spec. -- Acceptance criteria. -- Test plan and CI results. -- Reviewer feedback / prior PR comments (if any). -- Repo constraints (linting, style, release process). - -Outputs - -- Review comments grouped by severity. -- Approve / request changes with a clear, minimal fix path. -- Short final recap when asked. - -Output discipline (reduce review time) - -- Prefer short reviews (≤ 8 bullets total). -- Must group comments by severity: Blocker (must fix), Important (should fix), Nit (optional). -- Prefer grouping feedback counts: Blocker/Important (≤ 5) and Nit (≤ 3). -- Prefer pointing to file + line range + symbol over rewriting code. -- Must not produce long audit reports unless explicitly requested. - -Responsibilities - -- Implementation - - Must validate behavior against acceptance criteria and contracts. - - Prefer identifying the smallest safe change that fixes the issue. -- Acceptance-criteria verification - - Must verify each acceptance criterion against the literal code path that satisfies it — not against a test name, a test that is green, or the PR description. - - Must read the actual function body, return annotation, sort call, guard, or output string named by the criterion and confirm it does what the criterion claims. - - Must treat a passing test whose name matches the criterion as insufficient on its own; the test can be wrong, stale, or asserting something weaker than the criterion. - - Prefer quoting the file + line range of the code that satisfies (or fails) each criterion in the review. - - Worked examples - - Criterion "issues are returned sorted by number descending" → open the function, find the `sorted(...)` / `.sort(...)` call, confirm `reverse=True` (or a descending key) and that nothing re-orders the list afterwards. A green `test_sorted_descending` is not the check. - - Criterion "a cache hit skips the GitHub API call" → confirm the cache lookup and its early return occur *before* the API client call in the function body, not merely that a mock was asserted not-called in one test. - - Criterion "hyphenated input names are normalized to underscores" → confirm the real transformation in `get_action_input` (`replace("-", "_")` / equivalent) and that the env var name built from it is `INPUT_`. -- Quality - - Must verify format/lint/type/test/coverage gates are satisfied. - - Prefer requesting targeted tests for uncovered failure paths. -- Compatibility & contracts - - Must flag changes to externally-visible outputs (strings, exit codes, schemas). - - Must require explicit approval and test updates for contract changes. -- Security & reliability - - Must flag unsafe input handling, secrets exposure, auth/authz issues, and insecure defaults. - -Collaboration - -- Prefer asking targeted questions when context is missing. -- Prefer coordinating with SDET when test coverage or determinism is uncertain. -- Prefer aligning with spec owner when a contract change is proposed. - -Definition of Done - -- Review is concise and actionable. -- High-risk issues are flagged with clear impact and fix suggestions. -- Approval only when quality gates pass and contracts are respected. - -Non-goals - -- Must not request refactors unrelated to the PR’s intent. -- Avoid bikeshedding formatting if automated tools handle it. -- Avoid architectural rewrites unless explicitly requested. - -Repo specifics - -- Review modes - - Prefer following the repo’s review rubric in `.github/copilot-review-rules.md` (Blocker/Important/Nit, Default vs Double-check). -- Acceptance-criteria verification — toolkit examples - - Criterion "only the first failing step's code is reported" → open `living_doc_core/input_validation.py`, confirm `validate_inputs` raises after each step that found a problem, before the next step runs, and that each step collects every occurrence before it raises. - - Criterion "an unfamiliar producer version passes without a warning" → confirm nothing in `living_doc_core` reads `metadata.producer.version` and that the validation logs and warns nothing. - - Criterion "`requires-python` is `>=3.10` in every package" → open each package's `pyproject.toml` plus the root and read the literal `requires-python` / `[tool.black] target-version` / `[tool.mypy] python_version` values; a green build on one interpreter is not the check. -- Contract-sensitive outputs - - CLI argument names and defaults; the action's inputs and `output-path` output; `main.py`'s error lines. - - The shared input validation's codes and order, which are `living-doc-utilities`' (`docs/contracts/errors.md`); no local contract model or committed schema. -- High-risk areas - - The shared input validation (`packages/core/src/living_doc_core/input_validation.py`) and the reader table (`readers.py`). - - The action's input checks and their order (`main.py`). - - Package dependency rules — no `apps/cli` import from `core`; every import declared (`make deptry`). - - Logging — avoid leaking tokens/PII; keep the pipeline AI-free and offline. diff --git a/.github/agents/sdet.agent.md b/.github/agents/sdet.agent.md deleted file mode 100644 index 344fd05..0000000 --- a/.github/agents/sdet.agent.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -name: SDET -description: Ensures automated test coverage, determinism, and fast feedback across the codebase. ---- - -SDET (Software Development Engineer in Test) - -Purpose - -- Define the agent’s operating contract: mission, inputs/outputs, constraints, and quality bar. - -Writing style - -- Must use short headings and bullet lists. -- Must write rules as constraints — `Must` / `Must not` / `Prefer` / `Avoid`, sentence-leading, no trailing colons. -- Prefer constraints over prose. - -Mission - -- Deliver deterministic automated tests that validate contracts and provide fast feedback. - -Operating principles - -- Must keep changes small, explicit, and reviewable. -- Prefer correctness and maintainability over speed. -- Must avoid nondeterminism and hidden side effects. -- Must keep externally-visible behavior stable unless a contract update is intended. - -Inputs - -- Task description / issue / spec. -- Acceptance criteria. -- Test plan. -- Reviewer feedback / PR comments. -- Repo constraints (linting, style, release process). - -Outputs - -- Focused tests for new/changed behavior (unit by default). -- Minimal test fixtures and helpers. -- Coverage signals and actionable failure reproduction steps. -- Short final recap (What changed / Why / How to verify). - -Output discipline (reduce review time) - -- Prefer the smallest number of tests that prove the contract. -- Prefer ≤ 3 focused tests per change unless risk requires more. -- Prefer tests that cover success + failure paths. -- Avoid large fixtures; reuse shared fixtures when possible. -- Avoid long explanations; summarize what each new test asserts. - -Responsibilities - -- Implementation - - Must add/adjust tests for changed behavior and edge cases. - - Prefer unit tests; add integration tests only when the boundary behavior is the change. -- Quality - - Must keep tests deterministic (no timing dependence; stable ordering; fixed clocks when needed). - - Must isolate I/O and external calls behind mocks/fakes. -- Compatibility & contracts - - Must protect contract-sensitive outputs with tests when they matter. -- Security & reliability - - Must avoid real network calls in unit tests. - - Must avoid leaking secrets in test logs or fixtures. - -Collaboration - -- Prefer clarifying ambiguous acceptance criteria with the spec owner. -- Prefer pairing with Senior Developer on test-first for complex logic. -- Prefer providing Reviewer with minimal reproductions for failures. - -Definition of Done - -- Acceptance criteria covered by tests. -- Tests are deterministic and fast. -- Quality gates pass. -- Final recap provided in required format. - -Non-goals - -- Avoid broad refactors of the test suite unrelated to the change. -- Avoid adding new dependencies unless justified and compatible. -- Must not broaden scope beyond the task. - -Repo specifics - -- Test locations - - Per-package tests: `packages//tests/` and `apps/cli/tests/`, mirroring each `src/` tree. - - The action: repo-root `tests/` (the entry point and the venv step); the contract fixtures: repo-root `tests/fixtures/`. - - Shared fixtures: each package's own `tests/conftest.py`. -- Coverage target - - Must keep coverage ≥ 80% per package when running `make coverage-` / `make qa-`. -- Mocking rules - - Must isolate file I/O in unit tests; the pipeline makes no network calls, so there is nothing external to stub. - - Must not read the ambient filesystem outside `tmp_path` / fixtures. -- Concrete mock/fixture targets for this repo — see `.claude/agents/test-author.md`. diff --git a/.github/agents/senior-developer.agent.md b/.github/agents/senior-developer.agent.md deleted file mode 100644 index 4ede7e8..0000000 --- a/.github/agents/senior-developer.agent.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: Senior Developer -description: Implements features and fixes with high quality, meeting specs and tests. ---- - -Senior Developer - -Purpose - -- Define the agent’s operating contract: mission, inputs/outputs, constraints, and quality bar. - -Writing style - -- Must use short headings and bullet lists. -- Must write rules as constraints — `Must` / `Must not` / `Prefer` / `Avoid`, sentence-leading, no trailing colons. -- Prefer constraints over prose. - -Mission - -- Deliver maintainable features and fixes that meet acceptance criteria and pass quality gates. - -Operating principles - -- Must keep changes small, explicit, and reviewable. -- Prefer correctness and maintainability over speed. -- Must avoid nondeterminism and hidden side effects. -- Must keep externally-visible behavior stable unless a contract update is intended. - -Inputs - -- Task description / issue / spec. -- Acceptance criteria. -- Test plan. -- Reviewer feedback / PR comments. -- Repo constraints (linting, style, release process). - -Outputs - -- Focused code changes (prefer PRs over patches when applicable). -- Tests for new/changed logic (unit by default; integration/e2e as required). -- Minimal documentation updates when behavior/contracts change. -- Short final recap (What changed / Why / How to verify). - -Output discipline (reduce review time) - -- Prefer code changes over long explanations. -- Avoid large pasted code blocks unless requested. -- Must keep final recap ≤ 10 lines unless explicitly asked for more detail. - -Responsibilities - -- Implementation - - Must follow repository patterns and existing architecture. - - Must keep modules testable; isolate I/O and external calls behind boundaries. - - Avoid unnecessary refactors unrelated to the task. -- Quality - - Must meet formatting, lint, type-check, and test requirements. - - Must add type hints for new public APIs. - - Must use the repo logging framework (no print). -- Compatibility & contracts - - Must not change externally-visible outputs unless approved. - - If a contract change is required, must document it and update tests accordingly. -- Security & reliability - - Must handle inputs safely; avoid leaking secrets/PII in logs. - - Prefer validating retries/timeouts/failure-modes when external systems are involved. - -Collaboration - -- Prefer clarifying acceptance criteria before implementation if ambiguous. -- Prefer coordinating with SDET for complex/high-risk logic. -- Must address reviewer feedback quickly and precisely. -- If tradeoffs exist, prefer presenting options with impact. - -Definition of Done - -- Acceptance criteria met. -- All quality gates pass per repo policy. -- Tests added/updated for changed logic and edge cases. -- No regressions introduced; behavior stable unless intentionally changed. -- Docs updated where needed. -- Final recap provided in required format. - -Non-goals - -- Must not redesign architecture unless explicitly requested. -- Must not introduce new dependencies without justification and compatibility check. -- Must not broaden scope beyond the task. - -Repo specifics - -- Runtime/toolchain targets - - Python 3.10+ (supported floor set by `requires-python = ">=3.10"`); the CI matrix runs `3.10` through `3.14`. - - Must keep `X | Y` unions and other 3.10-native syntax; Must guard any 3.11+ standard-library use behind a `sys.version_info` fallback. -- Monorepo rules - - Must respect the dependency rules — `core` depends only on `living-doc-utilities`; a service package under `packages/services/` depends on `core` and `living-doc-utilities`; `apps/cli` depends on `click`, the services it registers, `core` and `living-doc-utilities`. - - Prefer a change that touches one package; add a dependency in the owning package's `pyproject.toml`. -- Logging conventions - - Must use lazy `%` formatting in logs; must not use f-strings in logging calls. - - Prefer wiring loggers via `living_doc_core.logging_config`. -- Imports - - Must keep Python imports at top of file (not inside functions/methods). -- Quality gates (use the `Makefile` targets — `.github/workflows/test.yml` runs the same) - - Full gate, all packages: `make qa` - - Per package: `make qa-` (`` ∈ `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix`, `cli`) - - Individual: `make format-` (check-only `make format-check-`), `make lint-` (Pylint ≥ 9.5), `make types-`, `make deptry-`, `make test-`, `make coverage-` (`--cov-fail-under=80`) -- Contract-sensitive outputs - - CLI argument names/defaults; the action's inputs and `output-path` output; `main.py`'s error lines. - - The shared input validation's codes and order, which are `living-doc-utilities`' (`docs/contracts/errors.md`). - - Must not define a contract model or commit a schema — the contracts are `living-doc-utilities`'; read and write them only through `read_artifact` / `write_artifact`. -- AI-free principle - - Must keep every collect → normalize → generate step deterministic and offline — no LLM call and no network request in the runtime path. diff --git a/.github/agents/specification-master.agent.md b/.github/agents/specification-master.agent.md deleted file mode 100644 index fb3ef01..0000000 --- a/.github/agents/specification-master.agent.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: Specification Master -description: Produces precise, testable specs and maintains repo documentation as the contract source of truth. ---- - -Specification Master - -Purpose - -- Define the agent’s operating contract: mission, inputs/outputs, constraints, and quality bar. - -Writing style - -- Must use short headings and bullet lists. -- Must write rules as constraints — `Must` / `Must not` / `Prefer` / `Avoid`, sentence-leading, no trailing colons. -- Prefer constraints over prose. - -Mission - -- Deliver precise, testable specifications with acceptance criteria and a verification plan. - -Operating principles - -- Must keep changes small, explicit, and reviewable. -- Prefer correctness and maintainability over speed. -- Must avoid nondeterminism and hidden side effects. -- Must keep externally-visible behavior stable unless a contract update is intended. - -Inputs - -- Task description / issue / spec request. -- Acceptance criteria needs (what must be true to ship). -- Test plan needs (unit/integration/e2e scope). -- Reviewer feedback / PR comments. -- Repo constraints (linting, style, release process). - -Outputs - -- Acceptance criteria (verifiable and contract-focused). -- Verification plan (tests to add/update; commands to run). -- Edge cases and failure modes. -- Minimal documentation updates when contracts change. -- Short final recap (What changed / Why / How to verify) when asked. -- Spec content - - Must include scope, inputs/outputs, invariants, edge cases, and how it will be tested. - - Prefer including contract-sensitive strings/exit codes when they are part of the behavior. - - Prefer including alternatives/rollout notes only when they reduce rework. - -Output discipline (reduce review time) - -- Prefer scan-friendly specs (bullets over prose). -- Must define success and failure paths. -- Prefer including concrete examples only when they reduce rework. -- Verbosity levels - - Prefer brief specs (≤ 40 lines) for small changes. - - Prefer standard specs (≤ 120 lines) for typical changes. - - Prefer detailed specs only for ambiguous or high-risk work. - -Responsibilities - -- Implementation - - Must define inputs/outputs, invariants, and expected error handling. - - Prefer specifying contract-sensitive strings and exit codes when relevant. -- Quality - - Must make checks testable and traceable to acceptance criteria. - - Prefer aligning acceptance criteria with existing repo patterns. -- Minimum structure - - Prefer an overview/scope section. - - Prefer a glossary/invariants section when terminology is ambiguous. - - Prefer interfaces/contracts (APIs, CLI, env vars) with expected errors. - - Prefer algorithms/rules with determinism and performance notes. - - Prefer phase-by-phase acceptance criteria linked to tests. -- Compatibility & contracts - - Must keep contracts stable unless an intentional change is approved. - - If a contract change is required, must document it and require test updates. -- Security & reliability - - Prefer calling out secrets handling, safe logging, and external call failure modes. - -Collaboration - -- Prefer clarifying scope and constraints before implementation starts. -- Prefer coordinating with SDET to translate specs into tests. -- Prefer pre-briefing Reviewer on contract changes and tradeoffs. - -Definition of Done - -- Acceptance criteria are unambiguous and testable. -- Verification plan is actionable. -- Contract changes (if any) are documented and include a test update plan. -- Final recap provided when requested. - -Non-goals - -- Must not redesign architecture unless explicitly requested. -- Avoid broad documentation rewrites unrelated to the task. -- Must not broaden scope beyond the task. - -Repo specifics - -- Spec locations - - Prefer `SPEC.md` for prospective (not-yet-built) commands, packages, and external-contract changes; follow `.claude/rules/docs-lifecycle.md` — an implemented section moves out of `SPEC.md` into the live docs in the same PR. - - Prefer `README.md` for the action's commands, inputs and output, each package's `README.md` for package behavior, `DEVELOPER.md` for local-dev workflow, and `living-doc-utilities`' `docs/contracts/` for the contracts themselves. -- Contract-sensitive outputs - - CLI argument names and defaults; the action's inputs and `output-path` output; `main.py`'s error lines. - - The shared input validation's codes and order, which are `living-doc-utilities`' (`docs/contracts/errors.md`). -- High-risk areas - - The shared input validation in `living_doc_core.input_validation` — its step order, and every occurrence of the first failing step reported. - - The reader table in `living_doc_core.readers` — keyed by contract name, every read through `read_artifact`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 9f03b0d..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,150 +0,0 @@ -# Copilot Instructions — Living Documentation Toolkit - -This file tells a coding agent how to work in this repository. It describes this repo's -own layout, contracts, and workflow; it is not shared with or copied from other repos. - -**Section order** — keep the sections below in exactly this order: -Overview → Repo specifics → Coding guidelines → Inputs → Language and style → -Logging and string formatting → Docstrings and comments → Patterns → Testing → -Tooling and quality gates → Common pitfalls → Learned rules. - -**House rules for this file** - -- Must write every guidance bullet as a constraint led by one of `Must`, `Must not`, `Prefer`, `Avoid`. -- Must not put a colon after the leading keyword, and Must not use any other keyword style such as `Do`, `Should`, or a two-keyword `Do` / `Avoid` variant. -- Prefer bullet lists over paragraphs. -- Must end the file with a single trailing newline. - -## Overview - -`Living Documentation Toolkit` is a **monorepo** of Python packages behind one CLI -(`living-doc`) and one composite GitHub Action. It sits between the upstream `living-doc-*` -collector actions and the downstream generator actions: it reads collector output as -`living-doc-utilities` contract artifacts, validates a command's inputs together, and -transforms them into the contracts the generators read. `normalize-docs`, `normalize-tests` and -`coverage-matrix` run. - -- Must treat the `living-doc` CLI running as a step in a GitHub Actions workflow as the supported path; running `living-doc ` locally is a development and debugging affordance only. -- Must keep the whole collect → normalize → generate pipeline AI-free — deterministic Python only, no LLM call and no network call anywhere in that path. -- Must scope a change to a single package where possible; split a change that spans packages unless they must change together. -- Must respect the package dependency rules — `core` depends only on `living-doc-utilities`; a service package under `packages/services/` depends on `core` and `living-doc-utilities`; `apps/cli` depends on `click`, the services it registers, `core` and `living-doc-utilities`. - -## Repo specifics - -Module map — five packages under `packages/` and `apps/`, each with its own `pyproject.toml`, -`src/` layout, and `tests/` directory: - -| Path | Package (`pip` name) | Responsibility | -|---|---|---| -| `packages/core` | `living-doc-core` | `logging_config.py` (`setup_logging`), `envelope.py` (`PRODUCER_NAME`, `TOOLKIT_VERSION`, `forwarded`, the producer identity and warning forwarding every transform shares), `readers.py` (the reader table, `read_documentation`, `read_tests`), `input_validation.py` (`validate_inputs`, the shared input validation), `views.py` (`select_records`, the view filter), `run_context.py` (`run_context`, the CI run for `metadata.run`). Depends on `living-doc-utilities==0.8.0`. | -| `packages/services/normalize_docs` | `living-doc-service-normalize-docs` | `builder.py` (`build_generator_ready`), `lineage.py` (`LINEAGE_TABLE`), `service.py` (`normalize_docs`, the command, and `check_no_field_loss`). | -| `packages/services/normalize_tests` | `living-doc-service-normalize-tests` | `builder.py` (`build_ui_test_catalog`), `lineage.py` (`LINEAGE_TABLE`), `service.py` (`normalize_tests`, the command, and the runtime field-loss check). | -| `packages/services/coverage_matrix` | `living-doc-service-coverage-matrix` | `builder.py` (`build_coverage_matrix`), `lineage.py` (`LINEAGE_TABLE`, over both inputs), `service.py` (`coverage_matrix`, the command, and `check_no_field_loss`, against the documentation input only). | -| `apps/cli` | `living-doc-cli` | CLI entry point — `main.py` (`cli()` click group), one module per command under `commands/`. Registers `normalize-docs`, `normalize-tests` and `coverage-matrix`; `coverage-matrix`'s `--fail-under` gate is the CLI's, after the write. | - -- Must treat the repository root as the action — `action.yml`, its entry point `main.py` (input checks, `COMMAND_INPUTS`), and the root `tests/`. -- Must treat `apps/cli/src/living_doc_cli/main.py` `cli()` as the CLI entry point — each command is registered there as it lands. -- Must read every contract artifact through `living_doc_core.readers`, which calls `living-doc-utilities`' `read_artifact`, and must write one only through `write_artifact` (R12 check 2). -- Must run `living_doc_core.input_validation.validate_inputs` over a command's inputs before it transforms them. -- Must select view records only through `living_doc_core.views.select_records`, and Must keep each command's lineage table in its own service package, checked by `assert_complete` in its tests and never at run time; `normalize-tests` has no view input and always writes `document.view` as `inner`. -- Must declare the toolkit's one version in every package, and Must read `metadata.producer.version` from the installed `living-doc-core`, never from a literal. - -Inputs — the action's inputs (full reference in [`README.md`](../README.md#inputs)): - -| Command | Inputs | -|---|---| -| `normalize-docs` | `input` (req, documentation), `output` (req), `view`, `document-title` | -| `normalize-tests` | `input` (req, tests), `output` (req) | -| `coverage-matrix` | `input` (req, documentation, `--doc-input`), `output` (req), `view`, `tests-input` (req), `fail-under` | - -Contract-sensitive outputs: - -- Must keep the action's input names, its `output-path` output, and `main.py`'s one-line error messages stable — `tests/test_main.py` asserts them exactly. -- Must keep the shared input validation's codes and order those of `living-doc-utilities`' `docs/contracts/errors.md` § Shared input validation order. -- Must not define a contract model or commit a contract schema — the models and schemas are `living-doc-utilities`'; `make no-vendored-schemas` fails on a committed `*-schema.json` / `*.schema.json`. -- Must keep CLI argument names and defaults stable once a command ships. - -QA commands — the per-package `Makefile` targets (`.github/workflows/test.yml` runs the same targets): - -- Full gate, every package: `make qa` (runs `format-check` → `lint` → `types` → `deptry` → `test` per package, then the root checks, then `no-vendored-schemas`, failing on the first). -- Per package: `make qa-` where `` is one of `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix`, `cli`. -- Individual gate per package: `make lint-`, `make format-`, `make format-check-`, `make types-`, `make deptry-`, `make test-`, `make coverage-`. -- `lint-` runs ruff (`E` / `F` / `I` / `B`, `tests` excluded per each package's `[tool.ruff]`) then Pylint; `format-` runs `ruff check --fix` then Black. -- Must not use repo-root `pylint $(git ls-files '*.py')` / `pytest tests/` as the QA command — CI gates changes per package from inside each package directory. - -## Coding guidelines - -- Must keep changes small and scoped to one package where possible. -- Prefer explicit code over clever constructs. -- Must keep externally visible behaviour stable unless the task is an intentional contract change. -- Must not change existing error messages or log texts without a stated reason. -- Prefer pure functions for parsing, matching, and tallying logic, and Avoid adding I/O or environment reads to the modules that are pure today. - -## Inputs - -- Must parse CLI arguments only in the command layer of `apps/cli` and pass typed values on. -- Must read input files only through `living_doc_core.readers` and validate them only through `living_doc_core.input_validation`, not scattered across the transform modules. -- Avoid duplicating validation that `living-doc-utilities` already runs — R5 belongs to `read_artifact`. - -## Language and style - -- Must target Python 3.10+ — the ecosystem floor set by `requires-python = ">=3.10"` and the `3.10 → 3.14` CI matrix. -- Must keep `X | Y` union annotations and other 3.10-native syntax; Must guard any 3.11+ standard-library use (`tomllib`, `datetime.UTC`, `enum.StrEnum`, `match`/`case` is fine on 3.10) behind a `sys.version_info` fallback. -- Must add type hints for new public functions and classes. -- Must keep imports at module top — no imports inside functions or methods. -- Must not disable a linter rule inline unless this file records the exception under Learned rules. - -## Logging and string formatting - -- Must use `logging` via `living_doc_core.logging_config`, never `print`. -- Must use lazy `%` formatting in logging calls — `logger.info("msg %s", value)`. -- Must not use f-strings inside logging calls. -- Prefer the clearest formatting for exception and failure messages, and Must keep the contract-sensitive error lines exact. - -## Docstrings and comments - -- Must match the existing module docstring style — a short summary of what the module contains. -- Prefer a one-line docstring summary for functions, expanding only where it adds information. -- Prefer self-explanatory code, and Prefer comments only for intent, edge cases, and the "why". -- Avoid tutorial-style prose or long examples in docstrings. - -## Patterns - -- Prefer the contract models of `living-doc-utilities` over any local representation of a contract. -- Must key a new input contract in the reader table by its contract name, never by its producer. -- Must report every occurrence of the first failing validation step, never the first occurrence only, and never pick a winner between two occurrences. -- Must keep integration boundaries — file I/O, contract reads and writes, the CLI the action runs — explicit and mockable. - -## Testing - -- Must use `pytest` with `pytest-cov`, and Must not use `unittest`. -- Must keep tests in each package's own `tests/` directory, mirroring its `src/` layout; the action's entry point and venv step are tested in the repo-root `tests/`. -- Must test behaviour — return values, the contract model a read returns, raised errors, each `InputProblem`'s code and message, exit codes, error lines. -- Must read contract inputs from the one fixture copy under the repo-root `tests/fixtures/`, or from a changed copy of it under `tmp_path`. -- Prefer shared fixtures in each package's `tests/conftest.py`. - -## Tooling and quality gates - -- Must run `make qa` (or `make qa-` for the package you touched) before finishing a code change — it runs `format-check` → `lint` → `types` → `deptry` → `test` and fails on the first failing gate. -- Must use the individual per-package targets while iterating — `make format-`, `make lint-`, `make types-`, `make deptry-`, `make test-`, `make coverage-`. -- Must keep `lint` clean per package — it runs ruff (`E` / `F` / `I` / `B`, `tests` excluded) then Pylint, and Pylint must score 9.5 or higher. -- Must keep `format-check` (Black, line length 120, config in each `pyproject.toml`) clean, and Prefer `make format-` (ruff autofix + Black) to fix import order and formatting in one step. -- Must keep `types` (mypy, config in each `pyproject.toml`) clean, and Prefer fixing types over adding ignores. -- Must keep `deptry` clean per package — every import declared in its `pyproject.toml`, every declared dependency imported. -- Must keep `coverage` (pytest, `--cov-fail-under=80`) passing per package. -- Must expect `.github/workflows/test.yml` to call the same `make` targets, so local and CI never drift. - -## Common pitfalls - -- Must verify a new dependency supports Python 3.10 before adding it, and Must add it to the owning package's `pyproject.toml` (the root `requirements.txt` lists the package directories, not their dependencies; `requirements-dev.txt` adds the development tools). -- Must keep a change within the package dependency rules — Avoid importing `apps/cli` from `core`. -- Must remove unused imports and variables in the same change, and Avoid leaving dead code. -- Avoid changing externally visible strings or exit codes unless the task calls for it and the version impact is stated. -- Must not pin `living-doc-utilities` to another version in one package only — every package that imports it pins the same version. - -## Learned rules - -- Must keep `main.py`'s check order — a known command, then an input it does not take, then an empty required input, then `output`, then `fail-under` — so each run fails on the first wrong input with one line. -- Prefer the inline `# pylint: disable=too-many-arguments,too-many-positional-arguments` on a click command function only — click passes each of its options as one parameter. -- Must keep `--verbose` the group's option only — `main.py` passes it before the command name, so no command declares its own. -- Must treat `metadata.producer.version` as audit information — never compare it, and never warn on an unfamiliar one. diff --git a/.github/copilot-review-rules.md b/.github/copilot-review-rules.md index 91b0402..fda23af 100644 --- a/.github/copilot-review-rules.md +++ b/.github/copilot-review-rules.md @@ -67,6 +67,12 @@ repo's own risk areas and review expectations; it is not shared with other repos - Must not bikeshed formatting that Black or Pylint already enforces. - Avoid proposing architectural rewrites unless they are explicitly requested. +## Documentation + +- Must review a change to `README.md`, `DEVELOPER.md`, `CONTRIBUTING.md`, `SPEC.md`, `docs/action.md` or a package page (`apps/cli/README.md`, `packages/core/README.md`, `packages/services//README.md`) against `DEVELOPER.md`, "Writing documentation" — that section is the only copy of the page rules; Must not restate them here. +- Must check a sample of ten decision- or fact-list items per changed page against the code its code map names (the `## Code map` of the `CONTEXT.md` beside the code), and flag any item the code does not bear out. +- Must flag a changed fact whose page and code map no longer agree. + ## Repo specifics - Must treat these as high-risk areas — the shared input validation in `packages/core/src/living_doc_core/input_validation.py` (its step order, and every occurrence of a step reported), the reader table in `readers.py`, and the action's input checks in `main.py`. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bffc76..4f77a21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,7 @@ jobs: runs-on: ubuntu-latest outputs: python_changed: ${{ steps.changes.outputs.python_changed }} + docs_changed: ${{ steps.changes.outputs.docs_changed }} steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -70,6 +71,16 @@ jobs: echo "python_changed=false" >> "$GITHUB_OUTPUT" fi + # A Markdown, `action.yml` or other non-Python file the pages anchor runs the tests job too: tests/docs/ + # checks every tracked page, its anchors, examples and option lists against `action.yml`, and the root tests + # read `action.yml`, the shell script and its example configuration. tests/docs/test_ci_filter.py keeps this + # list complete. + if echo "$changed_files" | grep -E '(\.([Mm][Dd]|[Mm][Aa][Rr][Kk][Dd][Oo][Ww][Nn])$|^action\.yml$|\.sh$|\.example$|^\.github/(dependabot\.yml|workflows/[^/]+\.ya?ml)$)' > /dev/null; then + echo "docs_changed=true" >> "$GITHUB_OUTPUT" + else + echo "docs_changed=false" >> "$GITHUB_OUTPUT" + fi + static-checks: needs: detect if: needs.detect.outputs.python_changed == 'true' @@ -114,7 +125,7 @@ jobs: tests: needs: detect - if: needs.detect.outputs.python_changed == 'true' + if: needs.detect.outputs.python_changed == 'true' || needs.detect.outputs.docs_changed == 'true' runs-on: ubuntu-latest name: Python Tests (Python ${{ matrix.python-version }}) strategy: diff --git a/.gitignore b/.gitignore index 0495547..ca6cc0c 100644 --- a/.gitignore +++ b/.gitignore @@ -216,7 +216,7 @@ outputs/ /doc-source.json /coverage-matrix.json # local-integration.sh: its configuration (the committed `.env.local.example` stays tracked), its own venv and its -# default output; DEVELOPER.md § Run CLI Locally writes to `output/` too. +# default output; DEVELOPER.md § CLI run writes to `output/` too. .env.local .venv-local-integration/ /output/ diff --git a/AGENTS.md b/AGENTS.md index 1483d94..583e950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,6 @@ Files and parts marked "From agentic-defaults" are replaced when the kit updates - Setup: `make install`: `requirements-dev.txt`, then every package editable with `[dev]` - Run: `living-doc …`, the CLI, after `make install`; the action's entry point: `python3 main.py` with `INPUT_*` variables -- `make test`: pytest with the 80 % coverage gate, every package and the root (one package: `make test-`, alias `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix` or `cli`; the root: `make test-root`) +- `make test`: pytest with the 80 % coverage gate, every package and the root (one package: `make test-`, alias `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix` or `cli`; the root: `make test-root`, the page checks in `tests/docs/` included) - `make lint` and `make format`: Ruff and Pylint (score ≥ 9.5); Ruff autofix and Black (line length 120) - Check, everything that must pass before work is done: `make qa`: format-check, lint, types, deptry and test per package, then the root, then `no-vendored-schemas` diff --git a/CONTEXT.md b/CONTEXT.md index 2784830..78d77a7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -29,9 +29,10 @@ command's inputs together, and writes the contract a generator reads. | `apps/cli/` | `living-doc-cli`: the click group and one module per command under `commands/` ([README](apps/cli/README.md)) | | `main.py`, `action.yml` | The action: `action.yml` builds the venv, `main.py` checks `INPUT_*` and runs `living-doc ` with `-I` | | `local-integration.sh`, `tools/` | The operator's run of the three commands outside any workflow; `tools/local_integration.py` validates and reports each output | -| `tests/` | The root tests (`test_main.py`, `test_action_yml.py`, `test_local_integration.py`) and the one fixture copy under `tests/fixtures/` | -| `.github/workflows/` | `test.yml` runs the `make` targets; `integration.yml` runs the action through `uses: ./` | -| `SPEC.md` | Process document with the pending-specifications list; specs move out as they ship | +| `tests/` | The root tests (`test_main.py`, `test_action_yml.py`, `test_local_integration.py`), the page checks in `tests/docs/`, and the one fixture copy under `tests/fixtures/` | +| `.github/workflows/` | `test.yml` runs the `make` targets, the tests also on a page-only change (`docs_changed`); `integration.yml` runs the action through `uses: ./` | +| `README.md`, `docs/action.md` | The entry page; the action's full reference (inputs, commands, output, run-level errors) | +| `DEVELOPER.md`, `CONTRIBUTING.md`, `SPEC.md` | Solo pages: development, contribution conventions, the spec process with its pending list | ## How it fits together @@ -86,7 +87,20 @@ All owned by `living-doc-utilities`; this repo defines none and commits no schem ## Conventions -- `.github/copilot-instructions.md`: the agent rules for this repo (dependency rules, logging, tests, gates). +- Pages follow `DEVELOPER.md` § Writing documentation, their only copy; `tests/docs/pages.py::APPROVED_PAGES` lists + them, and a new page goes there and is linked from `README.md`. The READMEs beside test data are not pages. +- A page states facts without `path::symbol`; the `CONTEXT.md` beside the code maps them in `## Code map` + (`tests/docs/pages.py::CODE_MAPS`). A changed fact updates both. +- Log through `living_doc_core.logging_config.setup_logging`, never `print`; lazy `%` formatting, no f-string in a + logging call. +- Imports at module top; type hints on new public functions; 3.10 syntax, a 3.11+ standard-library use behind a + `sys.version_info` fallback. +- No inline linter disable, except `too-many-arguments,too-many-positional-arguments` on a click command function + (click passes each option as one parameter) and `no-value-for-parameter` on the `cli()` call in + `apps/cli/src/living_doc_cli/main.py`. +- CLI arguments are parsed only in `apps/cli`'s command layer; builders stay pure, with no I/O or environment read. +- Tests use `pytest`, never `unittest`, in each package's own `tests/`, mirroring its `src/`. +- No error line, log text or exit code changes without a stated reason; the tests assert them exactly. - Read a contract only through `living_doc_core.readers`, validate only through `validate_inputs`, write only through `write_artifact`. - Every package is version `0.1.0` and pins the same `living-doc-utilities`; `metadata.producer.version` comes from @@ -113,3 +127,25 @@ All owned by `living-doc-utilities`; this repo defines none and commits no schem - `tests/test_local_integration.py` runs the real script in a copy whose venv is pre-seeded with the test's own interpreter, so no test installs from the network; `.env.local.example`'s keys must equal the script's `KEYS`. - `.local_context/` is private and never committed. + +## Code map + +What realises each fact of [`docs/action.md`](docs/action.md), the action's reference: + +- Python 3.10 or later on `PATH`, checked before anything is created → `action.yml::sys.version_info` +- The action's own venv under `$RUNNER_TEMP` → `action.yml::living-doc-toolkit-venv` +- Each run installs `requirements.txt` with `pip` → `action.yml::requirements.txt` +- Linux runners only → `.github/workflows/integration.yml::ubuntu-latest` +- The input files come from a collector step → collector-gh +- Every command reads local files only → `packages/core/src/living_doc_core/readers.py::read_documentation` +- The commands, the inputs each takes and the option each becomes → `main.py::COMMAND_INPUTS` +- The input table equals `action.yml` → `tests/docs/test_option_lists.py::test_the_input_table_equals_action_yml` +- `Taken by` equals `COMMAND_INPUTS` → `tests/docs/test_option_lists.py::test_the_taken_by_column_equals_command_inputs` +- An empty input is not passed; `output` reaches the CLI absolute; the input checks and their order → `main.py::build_command_line` +- `verbose-logging` or `RUNNER_DEBUG=1` adds the group's `--verbose` → `main.py::run` +- `output-path`, the absolute path written → `action.yml::output-path` +- No `output-path` below `fail-under` → `tests/test_main.py::test_the_entry_point_ends_a_coverage_matrix_below_fail_under_with_the_clis_line_and_sets_no_output` +- The venv step's error lines, before a venv exists → `tests/test_action_yml.py::test_a_python_below_3_10_fails_with_one_error_line_before_creating_a_venv` +- The `python -m venv` failure line → `tests/test_action_yml.py::test_a_failed_venv_create_fails_with_one_error_line` +- The import-check line, pointing at the runner's pip configuration → `tests/test_action_yml.py::test_packages_missing_from_the_venv_fail_the_import_check_with_one_error_line` +- A failed command's exit code is the step's; any other failure exits 1 → `main.py::run` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 213ad47..84b3874 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,23 @@ # How to Contribute? +## Purpose + +This page is how to contribute: reporting a bug, proposing a feature, the AI-free principle, and naming and +describing branches and pull requests. Anyone who wants to contribute reads it. + +Read before: [README](README.md) · Next: [Developer guide](DEVELOPER.md) + +## Contents + +- [Identifying and Reporting Bugs](#identifying-and-reporting-bugs) +- [Proposing New Features](#proposing-new-features) +- [AI-free Principle](#ai-free-principle) +- [Contributing to Development](#contributing-to-development) +- [Branch Naming](#branch-naming) +- [PR Naming](#pr-naming) +- [PR Description](#pr-description) +- [Target Branches](#target-branches) + ## **Identifying and Reporting Bugs** * **Ensure the bug has not already been reported** by searching our **[GitHub Issues](https://github.com/AbsaOSS/living-doc-toolkit/issues)**. * If you cannot find an open issue describing the problem, use the **Bug report** template to open a new one. Tag it with the **bug** label. @@ -25,7 +43,12 @@ This pipeline runs AI-free; contributions must keep every collect → normalize ## Branch Naming -Branches have to start with one of the allowed prefixes — `feature/`, `fix/`, `docs/`, `chore/` — followed immediately by the issue number, then a kebab-case scope: `/-`. +Branches have to start with one of the allowed prefixes, followed immediately by the issue number, then a kebab-case scope: `/-`. + +- `feature/`: new functionality and enhancements +- `fix/`: bug fixes and defect resolutions +- `docs/`: documentation-only updates; a docs-only PR uses `docs/`, not `feature/` +- `chore/`: maintenance, CI, dependency bumps and non-behavioural refactors Examples: - `feature/542-add-hierarchy-support` @@ -33,13 +56,21 @@ Examples: - `docs/203-contributing-rework` - `chore/318-update-ci-python-version` -Use lowercase kebab-case and reflect the actual scope. The `check-pr-requirements` CI check rejects a branch that has no issue number. +Use lowercase kebab-case, hyphens only, and reflect the actual scope; avoid vague terms such as `update` or `changes`. The `check-pr-requirements` CI check rejects a branch that has no issue number. Rename before pushing if needed: ```shell git branch -m /- ``` +Check the current branch: +```shell +git rev-parse --abbrev-ref HEAD | grep -E '^(feature|fix|docs|chore)/[0-9]+-' \ + || echo 'Branch naming violation (expected /-)' +``` + +`refactor/` and `perf/` may become prefixes later; they are not enforced yet. + ## PR Naming PR titles have to carry the related issue number, using one of these formats: `#123: Title` or `123 - Title`. diff --git a/DEVELOPER.md b/DEVELOPER.md index 7fbba01..560835d 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,359 +1,129 @@ -# Living Documentation Toolkit — Developer Guide - -- [Get Started](#get-started) -- [Monorepo Layout](#monorepo-layout) -- [QA Automation with Make](#qa-automation-with-make) -- [Running Static Code Analysis](#running-static-code-analysis) -- [Run Black Tool Locally](#run-black-tool-locally) -- [Run mypy Tool Locally](#run-mypy-tool-locally) -- [Dependency and Contract Checks](#dependency-and-contract-checks) -- [Running Unit Tests](#running-unit-tests) -- [Running Integration Tests](#running-integration-tests) -- [Code Coverage](#code-coverage) -- [Run CLI Locally](#run-cli-locally) -- [Local Integration Run](#local-integration-run) -- [Action venv](#action-venv) -- [CI](#ci) -- [Branch Naming Convention (PID:H-1)](#branch-naming-convention-pidh-1) +# Living Documentation Toolkit: developer guide -## Get Started +## Purpose -Clone the repository and navigate to the project directory: +This page is how to change this repository: setup, the layout, the quality gates, the test data, the local runs, +the action's venv, CI, and the page rules. Anyone who changes the code or the pages reads it. -```shell -git clone https://github.com/AbsaOSS/living-doc-toolkit.git -cd living-doc-toolkit -``` +Read before: [README](README.md) · Next: [Contributing](CONTRIBUTING.md) -### Set Up Python Environment +## Contents -The supported floor is **Python 3.10** (`requires-python = ">=3.10"` in every package's -`pyproject.toml`); CI runs the `3.10`–`3.14` matrix. Use any interpreter in that range. - -```shell -python3 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -``` +- [Setup](#setup) +- [Layout](#layout) +- [Gates](#gates) +- [Test data](#test-data) +- [CLI run](#cli-run) +- [Debug run](#debug-run) +- [Local Integration Run](#local-integration-run) +- [Action venv](#action-venv) +- [CI](#ci) +- [Writing documentation](#writing-documentation) -### Install All Packages (editable mode) +## Setup -Run `make install` from the repository root. It installs `requirements-dev.txt`, then -reinstalls every package editable with its `[dev]` extra, in dependency order: +1. Clone the repository: `git clone https://github.com/AbsaOSS/living-doc-toolkit.git`, then `cd living-doc-toolkit`. +2. Use Python 3.10 or later; create and activate a venv: `python3 -m venv .venv`, then `source .venv/bin/activate`. +3. Run `make install`: `requirements-dev.txt`, then every package editable with its `[dev]` extra, in dependency order → `Makefile::install` -```shell -pip install -r requirements-dev.txt -pip install -e packages/core[dev] -pip install -e "packages/services/normalize_docs[dev]" -pip install -e "packages/services/normalize_tests[dev]" -pip install -e "packages/services/coverage_matrix[dev]" -pip install -e "apps/cli[dev]" -``` +- The supported floor is 3.10, `requires-python = ">=3.10"` in every package; CI runs 3.10 to 3.14 → `packages/core/pyproject.toml::requires-python` +- Run every install from the repository root: pip reads `./packages/...` against the directory it runs in, not against the file. +- To install the toolkit as the action does, not editable: `pip install -r requirements.txt` in a fresh venv. + | File | What it holds | Who installs it | |---|---|---| -| `requirements.txt` | Runtime only: the `living-doc-utilities==0.8.0` pin, then `./packages/core`, `./packages/services/normalize_docs`, `./packages/services/normalize_tests`, `./packages/services/coverage_matrix`, `./apps/cli`, in dependency order | `action.yml`, into its venv, on every run; the README Quickstart | -| `requirements-dev.txt` | `-r requirements.txt`, the gate and test tools pinned, and `PyYAML` for `tests/test_action_yml.py` | `make install` and CI | +| `requirements.txt` | Runtime only: the `living-doc-utilities==0.8.0` pin, then `./packages/core`, `./packages/services/normalize_docs`, `./packages/services/normalize_tests`, `./packages/services/coverage_matrix`, `./apps/cli`, in dependency order | `action.yml`, into its venv, on every run; a plain install, above | +| `requirements-dev.txt` | `-r requirements.txt`, the gate and test tools pinned, and `PyYAML` for the root tests | `make install` and CI | -- Each package declares its own runtime dependencies in its `pyproject.toml`; `requirements.txt` - names the packages, not their dependencies, with one exception: the `living-doc-utilities` pin. +- Each package declares its own runtime dependencies in its `pyproject.toml`; `requirements.txt` names the packages, not their dependencies, with one exception: the `living-doc-utilities` pin. - Every package declares `living-doc-utilities==0.8.0`, from PyPI, as each imports it; one pin, the same in each. - `requirements.txt` pins the same version, so the file the action installs names the contract version it runs on. -- Every package declares the toolkit's one version, `0.1.0`, the version the action's release tag carries. - - `metadata.producer.version` of a file the toolkit writes is read from the installed `living-doc-core`, so a - release changes every package's version together. -- The development tools live in both places. Each package's `[dev]` extra keeps the ranges that - package needs on its own; `requirements-dev.txt` pins the versions `make install` and CI use. -- Run both installs from the repository root: pip reads `./packages/...` against the directory - it runs in, not against the file. -- `requirements.txt` carries no `-e`. `make install` gives the editable install for development; - the action and the Quickstart need a plain one. - - Why the order in `make install`: a later `pip install -r requirements.txt` would reinstall the - packages non-editable, and one call holding both `./packages/core` and `-e packages/core` - does not resolve. -- A plain install builds each package in place and leaves a `build/` directory beside its `src/`. - Each package's mypy `exclude` skips `build/`. +- Every package declares the toolkit's one version, `0.1.0`, the version the action's release tag carries → `packages/core/src/living_doc_core/__init__.py::__version__` + - A written file's `metadata.producer.version` comes from the installed `living-doc-core`, so a release changes every package's version together. +- The development tools live in both places. Each package's `[dev]` extra keeps the ranges that package needs on its own; `requirements-dev.txt` pins the versions `make install` and CI use. +- `requirements.txt` carries no `-e`. `make install` gives the editable install for development; the action and a plain install need a plain one. + - Why the order in `make install`: a later `pip install -r requirements.txt` would reinstall the packages non-editable. + - Why two calls: one call holding both `./packages/core` and `-e packages/core` does not resolve. +- A plain install builds each package in place and leaves a `build/` directory beside its `src/`; each package's mypy `exclude` skips it → `packages/core/pyproject.toml::build/` - Why: `make types-` runs `mypy .`, which would otherwise find every module twice. -- A new package under `packages/` or `apps/` goes into `requirements.txt` after the packages it - depends on, and a development tool goes into `requirements-dev.txt`, never `requirements.txt`. - - Why: the toolkit's own packages are not on PyPI, so a package that depends on an unlisted one fails - the install with "No matching distribution found"; and the action installs `requirements.txt` on - every run. -- Dependabot covers both files from its `pip` entry on `/`, beside one entry per package - directory, in `.github/dependabot.yml`. - - Each package entry, and the root entry, ignores `living-doc-utilities`: its pin moves by hand, in every package - and in the root `requirements.txt` in one change. - - Why: an entry updates its own directory only, so its bump would move one pin, and the five packages would - no longer install together. A new `living-doc-utilities` is also a contract change, made in its own issue. - -## Monorepo Layout - -| Path | Package | Description | +- A new package under `packages/` or `apps/` goes into `requirements.txt` after the packages it depends on. + - Why: the toolkit's packages are not on PyPI; a package depending on an unlisted one fails with "No matching distribution found". +- A development tool goes into `requirements-dev.txt`, never `requirements.txt`. + - Why: the action installs `requirements.txt` on every run. +- Dependabot covers both files from its `pip` entry on `/`, beside one entry per package directory → `.github/dependabot.yml::pip-root` + - Each package entry, and the root entry, ignores `living-doc-utilities`: its pin moves by hand, in every package and in the root `requirements.txt`, in one change. + - Why: an entry updates only its own directory, so its bump moves one pin and the five packages stop installing together. + - A new `living-doc-utilities` is also a contract change, made in its own issue. + +## Layout + +| Path | Package | What it holds | |---|---|---| | `packages/core` | `living-doc-core` | Logging, the shared envelope, reader table, input validation, view filter and CI run context ([README](packages/core/README.md)) | | `packages/services/normalize_docs` | `living-doc-service-normalize-docs` | The `normalize-docs` transform and its lineage table ([README](packages/services/normalize_docs/README.md)) | | `packages/services/normalize_tests` | `living-doc-service-normalize-tests` | The `normalize-tests` transform and its lineage table ([README](packages/services/normalize_tests/README.md)) | | `packages/services/coverage_matrix` | `living-doc-service-coverage-matrix` | The `coverage-matrix` transform and its lineage table over both inputs ([README](packages/services/coverage_matrix/README.md)) | -| `apps/cli` | `living-doc-cli` | CLI entry point (`living-doc` command group); registers `normalize-docs`, `normalize-tests` and `coverage-matrix` ([README](apps/cli/README.md)) | - -- A package directory `packages//` is named by the group's singular and its name: the distribution - `living-doc--`, with `-` for `_`, and the import `living_doc__`: - `packages/services/normalize_docs` is `living-doc-service-normalize-docs`, imported as - `living_doc_service_normalize_docs`. - - The packages ship only inside the action's release, never on PyPI. The action installs them into its own venv - (`action.yml`), where only the import packages land, beside `living_doc_utilities` and its dependencies; the - `living_doc_` prefix keeps them apart there, and `` is this repository's convention. -- Each command's transform is a package of its own under `packages/services/`, with its lineage table in it. -- The toolkit's packages ship no `py.typed`, so a package that imports another reads its annotations through a - mypy `follow_untyped_imports` override in its `pyproject.toml`, as the root does for `living_doc_core`. - -Each package has its own `pyproject.toml`, `src/` layout, and `tests/` directory. -All quality-gate commands below are designed to run **per-package** from within the -package directory, matching how CI executes them. - -The repository root holds the action: `action.yml`, its entry point `main.py`, and the root -`tests/` for both. The root gates run from the repository root with the root `pyproject.toml` -(see [Root checks](#root-checks)). - -`tests/fixtures/collector-gh/` holds the one copy of `living-doc`'s example `doc-source.json` and -`ui-tests.json`, with their source noted beside them; every package's tests and `integration.yml` -read it. `tests/fixtures/utilities-golden/` holds `living-doc-utilities`' six golden entities, which -`packages/services/normalize_docs`' tests read. - -## QA Automation with Make - -This project includes a `Makefile` exposing the shared **command vocabulary** -(the fleet-wide `repo-conventions.md` §7 "Command vocabulary"). -Use the Make targets instead of manual loops — `.github/workflows/test.yml` runs the same -targets, so local and CI never drift. - -### Quick Start - -First, install all packages with dev dependencies: - -```shell -make install -``` - -Then run every QA gate (`format-check` → `lint` → `types` → `deptry` → `test`) on all packages, -then the [root checks](#root-checks), then `no-vendored-schemas`: - -```shell -make qa -``` - -Show all available commands: - -```shell -make help -``` - -### Canonical targets - -Eight package targets, each with an **aggregate** form (every package) and a **per-package** -form `-`, and one repository-wide target. - -| Target | Runs | Gate | -|--------|------|------| -| `qa` | `format-check` → `lint` → `types` → `deptry` → `test` per package, then `qa-root`, then `no-vendored-schemas`, failing on the first | all of the below | -| `lint` | Pylint over tracked `*.py` (tests excluded) | score ≥ **9.5** / 10 | -| `format` | Black, rewriting files | line length **120** | -| `format-check` | Black `--check` (no writes) | line length **120** | -| `types` | mypy | clean | -| `deptry` | deptry, per package (no root form) | no undeclared, unused or transitive dependency | -| `test` | pytest | pass | -| `coverage` | pytest with coverage measurement | `--cov-fail-under=80` | -| `no-vendored-schemas` | R12 check 1 over the whole repository (no per-package form) | no tracked `*-schema.json` / `*.schema.json` outside `tests/` | - -### Run QA for a specific package - -Package aliases: `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix`, `cli`. - -```shell -make qa-core # all gates for packages/core -make qa-normalize-docs # packages/services/normalize_docs -make qa-normalize-tests # packages/services/normalize_tests -make qa-coverage-matrix # packages/services/coverage_matrix -make qa-cli # apps/cli -``` - -Run one gate on one package: - -```shell -make format-core # Black on packages/core -make lint-core # Pylint on packages/core -make types-core # mypy on packages/core -make deptry-core # deptry on packages/core -make test-core # tests on packages/core -make coverage-core # tests + coverage gate on packages/core -``` - -### Root checks - -`make qa-root` runs the gates over the root's own files. `make qa` runs it after the packages, -and each aggregate target (`make lint`, `make test`, …) except `deptry` includes its `-root` form. - -| Target | Runs | Gate | -|---|---|---| -| `format-check-root` / `format-root` | Black over `main.py`, `tools/` and `tests/` | line length **120** | -| `lint-root` | ruff over `main.py`, `tools/` and `tests/`, then Pylint over `main.py` and `tools/` | score ≥ **9.5** / 10 | -| `types-root` | mypy over `main.py` and `tools/` | clean | -| `test-root` / `coverage-root` | pytest over `tests/`, measuring `main.py` and `tools/` | ≥ **80 %** | - -### Deprecated target names - -The pre-Phase-0 names still work for one release and print a deprecation notice: -`py-qa` → `qa`, `black` → `format`, `pylint` → `lint`, `mypy` → `types`, -`pytest-unit` → `test`, and their `-` per-package forms. - -## Running Static Code Analysis - -This project uses [Pylint](https://pylint.readthedocs.io/) for static code analysis. -Pylint displays a global evaluation score rated out of 10.0. We aim to keep our code -quality at or above **9.5**. - -Each package's `pyproject.toml` configures Pylint. CI runs Pylint per-package and -excludes test files. - -### Run Pylint - -One package (`make lint-`) or all of them (`make lint`): - -```shell -make lint-core # wraps: cd packages/core && pylint --fail-under=9.5 $(git ls-files '*.py' | grep -v '^tests/') -make lint # every package -``` - -To run Pylint on a specific file, from inside the package directory: - -```shell -cd packages/core && pylint src/living_doc_core/input_validation.py -``` - -## Run Black Tool Locally - -This project uses [Black](https://github.com/psf/black) for code formatting. -Line length is set to **120 characters** (configured in each package's `pyproject.toml`). - -### Run Black - -Rewrite one package (`make format-`) or all of them (`make format`): - -```shell -make format-core # wraps: cd packages/core && black . -make format # every package -``` +| `apps/cli` | `living-doc-cli` | The `living-doc` command group; registers `normalize-docs`, `normalize-tests` and `coverage-matrix` ([README](apps/cli/README.md)) | -### Check-only mode (no changes) - -```shell -make format-check-core # wraps: cd packages/core && black --check . -make format-check # every package -``` - -### Expected Output -``` -All done! ✨ 🍰 ✨ -1 file reformatted. -``` - -## Run mypy Tool Locally - -This project uses [mypy](https://mypy.readthedocs.io/en/stable/) for static type -checking. Configuration is in each package's `pyproject.toml`. - -### Run mypy - -One package (`make types-`) or all of them (`make types`): - -```shell -make types-core # wraps: cd packages/core && mypy . -make types # every package -``` - -Configuration (`[tool.mypy]` in each `pyproject.toml`) sets `python_version = "3.10"` — the -supported floor — so type checks catch 3.11+-only stdlib use. - -## Dependency and Contract Checks - -```shell -make deptry-core # wraps: cd packages/core && python -m deptry . -make deptry # every package -make no-vendored-schemas # wraps: python -m living_doc_utilities.contracts.check_no_vendored_schemas -``` - -- `deptry` fails on an import its package does not declare, on a declared dependency nothing imports, and on an - import that only arrives through another package. Each package configures it under `[tool.deptry]`: - `optional_dependencies_dev_groups = ["dev"]`, because the `[dev]` extra holds only tools, and `known_first_party`, - its own `src/` package. - - It has no root form: the root declares no package, and `main.py` imports only the standard library and - `living_doc_core`, which `requirements.txt` installs. -- `no-vendored-schemas` is R12 check 1 of `living-doc-utilities`' [component checks](https://github.com/AbsaOSS/living-doc-utilities/blob/master/docs/contracts/component-checks.md): - no repository but `living-doc-utilities` commits a contract schema. It reads git's index, so a file not yet added - is not seen, and it skips anything under a `tests/` directory. - - Why: the contract models and schemas come from the installed `living-doc-utilities`; a committed copy stops - tracking it, and nothing reports that. -- R12 check 2, one read path and one write path, is the code's: every read goes through `read_artifact` and returns a - contract model ([core README](packages/core/README.md#the-reader-table)), and every write goes through - `write_artifact` from a contract model ([normalize-docs README](packages/services/normalize_docs/README.md#what-a-run-does), - [normalize-tests README](packages/services/normalize_tests/README.md#what-a-run-does), - [coverage-matrix README](packages/services/coverage_matrix/README.md#what-a-run-does)). -- R12 check 3 and R11 are each transform's own tests: the full-sample test, and `assert_complete` over its lineage - table ([normalize-docs README](packages/services/normalize_docs/README.md#lineage), - [normalize-tests README](packages/services/normalize_tests/README.md#lineage), - [coverage-matrix README](packages/services/coverage_matrix/README.md#lineage)). - -## Running Unit Tests - -Unit tests are written using [pytest](https://docs.pytest.org/) and live in each -package's `tests/` directory. - -### Run tests - -One package (`make test-`) or all of them (`make test`): - -```shell -make test-core # wraps: cd packages/core && pytest tests/ -make test # every package -``` - -`make coverage` / `make coverage-` adds the explicit `--cov=src --cov-fail-under=80` -gate that CI enforces. - -## Running Integration Tests - -`.github/workflows/integration.yml` runs the action itself, through `uses: ./`, on real -interpreters ([CI](#ci)). It has no local form: it needs a GitHub runner. - -- Locally, `normalize-docs`, `normalize-tests` and `coverage-matrix` run end to end over their fixtures and - committed goldens, and through the entry point in `tests/test_main.py`. -- Each command's change brings its own end-to-end tests and its step in `action-run`. - -## Code Coverage - -Code coverage is collected with `pytest-cov`. The minimum threshold is **80 %**. - -### Check coverage - -```shell -make coverage-core # wraps: cd packages/core && pytest tests/ --cov=src --cov-fail-under=80 -make coverage # every package -``` - -### Generate HTML report - -Each package's `pyproject.toml` sets `--cov-report=html`, so `make coverage-core` also -writes `packages/core/htmlcov/index.html`: - -```shell -make coverage-core -open packages/core/htmlcov/index.html -``` - -## Run CLI Locally - -After installing all packages (see [Get Started](#get-started)), the `living-doc` -command is available. It registers `normalize-docs`, `normalize-tests` and `coverage-matrix`: +- A package directory `packages//` is named by the group's singular and its name: the distribution `living-doc--`, with `-` for `_`, and the import `living_doc__` → `packages/services/normalize_docs/pyproject.toml::living-doc-service-normalize-docs` + - So `packages/services/normalize_docs` is `living-doc-service-normalize-docs`, imported as `living_doc_service_normalize_docs`. + - The packages ship only inside the action's release, never on PyPI. + - Why the prefix: the packages share the action's venv with `living_doc_utilities` and its dependencies; `living_doc_` keeps them apart. + - `` is this repository's convention. +- Each command's transform is a package of its own under `packages/services/`, with its lineage table in it. +- Each package has its own `pyproject.toml`, `src/` layout and `tests/` directory; its gates run from inside its directory, as CI runs them → `Makefile::dir-core` +- The toolkit's packages ship no `py.typed`, so a package importing another reads its annotations through a mypy `follow_untyped_imports` override → `pyproject.toml::follow_untyped_imports` + - The override sits in its `pyproject.toml`; the root has one for `living_doc_core`. +- The repository root holds the action: `action.yml`, its entry point `main.py`, the local run's `local-integration.sh` and `tools/`, and the root `tests/` for them. The root gates run from the root with the root `pyproject.toml` → `Makefile::ROOT_PY` + +## Gates + +`make qa` runs `format-check`, `lint`, `types`, `deptry` and `test` on every package, then the root's gates (`qa-root`). +It ends with `no-vendored-schemas` and stops at the first failure → `Makefile::qa`. Run it before opening a pull request. + + +| Target | Runs | Gate | One package | The root | Old name | +|---|---|---|---|---|---| +| `make qa` | the five package gates, then `qa-root`, then `no-vendored-schemas` | every gate below | `qa-` | `qa-root`: every root gate, no `deptry` | `py-qa` | +| `make format` | ruff autofix, then Black, rewriting files | line length 120 | `format-` | `format-root`: `main.py`, `tools/`, `tests/` | `black` | +| `make format-check` | Black in check mode, no writes | line length 120 | `format-check-` | `format-check-root`: `main.py`, `tools/`, `tests/` | — | +| `make lint` | ruff, then Pylint over the tracked `*.py` outside `tests/` | Pylint ≥ 9.5 / 10 | `lint-` | `lint-root`: ruff over `main.py`, `tools/`, `tests/`; Pylint over `main.py`, `tools/` | `pylint` | +| `make types` | mypy, `[tool.mypy]` `python_version = "3.10"` | clean | `types-` | `types-root`: `main.py`, `tools/` | `mypy` | +| `make deptry` | deptry | no undeclared, unused or transitive dependency | `deptry-` | none | — | +| `make test` | pytest over `tests/`; each `pyproject.toml` adds coverage with `pytest-cov` | pass, coverage ≥ 80 % | `test-` | `test-root`: `tests/`, measuring `main.py` and `tools/` | `pytest-unit` | +| `make coverage` | pytest with `--cov=src --cov-fail-under=80` on the command line; `--cov-report=html` writes `/htmlcov/index.html` | coverage ≥ 80 % | `coverage-` | `coverage-root` | — | +| `make no-vendored-schemas` | `living-doc-utilities`' vendored-schema check over the whole repository | no tracked `*-schema.json` or `*.schema.json` outside a `tests/` directory | none | none | — | + +- The aliases are `core`, `normalize-docs`, `normalize-tests`, `coverage-matrix` and `cli` → `Makefile::ALIASES` +- Each aggregate target but `deptry` and `no-vendored-schemas` runs its root form too → `Makefile::ROOT_TARGETS` +- An old name, and its `-` form, prints a deprecation notice and runs the new target; they stay for one release → `Makefile::DEPRECATED` +- `test.yml` runs the same targets, so local runs and CI never drift → `.github/workflows/test.yml::coverage` +- `make help` lists every target → `Makefile::help` +- Black, ruff, Pylint, mypy and pytest read their settings from each package's `pyproject.toml`; one file: `cd packages/core && pylint src/living_doc_core/input_validation.py` → `packages/core/pyproject.toml::tool.pylint` +- mypy checks against the 3.10 floor, so a standard-library use newer than 3.10 fails → `packages/core/pyproject.toml::python_version` +- `deptry` fails on an undeclared import, on a declared dependency nothing imports, and on an import only another package brings → `packages/core/pyproject.toml::tool.deptry` + - Each package sets `optional_dependencies_dev_groups = ["dev"]`, because the `[dev]` extra holds only tools, and `known_first_party`, its own `src/` package. + - It has no root form: the root declares no package, and `main.py` imports only the standard library and `living_doc_core`, which `requirements.txt` installs. +- `no-vendored-schemas` is R12 check 1 of `living-doc-utilities`' [component checks](https://github.com/AbsaOSS/living-doc-utilities/blob/master/docs/contracts/component-checks.md): no repository but `living-doc-utilities` commits a contract schema → `Makefile::check_no_vendored_schemas` + - It reads git's index, so it misses a file not yet added, and skips anything under a `tests/` directory. + - Why: the contract models and schemas come from the installed `living-doc-utilities`; a committed copy stops tracking it, and nothing reports that. +- R12 check 2, one read path and one write path, is the code's. Every read goes through `read_artifact` and returns a contract model → [The reader table](packages/core/README.md#the-reader-table) + - Every write goes through `write_artifact` from a contract model: each command's § What a run does, [normalize-docs](packages/services/normalize_docs/README.md#what-a-run-does), [normalize-tests](packages/services/normalize_tests/README.md#what-a-run-does), [coverage-matrix](packages/services/coverage_matrix/README.md#what-a-run-does). +- R12 check 3 and R11 are each transform's own tests: the full-sample test, and `assert_complete` over its lineage table → each command's § Lineage, [normalize-docs](packages/services/normalize_docs/README.md#lineage), [normalize-tests](packages/services/normalize_tests/README.md#lineage), [coverage-matrix](packages/services/coverage_matrix/README.md#lineage) +- The page checks in `tests/docs/` are root tests, so `make test` and `make qa` run them → [Checks](#checks) + +## Test data + +- `tests/fixtures/collector-gh/` holds the one copy of `living-doc`'s example `doc-source.json` and `ui-tests.json`, with their source noted beside them; every package's tests and `integration.yml` read it → [README](tests/fixtures/collector-gh/README.md) +- `tests/fixtures/utilities-golden/` holds `living-doc-utilities`' six golden entities, which `packages/services/normalize_docs`' tests read → [README](tests/fixtures/utilities-golden/README.md) +- Each command's golden output is in its package's `tests/expected/`, with how to regenerate it → [normalize-docs](packages/services/normalize_docs/tests/expected/README.md), [normalize-tests](packages/services/normalize_tests/tests/expected/README.md), [coverage-matrix](packages/services/coverage_matrix/tests/expected/README.md) +- Each command runs end to end over the fixtures against its golden, and through the entry point → `tests/test_main.py::test_the_entry_point_runs_coverage_matrix_over_the_fixture_pair_and_sets_the_output_path` +- Each command's change brings its own end-to-end tests and its step in `integration.yml`'s `action-run` → [CI](#ci) + +## CLI run + +After `make install`, `living-doc` runs the commands over the fixtures; every option is in the [CLI reference](apps/cli/README.md). ```shell living-doc --version @@ -366,16 +136,32 @@ living-doc coverage-matrix --doc-input tests/fixtures/collector-gh/doc-source.js --tests-input tests/fixtures/collector-gh/ui-tests.json --output output/coverage-matrix.json --fail-under 50 ``` +## Debug run + +The run of `main.py` as the action runs it, in the development venv, from the repository root after `make install`. +What it checks and the lines it fails with: [Errors](docs/action.md#errors). + +```shell +export INPUT_COMMAND=normalize-docs +export INPUT_INPUT=tests/fixtures/collector-gh/doc-source.json +export INPUT_OUTPUT=output/generator-ready.json +export INPUT_VERBOSE_LOGGING=true # optional: --verbose and DEBUG lines; default false; RUNNER_DEBUG=1 too +python3 main.py # writes output/generator-ready.json +``` + +- Each input is an `INPUT_` variable, upper case with `_` for `-`, as `action.yml` passes it → `action.yml::INPUT_DOCUMENT_TITLE` +- Without `GITHUB_OUTPUT`, as outside GitHub Actions, the run sets no `output-path` output and logs that once → `tests/test_main.py::test_a_run_outside_github_actions_sets_no_output_and_says_so` +- `tests/test_main.py` replaces the CLI to test the run itself: the command line, the exit code passed through, and `output-path` → `tests/test_main.py::test_a_failed_command_ends_the_run_with_its_exit_code_and_sets_no_output` + - It also runs `normalize-docs`, `normalize-tests` and `coverage-matrix` through the entry point over their fixtures, for real. + ## Local Integration Run `local-integration.sh` runs the real toolkit over a given `doc-source.json` and `ui-tests.json`, on the machine that -has them, outside any workflow. It runs the three commands through `main.py` in a venv of its own, re-reads every +has them, outside any workflow. It runs the three commands through `main.py` in a venv of its own. It re-reads every output with `read_artifact`, and prints what ran and what came out. No workflow runs it; like any local run, it is a development and operator-check affordance, not a second supported deployment target. -- The usual inputs are the two files a `living-doc-collector-gh` run wrote: its - [Local Integration Run](https://github.com/AbsaOSS/living-doc-collector-gh/blob/master/DEVELOPER.md#local-integration-run) - says where they land. The script never derives another component's paths; you configure both files. +- The usual inputs are the two files a `living-doc-collector-gh` run wrote: its [Local Integration Run](https://github.com/AbsaOSS/living-doc-collector-gh/blob/master/DEVELOPER.md#local-integration-run) says where they land. The script never derives another component's paths; you configure both files → `.env.local.example::DOC_INPUT` ### What You Prepare @@ -420,19 +206,25 @@ As committed, the example runs over the fixture pair in `tests/fixtures/collecto A run, in order: -1. **Checks** what `--check` checks. A missing setting or input file, an input inside a directory the run clears, a `VIEW` other than `inner` or `release`, or a Python below 3.10 stops it with one line that names it. +1. **Checks** what `--check` checks. One line names what stops it: + - a missing setting or input file; + - an input inside a directory the run clears; + - a `VIEW` other than `inner` or `release`; + - a Python below 3.10. 2. **Builds its venv**, `.venv-local-integration/` (git-ignored), separate from the development `.venv`. It installs only `requirements.txt` (`pip install -r`), from the repository root: a runtime requirement missing from that file fails here, as on a runner. - It follows [Action venv](#action-venv): the same six variables unset, the same import check after the install, and `main.py` run with `-E`. - The venv is reused only when the same interpreter built it from the same `requirements.txt`. 3. **Prints what runs**: this repository's commit, the installed `living-doc-utilities` version, the Python version, both inputs, the view and the output directory. The commit is marked when tracked files differ from it. -4. **Runs `normalize-docs`, `normalize-tests`, then `coverage-matrix`**, each as its own `main.py` run with only the inputs it takes ([The entry point](#the-entry-point)), into `$OUT_DIR/toolkit//`, having cleared the three directories first. Each command is echoed before it runs. +4. **Runs `normalize-docs`, `normalize-tests`, then `coverage-matrix`**, each as its own `main.py` run with only the inputs it takes ([Inputs](docs/action.md#inputs)). Each command is echoed before it runs. + - The outputs go to `$OUT_DIR/toolkit//`; the run clears those three directories first. - A command that fails stops the run with one line naming it, after the toolkit's own error lines. A `DOC_INPUT` that is not a `doc-source` artifact stops `normalize-docs` with `[CONTRACT_MISMATCH]`. 5. **Validates and reports** each output → `tools/local_integration.py::report` - - It re-reads the output with `read_artifact` against its contract, then prints its producer, run time, size and `metadata.stats.cardinality`, and for `coverage-matrix` the root `summary`. + - It re-reads the output with `read_artifact` against its contract, then prints its producer, run time, size and `metadata.stats.cardinality`. + - For `coverage-matrix` it also prints the root `summary`. - It prints each `metadata.source_inputs[]` entry: the input's contract, the producer that wrote it, `source.project_id` and `source.repositories`. - It prints the warnings by code: each code's count and its first ten occurrences. - - Per input, it matches each of the input's warnings to one the output carries unchanged, every field the same but the context, which names the input first. It prints how many matched beside the input's own count; one that does not match fails the run ([`errors.md` § Forwarded warnings](https://github.com/AbsaOSS/living-doc-utilities/blob/master/docs/contracts/errors.md#forwarded-warnings)). - - In the report, text from the files is printed with control characters spelled out (`\n`, `\x1b`), so an input cannot forge a report line. The toolkit's own log lines above it are printed as the toolkit writes them. + - Per input, it matches each warning to one the output carries unchanged, every field the same but the context. The context names the input first. It prints how many matched beside the input's own count; one that does not match fails the run ([`errors.md` § Forwarded warnings](https://github.com/AbsaOSS/living-doc-utilities/blob/master/docs/contracts/errors.md#forwarded-warnings)). + - The report spells out control characters in text from the files (`\n`, `\x1b`), so an input cannot forge a line. The toolkit's own log lines above it are printed as the toolkit writes them. - It exits non-zero when a check, the install, a command or a validation fails → `tools/local_integration.py::main` - Outside GitHub Actions every `metadata.run` field is empty; the inputs' producers in `metadata.source_inputs[]` say where the run's data came from. @@ -440,185 +232,154 @@ A run, in order: ## Action venv -`action.yml` runs the toolkit in a venv of its own, built from the caller's Python. The rules -are `living-doc-generator-markdown`'s; what differs here follows them. - -- The action uses the Python on `PATH`; the caller prepares it, for example with `actions/setup-python`. -- Below 3.10 it fails before creating anything, with one line: - `::error::living-doc-toolkit needs Python >= 3.10 on PATH. Add actions/setup-python before this step.` -- It installs `requirements.txt` into `$RUNNER_TEMP/living-doc-toolkit-venv`. It never changes the - caller's interpreter, `PATH` or environment, and writes nothing to `$GITHUB_ENV` or `$GITHUB_PATH`. -- `PIP_TARGET`, `PIP_PREFIX`, `PIP_USER`, `PIP_ROOT`, `PIP_PYTHON` and `PYTHONPATH` are unset before - the install. - - Why: pip reads them as configuration, so a caller exporting one would empty or shadow the venv, - or (`PIP_PYTHON`) install into the caller's own interpreter. -- The venv is reused only when the interpreter now on `PATH` built it, compared as `sys.base_prefix` - plus version; otherwise it is rebuilt. -- After the install, one import of `living_doc_cli.main` through the venv's interpreter proves the - packages landed there. Its failure is one line pointing at the runner's pip configuration. -- `main.py` runs with `-E`, so a caller's `PYTHONPATH` cannot shadow the venv. Every input reaches it - through the run step's `env:`; no `run:` script holds `${{ inputs.* }}`. -- `main.py` runs the CLI with `-I`, which is `-E` that also keeps the working directory off `sys.path`. - - Why: the run step runs in the caller's checkout, and `python -m` searches the working directory - first, so a `click.py` or `json.py` there would replace the venv's module. -- Linux runners only: the venv's POSIX `bin/` layout is assumed. -- `tests/test_action_yml.py` runs the venv step against a fake interpreter. `integration.yml` shows - the reuse, the rebuild on an interpreter change, and the rejection of Python 3.9 on real ones - ([CI](#ci)). +`action.yml` runs the toolkit in a venv of its own, built from the caller's Python. The rules are +`living-doc-generator-markdown`'s; what differs here follows them. What the caller provides, and the lines the step +fails with: [Prerequisites](docs/action.md#prerequisites), [Errors](docs/action.md#errors). -What differs from `living-doc-generator-markdown`: - -- The install and the import check run from the action's own directory, `$GITHUB_ACTION_PATH`. - - Why: `requirements.txt` names `./packages/...`, which pip reads against the directory it runs - in; from the caller's workspace the paths do not exist. - - Why the variable, not `${{ github.action_path }}`: in a `container:` job the expression gives - the runner host's path, which does not exist inside the container. -- The import check imports `living_doc_cli.main`, which imports `click`, the CLI package and each - command it registers, with the packages those commands import: the three services, - `living-doc-core` and `living-doc-utilities`. -- pip rebuilds a local directory on every install, so each run reinstalls the five packages into the - reused venv; their dependencies stay installed. - -### The entry point - -`main.py` checks the inputs, then runs `python -I -m living_doc_cli.main --