From 22cf2d830bca307d8da8bb1945a241f4ac0c1614 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 21 Jun 2026 14:21:57 +0200 Subject: [PATCH 1/5] Refactor documentation structure: consolidate internals page and adjust heading levels --- docs/api_reference.jl | 273 +++++++---------------- ext/DocumenterReference/page_building.jl | 8 +- 2 files changed, 84 insertions(+), 197 deletions(-) diff --git a/docs/api_reference.jl b/docs/api_reference.jl index c2dfa0ad..46ec02fe 100644 --- a/docs/api_reference.jl +++ b/docs/api_reference.jl @@ -6,214 +6,101 @@ Generate the API reference documentation for CTBase. Returns the list of pages. """ function generate_api_reference(src_dir::String) - # Helper to build absolute paths src(files...) = [abspath(joinpath(src_dir, f)) for f in files] ext_dir = abspath(joinpath(src_dir, "..", "ext")) ext(files...) = [abspath(joinpath(ext_dir, f)) for f in files] - # Symbols to exclude (must match make.jl if shared, or be defined here) EXCLUDE_SYMBOLS = Symbol[:include, :eval] + EXCLUDE_INTERNALS = vcat(EXCLUDE_SYMBOLS, Symbol[ + :DOCTYPE_ABSTRACT_TYPE, :DOCTYPE_CONSTANT, :DOCTYPE_FUNCTION, + :DOCTYPE_MACRO, :DOCTYPE_MODULE, :DOCTYPE_STRUCT, + ]) + + # ── Public API: one flat page per module ────────────────────────────────── + modules_config = [ + (mod=CTBase.Core, title="Core", filename="core", files=src( + joinpath("Core", "Core.jl"), joinpath("Core", "default.jl"), + joinpath("Core", "types.jl"), joinpath("Core", "matrix_utils.jl"), + joinpath("Core", "function_utils.jl"), joinpath("Core", "macros.jl"), + )), + (mod=CTBase.Interpolation, title="Interpolation", filename="interpolation", files=src( + joinpath("Interpolation", "Interpolation.jl"), joinpath("Interpolation", "types.jl"), + joinpath("Interpolation", "ctinterpolate.jl"), joinpath("Interpolation", "display.jl"), + )), + (mod=CTBase.Descriptions, title="Descriptions", filename="descriptions", files=src( + joinpath("Descriptions", "Descriptions.jl"), joinpath("Descriptions", "types.jl"), + joinpath("Descriptions", "similarity.jl"), joinpath("Descriptions", "display.jl"), + joinpath("Descriptions", "catalog.jl"), joinpath("Descriptions", "complete.jl"), + joinpath("Descriptions", "remove.jl"), + )), + (mod=CTBase.Exceptions, title="Exceptions", filename="exceptions", files=src( + joinpath("Exceptions", "Exceptions.jl"), joinpath("Exceptions", "types.jl"), + joinpath("Exceptions", "display.jl"), + )), + (mod=CTBase.Unicode, title="Unicode", filename="unicode", files=src( + joinpath("Unicode", "Unicode.jl"), joinpath("Unicode", "subscripts.jl"), + joinpath("Unicode", "superscripts.jl"), + )), + (mod=CTBase.DevTools, title="DevTools", filename="devtools", files=src( + joinpath("DevTools", "DevTools.jl"), joinpath("DevTools", "coverage_postprocessing.jl"), + joinpath("DevTools", "documenter_reference.jl"), joinpath("DevTools", "test_runner.jl"), + )), + ] pages = [ CTBase.automatic_reference_documentation(; subdirectory="api", - primary_modules=[ - CTBase.Core => src( - joinpath("Core", "Core.jl"), - joinpath("Core", "default.jl"), - joinpath("Core", "types.jl"), - joinpath("Core", "matrix_utils.jl"), - joinpath("Core", "function_utils.jl"), - joinpath("Core", "macros.jl"), - ), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Core", - title_in_menu="Core", - filename="core", - ), - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CTBase.Interpolation => src( - joinpath("Interpolation", "Interpolation.jl"), - joinpath("Interpolation", "types.jl"), - joinpath("Interpolation", "ctinterpolate.jl"), - joinpath("Interpolation", "display.jl"), - ), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Interpolation", - title_in_menu="Interpolation", - filename="interpolation", - ), - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CTBase.Descriptions => src( - joinpath("Descriptions", "Descriptions.jl"), - joinpath("Descriptions", "types.jl"), - joinpath("Descriptions", "similarity.jl"), - joinpath("Descriptions", "display.jl"), - joinpath("Descriptions", "catalog.jl"), - joinpath("Descriptions", "complete.jl"), - joinpath("Descriptions", "remove.jl"), - ), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Descriptions", - title_in_menu="Descriptions", - filename="descriptions", - ), - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CTBase.Exceptions => src( - joinpath("Exceptions", "Exceptions.jl"), - joinpath("Exceptions", "types.jl"), - joinpath("Exceptions", "display.jl"), - ), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="Exceptions", - title_in_menu="Exceptions", - filename="exceptions", - ), - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CTBase.Unicode => src( - joinpath("Unicode", "Unicode.jl"), - joinpath("Unicode", "subscripts.jl"), - joinpath("Unicode", "superscripts.jl"), - ), - ], - exclude=EXCLUDE_SYMBOLS, - public=true, - private=false, # there is no private API - title="Unicode", - title_in_menu="Unicode", - filename="unicode", - ), - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CTBase.DevTools => src( - joinpath("DevTools", "DevTools.jl"), - joinpath("DevTools", "coverage_postprocessing.jl"), - joinpath("DevTools", "documenter_reference.jl"), - joinpath("DevTools", "test_runner.jl"), - ), - ], + primary_modules=[cfg.mod => cfg.files], exclude=EXCLUDE_SYMBOLS, - public=true, - private=true, - title="DevTools", - title_in_menu="DevTools", - filename="devtools", - ), + public=true, private=false, + title=cfg.title, title_in_menu=cfg.title, filename=cfg.filename, + ) + for cfg in modules_config ] - DocumenterReference = Base.get_extension(CTBase, :DocumenterReference) - if !isnothing(DocumenterReference) - EXCLUDE_DOCREF = vcat( - EXCLUDE_SYMBOLS, - Symbol[ - :DOCTYPE_ABSTRACT_TYPE, - :DOCTYPE_CONSTANT, - :DOCTYPE_FUNCTION, - :DOCTYPE_MACRO, - :DOCTYPE_MODULE, - :DOCTYPE_STRUCT, - ], - ) - push!( - pages, - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - DocumenterReference => ext( - joinpath("DocumenterReference", "DocumenterReference.jl"), - joinpath("DocumenterReference", "config_helpers.jl"), - joinpath("DocumenterReference", "entry_point.jl"), - joinpath("DocumenterReference", "page_building.jl"), - joinpath("DocumenterReference", "source_file_detection.jl"), - joinpath("DocumenterReference", "symbol_classification.jl"), - joinpath("DocumenterReference", "symbol_iteration.jl"), - joinpath("DocumenterReference", "type_formatting.jl"), - joinpath("DocumenterReference", "types.jl"), - ), - ], - external_modules_to_document=[CTBase], - exclude=EXCLUDE_DOCREF, - public=false, # there is no public API - private=true, - title="DocumenterReference", - title_in_menu="DocumenterReference", - filename="documenter_reference", - ), - ) - end + # ── Internals: all private symbols in one page, sections by module ──────── + internals_modules = Any[cfg.mod => cfg.files for cfg in modules_config] - CoveragePostprocessing = Base.get_extension(CTBase, :CoveragePostprocessing) - if !isnothing(CoveragePostprocessing) - push!( - pages, - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - CoveragePostprocessing => ext( - joinpath("CoveragePostprocessing", "CoveragePostprocessing.jl"), - joinpath("CoveragePostprocessing", "entry_point.jl"), - joinpath("CoveragePostprocessing", "helpers.jl"), - ), - ], - external_modules_to_document=[CTBase], - exclude=EXCLUDE_SYMBOLS, - public=false, # there is no public API - private=true, - title="CoveragePostprocessing", - title_in_menu="CoveragePostprocessing", - filename="coverage_postprocessing", - ), - ) + for (sym, files) in [ + (:DocumenterReference, ext( + joinpath("DocumenterReference", "DocumenterReference.jl"), + joinpath("DocumenterReference", "config_helpers.jl"), + joinpath("DocumenterReference", "entry_point.jl"), + joinpath("DocumenterReference", "page_building.jl"), + joinpath("DocumenterReference", "source_file_detection.jl"), + joinpath("DocumenterReference", "symbol_classification.jl"), + joinpath("DocumenterReference", "symbol_iteration.jl"), + joinpath("DocumenterReference", "type_formatting.jl"), + joinpath("DocumenterReference", "types.jl"), + )), + (:CoveragePostprocessing, ext( + joinpath("CoveragePostprocessing", "CoveragePostprocessing.jl"), + joinpath("CoveragePostprocessing", "entry_point.jl"), + joinpath("CoveragePostprocessing", "helpers.jl"), + )), + (:TestRunner, ext( + joinpath("TestRunner", "TestRunner.jl"), + joinpath("TestRunner", "arg_parsing.jl"), + joinpath("TestRunner", "entry_point.jl"), + joinpath("TestRunner", "progress.jl"), + joinpath("TestRunner", "test_execution.jl"), + joinpath("TestRunner", "test_selection.jl"), + joinpath("TestRunner", "types.jl"), + )), + ] + extmod = Base.get_extension(CTBase, sym) + isnothing(extmod) || push!(internals_modules, extmod => files) end - TestRunner = Base.get_extension(CTBase, :TestRunner) - if !isnothing(TestRunner) - push!( - pages, - CTBase.automatic_reference_documentation(; - subdirectory="api", - primary_modules=[ - TestRunner => ext( - joinpath("TestRunner", "TestRunner.jl"), - joinpath("TestRunner", "arg_parsing.jl"), - joinpath("TestRunner", "entry_point.jl"), - joinpath("TestRunner", "progress.jl"), - joinpath("TestRunner", "test_execution.jl"), - joinpath("TestRunner", "test_selection.jl"), - joinpath("TestRunner", "types.jl"), - ), - ], - external_modules_to_document=[CTBase], - exclude=EXCLUDE_SYMBOLS, - public=false, # there is no public API - private=true, - title="TestRunner", - title_in_menu="TestRunner", - filename="test_runner", - ), - ) - end + push!(pages, CTBase.automatic_reference_documentation(; + subdirectory="api", + primary_modules=internals_modules, + external_modules_to_document=[CTBase], + exclude=EXCLUDE_INTERNALS, + public=false, private=true, + title="Internals", title_in_menu="Internals", filename="internals", + )) + return pages end diff --git a/ext/DocumenterReference/page_building.jl b/ext/DocumenterReference/page_building.jl index 793c1272..ba9e6c23 100644 --- a/ext/DocumenterReference/page_building.jl +++ b/ext/DocumenterReference/page_building.jl @@ -85,12 +85,12 @@ function _collect_module_docstrings(config::_Config, symbol_list; include_module effective_source_files, config.include_without_source, ) - push!(docstrings, "## `$(current_module)`\n\n```@docs\n$(current_module)\n```\n\n") + push!(docstrings, "### `$(current_module)`\n\n```@docs\n$(current_module)\n```\n\n") end _iterate_over_symbols(config, symbol_list) do key, type type == DOCTYPE_MODULE && return nothing - push!(docstrings, "## `$key`\n\n```@docs\n$(current_module).$key\n```\n\n") + push!(docstrings, "### `$key`\n\n```@docs\n$(current_module).$key\n```\n\n") return nothing end @@ -361,7 +361,7 @@ function _build_private_page_content( all_docstrings = String[] for (mod, _, private_docs) in module_contents if !isempty(private_docs) - push!(all_docstrings, "\n---\n\n### From `$(mod)`\n\n") + push!(all_docstrings, "\n---\n\n## From `$(mod)`\n\n") append!(all_docstrings, private_docs) end end @@ -412,7 +412,7 @@ function _build_public_page_content( all_docstrings = String[] for (mod, public_docs, _) in module_contents if !isempty(public_docs) - push!(all_docstrings, "\n---\n\n### From `$(mod)`\n\n") + push!(all_docstrings, "\n---\n\n## From `$(mod)`\n\n") append!(all_docstrings, public_docs) end end From 1dfb8941fe4c076cf684935439f7506eac13c8bd Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 21 Jun 2026 14:28:31 +0200 Subject: [PATCH 2/5] Update documentation philosophy to reflect unified Internals page structure --- dev/philosophy/documentation.md | 81 +++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/dev/philosophy/documentation.md b/dev/philosophy/documentation.md index aa010998..eb7dda35 100644 --- a/dev/philosophy/documentation.md +++ b/dev/philosophy/documentation.md @@ -7,9 +7,12 @@ How docstrings become a published site via Documenter.jl and ## Principles 1. **Auto-generated API reference** — never hand-write API pages; generate one per - submodule (and one per loaded extension). -2. **Public + private documented** — both `public=true` and `private=true`: users reach - internals via qualified paths, so internals are part of the documented surface. + submodule (and one unified internals page for all private symbols). +2. **Public + private documented, separately** — one flat page per submodule for + exported symbols (`public=true, private=false`); one unified "Internals" page for all + private symbols across all submodules and extensions (`public=false, private=true`). + Users reach internals via qualified paths, so internals are part of the documented + surface — but kept out of the main navigation. 3. **Guides separate from API** — narrative guides are hand-written under `docs/src//`; the API reference is generated and cleaned up after the build. 4. **Index is the entry point** — `docs/src/index.md`: scope, module table, guide links, @@ -30,24 +33,72 @@ docs/ └── api/ # auto-generated (removed after build) ``` -## API generation (per submodule) +## API generation pattern + +The standard pattern uses a shared `modules_config` list to avoid repeating file lists, +then generates public pages in a loop and one unified Internals page. ```julia -CTBase.automatic_reference_documentation(; +# ── Shared config: one entry per submodule ──────────────────────────────────── +modules_config = [ + (mod=MyPackage.SubA, title="SubA", filename="suba", files=src( + "SubA/SubA.jl", "SubA/types.jl", "SubA/helpers.jl", + )), + (mod=MyPackage.SubB, title="SubB", filename="subb", files=src( + "SubB/SubB.jl", "SubB/core.jl", + )), +] + +EXCLUDE = Symbol[:include, :eval] + +# ── Public pages: one flat page per submodule ───────────────────────────────── +pages = [ + MyPackage.automatic_reference_documentation(; + subdirectory = "api", + primary_modules = [cfg.mod => cfg.files], + exclude = EXCLUDE, + public = true, + private = false, + title = cfg.title, + filename = cfg.filename, + ) + for cfg in modules_config +] + +# ── Internals: all private symbols in one page, sections by module ──────────── +internals_modules = Any[cfg.mod => cfg.files for cfg in modules_config] + +# Extensions are detected and added conditionally +for (sym, files) in [ + (:MyExt, ext("MyExt/MyExt.jl", "MyExt/helpers.jl")), +] + extmod = Base.get_extension(MyPackage, sym) + isnothing(extmod) || push!(internals_modules, extmod => files) +end + +push!(pages, MyPackage.automatic_reference_documentation(; subdirectory = "api", - primary_modules = [MyPackage.SubA => src("SubA/SubA.jl", "SubA/x.jl")], - external_modules_to_document = [MyPackage], # include re-exported symbols - exclude = Symbol[:include, :eval], - public = true, + primary_modules = internals_modules, + external_modules_to_document = [MyPackage], + exclude = EXCLUDE, + public = false, private = true, - title = "SubA", - filename = "api_suba", -) + title = "Internals", + filename = "internals", +)) +``` + +The resulting navigation is: + +```text +API Reference + SubA ← exported symbols only + SubB ← exported symbols only + Internals ← all private symbols, sections by module (including extensions) ``` Keep the per-submodule file lists in sync with `src/` — a missing file silently drops -docstrings and breaks `@ref` links. Extensions are detected with `Base.get_extension` -and documented conditionally. +docstrings and breaks `@ref` links. ## Cross-reference infrastructure @@ -85,7 +136,7 @@ module table, guide links via `[@ref]`, and a short Quick Start with qualified c ## Checklist - [ ] `make.jl` uses `with_api_reference()`; `api_reference.jl` has generate/with/cleanup. -- [ ] One `automatic_reference_documentation` per submodule, `public=true` + `private=true`. +- [ ] One `automatic_reference_documentation` per submodule (`public=true, private=false`) + one unified Internals page (`public=false, private=true`). - [ ] Extensions detected via `Base.get_extension` and documented when present. - [ ] `InterLinks` set up and passed via `plugins=[links]` if `@extref` is used. - [ ] Guides under `docs/src//`; no hand-written API pages. From d1e289e3cf25a85e360dbc4c1d17ca2038f94345 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 21 Jun 2026 14:54:49 +0200 Subject: [PATCH 3/5] refactor(dev): merge test/docs READMEs into dev/, add coverage + MCP fallback + backend docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove test/README.md and docs/README.md (content absorbed into dev/) - dev/RULES.md: add MCP fallback (§1), new Coverage section (§2), build command + local preview subsections (§3), update checklist - dev/philosophy/documentation.md: add Backend section (Documenter.jl vs DocumenterVitepress), update layout tree with .vitepress/ - dev/PHILOSOPHY.md, dev/PLAN.md: renamed/moved from philosophy/ root Co-Authored-By: Cascade --- AGENTS.md | 4 +- CHANGELOGS.md | 2 +- CLAUDE.md | 4 +- dev/{philosophy => }/PHILOSOPHY.md | 36 +++--- dev/{planning.md => PLAN.md} | 0 dev/RULES.md | 89 +++++++++++++- dev/philosophy/documentation.md | 25 ++++ docs/README.md | 58 --------- test/README.md | 187 ----------------------------- 9 files changed, 132 insertions(+), 273 deletions(-) rename dev/{philosophy => }/PHILOSOPHY.md (62%) rename dev/{planning.md => PLAN.md} (100%) delete mode 100644 docs/README.md delete mode 100644 test/README.md diff --git a/AGENTS.md b/AGENTS.md index eecb789b..7a2efe19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,7 @@ dev/ # Code philosophy, operational rules, plan template (ver |---|---| | [`dev/philosophy/PHILOSOPHY.md`](dev/philosophy/PHILOSOPHY.md) | Code philosophy — modules, types/traits, exceptions, docstrings, testing, docs | | [`dev/RULES.md`](dev/RULES.md) | Operational rules — running tests (MCP), building docs, git, output capture | -| [`dev/planning.md`](dev/planning.md) | Plan template — phases, steps, human checkpoints | +| [`dev/PLAN.md`](dev/PLAN.md) | Plan template — phases, steps, human checkpoints | --- @@ -70,6 +70,6 @@ Workflows live in `.devin/workflows/`. - **No top-level exports** — use `CTBase.Submodule.symbol` everywhere. - **Qualified imports** — `using PackageName: PackageName`, never bare `using`. - **Fake types at module top-level** — never inside test functions. -- **Plans before code** — write a plan and confirm with the user before touching files. Template: [`dev/planning.md`](dev/planning.md). +- **Plans before code** — write a plan and confirm with the user before touching files. Template: [`dev/PLAN.md`](dev/PLAN.md). - **Docstrings last** — written only after all implementation steps are stable. - **Never commit or push without explicit user approval.** diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 90af92e9..49e62511 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -85,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `testing.md`: Categories, fakes/stubs, module + callable function template - `documentation.md`: API generation, guides, draft workflow - **Added agent guides**: Added `AGENTS.md` (agent navigation guide) and `CLAUDE.md` (Claude project context) -- **Added planning template**: Added `dev/planning.md` for implementation plans +- **Added planning template**: Added `dev/PLAN.md` for implementation plans - **Added operational rules**: Added `dev/RULES.md` for MCP, doc build, git, and output capture procedures #### **Documentation Build Improvements** diff --git a/CLAUDE.md b/CLAUDE.md index 9dc09e88..6ee1f984 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ 4. **Qualify everything** — `Module.symbol` at every call site; `import Pkg: Pkg` not `using Pkg`; no top-level package exports. 5. **Write a plan before coding** — any task touching more than one file or a public - interface needs a plan confirmed by the user first. Template in `dev/planning.md`. + interface needs a plan confirmed by the user first. Template in `dev/PLAN.md`. 6. **Docstrings last** — written only after the API is stable. 7. **Fake types at module top-level** — never inside test functions (world-age issues). @@ -20,7 +20,7 @@ | --- | --- | | Code philosophy (modules, types/traits, exceptions, docstrings, testing, docs) | [`dev/philosophy/`](dev/philosophy/PHILOSOPHY.md) | | Operational rules (MCP, doc build, git, output capture) | [`dev/RULES.md`](dev/RULES.md) | -| Plan template | [`dev/planning.md`](dev/planning.md) | +| Plan template | [`dev/PLAN.md`](dev/PLAN.md) | ## Project structure (quick reference) diff --git a/dev/philosophy/PHILOSOPHY.md b/dev/PHILOSOPHY.md similarity index 62% rename from dev/philosophy/PHILOSOPHY.md rename to dev/PHILOSOPHY.md index 94787ada..4a8ace07 100644 --- a/dev/philosophy/PHILOSOPHY.md +++ b/dev/PHILOSOPHY.md @@ -3,35 +3,37 @@ Design philosophy for the control-toolbox ecosystem, written **abstractly**: the principles apply to all packages, but the examples and templates stay **generic** (no package-specific symbols). For *tools and procedures* (tests, docs, git), see -[`../RULES.md`](../RULES.md). +[`RULES.md`](RULES.md). + +The detail files referenced below live in the [`philosophy/`](philosophy/) directory. ## The tenets on one page 1. **One module per responsibility.** Each submodule has a single role, its own directory, its own manifest. The package manifest exports **nothing**. - → [`modules.md`](modules.md) + → [`modules.md`](philosophy/modules.md) 2. **Everything is qualified.** Import modules, not their symbols; call `Module.symbol` everywhere. Explicit origin, safe refactors, no shadowing. - → [`modules.md`](modules.md) + → [`modules.md`](philosophy/modules.md) 3. **One abstract type per *noun*, one trait-parameter per *adjective*.** Conceptual variants ("is it an X or a Y") are types; orthogonal axes (autonomous?, in-place?, …) are traits in a type parameter. Dispatch by extracting the trait. - → [`types-traits-interfaces.md`](types-traits-interfaces.md) + → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) 4. **Program against abstractions.** Methods live on abstract types as much as possible; contracts are `NotImplemented` stubs; subtypes honor the contract (LSP). - → [`types-traits-interfaces.md`](types-traits-interfaces.md) + → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) 5. **SOLID, DRY, KISS, YAGNI.** Single responsibility, open/closed via dispatch, no duplication, the simplest thing that works, nothing speculative. - → [`types-traits-interfaces.md`](types-traits-interfaces.md) + → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) 6. **Structured errors.** Seven typed exceptions; sharp rule: single-argument value → `IncorrectArgument`; relation/state/composition → `PreconditionError`; unimplemented contract → `NotImplemented`; optional dependency → `ExtensionError`. - → [`exceptions.md`](exceptions.md) + → [`exceptions.md`](philosophy/exceptions.md) 7. **Type stability by default.** Parametric types, no `Any` in hot paths, function barriers, verified with `@inferred`. @@ -39,24 +41,24 @@ principles apply to all packages, but the examples and templates stay **generic* 8. **Everything is documented.** A docstring on every symbol, fixed templates, cross-references `@ref`/`@extref`, safe and reproducible examples. - → [`docstrings.md`](docstrings.md) + → [`docstrings.md`](philosophy/docstrings.md) 9. **Tests: module + callable function + everything qualified.** Each test file is a module with an entry function redefined in the outer scope; fakes at module top-level; categories unit / integration / contract / error. - → [`testing.md`](testing.md) + → [`testing.md`](philosophy/testing.md) 10. **Auto-generated API docs, public *and* private; guides separate.** Users reach internals via qualified paths, so both are documented. - → [`documentation.md`](documentation.md) + → [`documentation.md`](philosophy/documentation.md) ## Index | File | Content | -|---|---| -| [`modules.md`](modules.md) | Submodule organization, imports/qualification, DAG, exports | -| [`types-traits-interfaces.md`](types-traits-interfaces.md) | Types vs traits, interfaces/contracts, SOLID/DRY/YAGNI, type stability | -| [`exceptions.md`](exceptions.md) | The 7 exceptions and the choice rule | -| [`docstrings.md`](docstrings.md) | Docstring templates, cross-references, example safety | -| [`testing.md`](testing.md) | Categories, fakes/stubs, **module + callable function template** | -| [`documentation.md`](documentation.md) | API generation, guides, draft workflow | +| --- | --- | +| [`modules.md`](philosophy/modules.md) | Submodule organization, imports/qualification, DAG, exports | +| [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) | Types vs traits, interfaces/contracts, SOLID/DRY/YAGNI, type stability | +| [`exceptions.md`](philosophy/exceptions.md) | The 7 exceptions and the choice rule | +| [`docstrings.md`](philosophy/docstrings.md) | Docstring templates, cross-references, example safety | +| [`testing.md`](philosophy/testing.md) | Categories, fakes/stubs, **module + callable function template** | +| [`documentation.md`](philosophy/documentation.md) | API generation, guides, draft workflow | diff --git a/dev/planning.md b/dev/PLAN.md similarity index 100% rename from dev/planning.md rename to dev/PLAN.md diff --git a/dev/RULES.md b/dev/RULES.md index e9479e8b..2b71b56f 100644 --- a/dev/RULES.md +++ b/dev/RULES.md @@ -30,9 +30,85 @@ Rules: - Run the **targeted** suite first (fast iteration), then the **full** suite before considering a phase done (regression check). +### Fallback when `ct-dev-mcp` is unavailable + +If the MCP server cannot be reached, fall back to the standard Julia interface — always +capture with `tee`: + +```bash +# All tests +julia --project -e 'using Pkg; Pkg.test("MyPackage")' 2>&1 | tee /tmp/_all.log + +# Targeted scope +julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["suite//*"])' 2>&1 | tee /tmp/_.log + +# All tests including optional ones +julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["-a"])' 2>&1 | tee /tmp/_all.log +``` + +Use `generate_report` on the captured log once MCP is available again. + +--- + +## 2. Coverage + +Coverage measures which source lines are exercised by the test suite. + +### Prerequisites + +`Coverage.jl` must be installed in the **base Julia environment** (not the project +environment): + +```bash +julia --project=@v1.xx -e 'using Pkg; Pkg.add("Coverage")' +``` + +This is required because coverage post-processing runs outside the package project. + +### Running coverage + +```bash +julia --project=@. -e 'using Pkg; Pkg.test("MyPackage"; coverage=true); include("test/coverage.jl")' 2>&1 | tee /tmp/_coverage.log +``` + +**Outputs** (under `.coverage/`): + +- `lcov.info` — LCOV format, consumed by Codecov/CI. +- `cov_report.md` — human-readable summary of coverage gaps. +- `cov/` — per-file `.cov` detail files. + +### Rules + +- Do **not** commit `.coverage/`; add it to `.gitignore`. +- Run coverage on the **full** suite (not a subset), or results are misleading. +- Read `cov_report.md` to identify uncovered lines; do not chase 100% — cover logic, + not trivial accessors. + --- -## 2. Building documentation — "draft first" workflow +## 3. Building documentation — "draft first" workflow + +### Build command + +From the package root: + +```bash +julia --project=. docs/make.jl 2>&1 | tee /tmp/_docs.log +``` + +`make.jl` prepends both `docs/` and the package root to `LOAD_PATH`; no `Pkg.develop` +step is needed. + +### Local preview + +Depends on the backend (see [`philosophy/documentation.md`](philosophy/documentation.md)): + +- **DocumenterVitepress** (current ecosystem standard): `npx serve docs/build/1 --listen 5173` + or with LiveServer: `julia --project=docs -e 'using LiveServer; LiveServer.serve(dir="docs/build/1", single_page=true)'` +- **Documenter.jl** (plain HTML): `open docs/build/index.html` (macOS) / + `xdg-open docs/build/index.html` (Linux). + +### Draft-first workflow `docs/make.jl` exposes a `draft` switch. In **draft mode**, the Julia code in `@example`/`@setup` blocks is **not executed**: the build is fast and validates the @@ -66,7 +142,7 @@ On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E --- -## 3. Git & commits +## 4. Git & commits - **Never commit or push without explicit user approval.** Even after validated work, ask before `git commit`/`git push`. @@ -79,7 +155,7 @@ On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E --- -## 4. Editing files +## 5. Editing files - Prefer the **dedicated tools** (read/edit/write) over `cat`/`sed`/`awk`/`echo` in the shell for reading or modifying files. @@ -88,7 +164,7 @@ On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E --- -## 5. Capturing long output +## 6. Capturing long output - Any verbose command (tests, doc build): `… 2>&1 | tee /tmp/_.log`. - Name the log by package + scope to avoid collisions between sessions. @@ -96,7 +172,7 @@ On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E --- -## 6. Ask vs proceed +## 7. Ask vs proceed - **Ask** before any hard-to-reverse or outward-facing action (commit, push, sending to an external service, deleting/overwriting files you did not create). @@ -111,7 +187,8 @@ On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E - [ ] Tests run via `ct-dev-mcp` (`get_test_command` → run+`tee` → `generate_report`). - [ ] Targeted suite green, then full suite green. -- [ ] Docs built in draft (links OK), then per file, then full. +- [ ] Coverage run on the full suite; `cov_report.md` reviewed. +- [ ] Docs built in draft (links OK), then per file, then full; previewed locally. - [ ] No git action without explicit approval. - [ ] Dedicated file tools used; files read before editing. - [ ] Long output captured via `tee` under `/tmp`. diff --git a/dev/philosophy/documentation.md b/dev/philosophy/documentation.md index eb7dda35..43f90b58 100644 --- a/dev/philosophy/documentation.md +++ b/dev/philosophy/documentation.md @@ -20,6 +20,30 @@ How docstrings become a published site via Documenter.jl and 5. **Cross-refs resolve at build time** — every `@extref` is backed by an `InterLinks` entry in `make.jl`. +## Backend + +Two backends are used in the ecosystem: + +| Backend | Format | Node.js | +| --- | --- | --- | +| `Documenter.jl` | classic HTML | not required | +| `DocumenterVitepress` | VitePress site | required | + +Selected via the `format` keyword in `makedocs`: + +```julia +# Documenter.jl (classic) +format = Documenter.HTML(...) + +# DocumenterVitepress +format = DocumenterVitepress.MarkdownVitepress(; repo=..., devbranch=..., devurl=...) +``` + +Both backends use the same Julia API (`makedocs`, `with_api_reference`, `InterLinks`, +`draft`). DocumenterVitepress adds a `docs/src/.vitepress/` directory for the VitePress +site configuration and requires `node_modules/` in `docs/` (gitignored; install once +with `npm install` from `docs/`). + ## Layout ```text @@ -28,6 +52,7 @@ docs/ ├── api_reference.jl # generate_api_reference() + with_api_reference() + cleanup ├── inventories/ # InterLinks fallback inventories (one per dependency) └── src/ + ├── .vitepress/ # VitePress config (DocumenterVitepress only) ├── index.md # landing page ├── / # hand-written guides └── api/ # auto-generated (removed after build) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index ab3c0018..00000000 --- a/docs/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Documentation Guide for CTBase - -This directory contains the source files and build scripts for the `CTBase.jl` documentation. - -## Overview - -The documentation is built using [Documenter.jl](https://github.com/JuliaDocs/Documenter.jl). It features an automated API reference generation system provided by the `DocumenterReference` extension in `CTBase.jl`. - -## Building the Documentation - -From the root of the repository: - -```bash -julia --project=. docs/make.jl -``` - -`make.jl` prepends both `docs/` and the package root to `LOAD_PATH`, so the package -project is picked up automatically — no `Pkg.develop` step needed. - -### Draft mode (fast link validation) - -Set `draft = true` at the top of `docs/make.jl` to skip `@repl`/`@example` block -execution. This is much faster when iterating on cross-references and page structure: - -```bash -# edit docs/make.jl: draft = true (line 16) -julia --project=. docs/make.jl -``` - -!!! note "warnonly" - `make.jl` uses `warnonly=[:cross_references]`, so cross-reference warnings do not - abort the build. Other errors (missing pages, broken `@repl` blocks) still fail. - -## Viewing the Documentation - -After a successful build, the generated HTML files are located in `docs/build/`. You can open `index.html` in your browser: - -```bash -# macOS -open docs/build/index.html - -# Linux -xdg-open docs/build/index.html -``` - -## Directory Structure - -- `src/`: Contains the manual markdown files (Introduction, Getting Started, Guides, etc.). -- `make.jl`: The main build script for Documenter. -- `api_reference.jl`: Contains the logic for automatic API reference generation. It extracts docstrings from the source code and creates temporary markdown files. -- `build/`: The directory where the static website is generated (ignored by git). - -## Automated API Reference - -The `api_reference.jl` script uses `CTBase.automatic_reference_documentation()` to scan the modules. -- It generates public and private API pages for each sub-module. -- These files are created temporarily in `docs/src/` during the build process. -- The `with_api_reference()` wrapper in `make.jl` ensures these temporary files are deleted after the build. diff --git a/test/README.md b/test/README.md deleted file mode 100644 index d05c79e7..00000000 --- a/test/README.md +++ /dev/null @@ -1,187 +0,0 @@ -# Testing Guide for CTBase - -This directory contains the test suite for `CTBase.jl`. It follows the testing conventions and infrastructure provided by [CTBase.jl](https://github.com/control-toolbox/CTBase.jl). - -For detailed guidelines on testing and coverage, please refer to: - -- [CTBase Test Coverage Guide](https://control-toolbox.org/CTBase.jl/stable/test-coverage-guide.html) -- [CTBase TestRunner Extension](https://github.com/control-toolbox/CTBase.jl/blob/main/ext/TestRunner.jl) -- [CTBase CoveragePostprocessing](https://github.com/control-toolbox/CTBase.jl/blob/main/ext/CoveragePostprocessing.jl) - ---- - -## 1. Running Tests - -Tests are executed using the standard Julia Test interface, enhanced by `CTBase.TestRunner`. - -### Default Run (All Enabled Tests) - -Runs all tests enabled by default in `test/runtests.jl`. - -```bash -julia --project -e 'using Pkg; Pkg.test("CTBase")' -``` - -### Running Specific Test Groups - -You can run specific test files or groups using the `test_args` argument. The argument supports glob-style patterns. - -**Run all tests in the `core` directory:** - -```bash -julia --project -e 'using Pkg; Pkg.test("CTBase"; test_args=["suite/core/*"])' -``` - -**Run specific test files:** - -```bash -julia --project -e 'using Pkg; Pkg.test("CTBase"; test_args=["suite/core/test_default", "suite/unicode/test_utils"])' -``` - -### Running All Tests (Including Optional/Long Tests) - -To run absolutely every test available (including those potentially marked as optional or skipped by default): - -```bash -julia --project -e 'using Pkg; Pkg.test("CTBase"; test_args=["-a"])' -``` - -## 2. Coverage - -To generate a coverage report, you must run the tests with `coverage=true` and then execute the coverage post-processing script. - -### ⚠️ Prerequisites - -**Important**: The `Coverage` package must be installed in your base Julia environment for coverage to work properly: - -```bash -# In your base Julia environment (not the project environment) -julia --project=@v1.12 -e 'using Pkg; Pkg.add("Coverage")' -``` - -This is required because coverage processing happens at the Julia level and needs the `Coverage` package to be available globally. - -### Command - -```bash -julia --project=@. -e 'using Pkg; Pkg.test("CTBase"; coverage=true); include("test/coverage.jl")' -``` - -**Outputs:** - -- `.coverage/lcov.info`: LCOV format file (useful for CI integration like Codecov). -- `.coverage/cov_report.md`: Human-readable summary of coverage gaps. -- `.coverage/cov/`: detailed `.cov` files. - -## 3. Adding New Tests - -### File and Function Naming - -- **File Name:** Must follow the pattern `test_.jl` (e.g., `test_default.jl`). -- **Entry Function:** The file **MUST** contain a function named `test_()` (matching the filename) that serves as the entry point. - -**Example (`test/suite/core/test_default.jl`):** - -```julia -module TestCore - -using Test -using CTBase -using Main.TestOptions # Access shared test options - -function test_default() - @testset "Core Tests" verbose = VERBOSE showtiming = SHOWTIMING begin - # Your tests here - end -end - -end # module - -# CRITICAL: Redefine the function in the outer scope so TestRunner can find it -test_default() = TestCore.test_default() -``` - -### Registering the Test - -All test files in `test/suite/*/` are automatically discovered by the pattern `"suite/*/test_*"` in `test/runtests.jl`. Simply place your test file in the appropriate subdirectory under `test/suite/`. - -## 4. Best Practices & Rules - -### ⚠️ Crucial: Struct Definitions - -**NEVER define `struct`s inside the test function.** -All helper methods, mocks, and structs must be defined at the **top-level** of the file (or module). Defining structs inside the function causes world-age issues and invalidates precompilation. - -### Test Structure - -- **Unit vs. Integration:** Clearly separate unit tests (testing single functions/components in isolation) from integration tests (testing the interaction between components). -- **Mocks and Fakes:** Use mock objects or fake implementations to isolate the code under test. -- **Qualification of methods**: always **qualify the method call** even if a method is exported (e.g., `CTBase.ctindice(...)`). This makes it explicit what is being tested and avoids any ambiguity. -- **Verification of exports**: dedicated tests should be added to verify that methods are correctly exported when necessary (e.g., using `isdefined(CTBase, :...)`). - -### Directory Structure - -All test files are organized under `test/suite/` to maintain orthogonal relationship with the source code structure. Place your test file in the appropriate subdirectory based on functionality: - -- `suite/core/`: Core module tests (ctNumber, __display, internal utilities) -- `suite/unicode/`: Unicode module tests (ctindice, ctindices, ctupperscript, ctupperscripts) -- `suite/descriptions/`: Descriptions module tests (add, complete, remove, integration) -- `suite/exceptions/`: Exceptions module tests (exception types, display, configuration) -- `suite/extensions/`: Extensions module tests (TestRunner, DocumenterReference, CoveragePostprocessing) -- `suite/meta/`: Meta tests (Aqua.jl quality checks, code quality) - -### Module Testing Pattern - -Each test file should follow the modular pattern: - -```julia -module Test - -using Test -using CTBase -using Main.TestOptions - -# Define all structs and helpers at top-level -struct DummyTag <: CTBase.DevTools.AbstractTag end - -function test_() - @testset " Tests" verbose = VERBOSE showtiming = SHOWTIMING begin - # Test public API - @test CTBase.(args) == expected - - # Test internal functions with qualification - @test CTBase..(args) == expected - - # Test error cases - @test_throws CTBase. CTBase.(invalid_args) - end -end - -end # module - -# Export to outer scope -test_() = Test.test_() -``` - -## 5. Test Organization Principles - -### Orthogonal Structure - -The test structure mirrors the source code structure: - -```text -src/ -├── Core/Core.jl → test/suite/core/test_default.jl -├── Unicode/Unicode.jl → test/suite/unicode/test_utils.jl -├── Descriptions/Descriptions.jl → test/suite/descriptions/test_description.jl -├── DevTools/DevTools.jl → test/suite/extensions/test_*.jl -└── Exceptions/ → test/suite/exceptions/test_*.jl -``` - -### Internal vs Public API Testing - -- **Public API**: Test functions accessible via `CTBase.f` -- **Internal Functions**: Test via qualification `CTBase.SubModule.f` -- **Extension Tags**: Test via qualification `CTBase.DevTools.TagType` - -This ensures tests validate both the user-facing API and internal implementation details. From 6e9b94dc54c972d289934468470364613e6b7a28 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 21 Jun 2026 14:55:15 +0200 Subject: [PATCH 4/5] foo --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b3bc4b36..3ca3aa61 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ reports/ .vscode/ .extras/ .tmp/ +.claude/ # Temporary test artifacts (created during test runs) test/_test_*/ \ No newline at end of file From 7f78806ce707afbb6554348421f30d9639890d95 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 21 Jun 2026 15:10:41 +0200 Subject: [PATCH 5/5] refactor: move dev/ to Handbook, add directory READMEs, update AGENTS/CLAUDE - Remove dev/ directory (moved to control-toolbox/Handbook) - AGENTS.md: rewrite with generic layout, redirect Developer Resources to Handbook - CLAUDE.md: identical to AGENTS.md - README.template.md: enhance description of CTBase's foundational role - Add README.md in src/, ext/, test/, docs/ with package-specific context - All point to control-toolbox Handbook for conventions Co-Authored-By: Cascade --- AGENTS.md | 70 ++---- CLAUDE.md | 61 ++--- README.template.md | 2 +- dev/PHILOSOPHY.md | 64 ------ dev/PLAN.md | 262 ---------------------- dev/RULES.md | 194 ---------------- dev/philosophy/docstrings.md | 136 ----------- dev/philosophy/documentation.md | 169 -------------- dev/philosophy/exceptions.md | 104 --------- dev/philosophy/modules.md | 155 ------------- dev/philosophy/testing.md | 126 ----------- dev/philosophy/types-traits-interfaces.md | 166 -------------- docs/README.md | 15 ++ ext/README.md | 7 + src/README.md | 8 + test/README.md | 9 + 16 files changed, 92 insertions(+), 1456 deletions(-) delete mode 100644 dev/PHILOSOPHY.md delete mode 100644 dev/PLAN.md delete mode 100644 dev/RULES.md delete mode 100644 dev/philosophy/docstrings.md delete mode 100644 dev/philosophy/documentation.md delete mode 100644 dev/philosophy/exceptions.md delete mode 100644 dev/philosophy/modules.md delete mode 100644 dev/philosophy/testing.md delete mode 100644 dev/philosophy/types-traits-interfaces.md create mode 100644 docs/README.md create mode 100644 ext/README.md create mode 100644 src/README.md create mode 100644 test/README.md diff --git a/AGENTS.md b/AGENTS.md index 7a2efe19..b68cde71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,72 +4,38 @@ Quick-reference for any agent working on this repository. --- -## Project Overview +## Repository Layout -**CTBase.jl** is the foundational Julia package in the [control-toolbox](https://github.com/control-toolbox) ecosystem. -It provides the **base layer**: shared types, exceptions, extensions infrastructure, and development tools. - ---- - -## Source Architecture - -Submodule layout (all public symbols accessed via qualified paths — no top-level exports): +Standard control-toolbox package structure. Each directory may contain a `README.md` +with package-specific details — read it before working in that directory. ```text -src/ -├── CTBase.jl # Top-level manifest — exports nothing -├── Core/ # Core types and utilities -├── Descriptions/ # Type descriptions and metadata -├── Exceptions/ # Structured exception types -├── Extensions/ # Extension infrastructure -└── Unicode/ # Unicode utilities - -ext/ -├── CoveragePostprocessing.jl # Coverage report postprocessing -├── DocumenterReference.jl # Documenter.jl reference generation -└── TestRunner.jl # Test runner with progress bar - -test/suite/ # Tests organised by functionality (not by src layout) -docs/ # Documenter.jl site (auto-generated API) -dev/ # Code philosophy, operational rules, plan template (versioned) +src/ # Source code: one submodule per responsibility, no top-level exports +ext/ # Weak-dependency extensions (loaded on demand by Julia) +test/suite/ # Test suite: organised by functionality, not by src/ layout +docs/ # Documentation site (DocumenterVitepress) ``` --- -## Developer resources - -| File | Purpose | -|---|---| -| [`dev/philosophy/PHILOSOPHY.md`](dev/philosophy/PHILOSOPHY.md) | Code philosophy — modules, types/traits, exceptions, docstrings, testing, docs | -| [`dev/RULES.md`](dev/RULES.md) | Operational rules — running tests (MCP), building docs, git, output capture | -| [`dev/PLAN.md`](dev/PLAN.md) | Plan template — phases, steps, human checkpoints | - ---- - -## Devin Workflows +## Developer Resources -| Workflow | Trigger | Purpose | -|---|---|---| -| `architecture.md` | — | Introducing new types, restructuring modules, reviewing SOLID/patterns | -| `docstrings.md` | — | Writing or reviewing Julia docstrings | -| `documentation.md` | `glob: docs/**/*` | Documenter.jl layout, `make.jl` template, `api_reference.jl`, `InterLinks` setup | -| `exceptions.md` | — | Adding error paths, contract stubs, argument validation | -| `modules.md` | `glob: src/**/*.jl, ext/**/*.jl` | Submodule conventions: qualified imports, manifest pattern, export policy, DAG ordering | -| `performance.md` | — | Hot paths, inner loops, profiling, benchmarking | -| `plan.md` | — | Writing an implementation plan before coding | -| `testing-creation.md` | — | Writing or reviewing test files under `test/suite/` | -| `testing-execution.md` | `model_decision` | How to run tests (commands, `tee` capture) | -| `type-stability.md` | — | New structs, parametric types, `@inferred` test design | +Design philosophy, operational rules, and plan templates live in the +[control-toolbox Handbook](https://github.com/control-toolbox/Handbook): -Workflows live in `.devin/workflows/`. +| Topic | Link | +| --- | --- | +| Code philosophy (modules, types/traits, exceptions, docstrings, testing, docs) | [`PHILOSOPHY.md`](https://raw.githubusercontent.com/control-toolbox/Handbook/refs/heads/main/PHILOSOPHY.md) | +| Operational rules (tests, coverage, docs, git) | [`RULES.md`](https://raw.githubusercontent.com/control-toolbox/Handbook/refs/heads/main/RULES.md) | +| Plan template | [`PLAN.md`](https://raw.githubusercontent.com/control-toolbox/Handbook/refs/heads/main/PLAN.md) | --- ## Key Conventions -- **No top-level exports** — use `CTBase.Submodule.symbol` everywhere. -- **Qualified imports** — `using PackageName: PackageName`, never bare `using`. +- **No top-level exports** — use `Package.Submodule.symbol` everywhere. +- **Qualified imports** — `import Pkg: Pkg`, never bare `using`. - **Fake types at module top-level** — never inside test functions. -- **Plans before code** — write a plan and confirm with the user before touching files. Template: [`dev/PLAN.md`](dev/PLAN.md). +- **Plans before code** — write a plan and confirm with the user before touching files. - **Docstrings last** — written only after all implementation steps are stable. - **Never commit or push without explicit user approval.** diff --git a/CLAUDE.md b/CLAUDE.md index 6ee1f984..8ba76ac8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,34 +1,41 @@ -# CTBase.jl — Claude project context +# CTBase.jl — Agent Navigation Guide -## Essential rules +Quick-reference for any agent working on this repository. -1. **Never commit or push without explicit approval.** Ask first, every time. -2. **Run tests via the `ct-dev-mcp` MCP** — `get_test_command` → run with `tee` → - `generate_report`. Never invent the test command. -3. **Build docs draft-first** — `draft = true` globally to validate links, then per file - with `Draft = false`, then full build. See `dev/RULES.md`. -4. **Qualify everything** — `Module.symbol` at every call site; `import Pkg: Pkg` not - `using Pkg`; no top-level package exports. -5. **Write a plan before coding** — any task touching more than one file or a public - interface needs a plan confirmed by the user first. Template in `dev/PLAN.md`. -6. **Docstrings last** — written only after the API is stable. -7. **Fake types at module top-level** — never inside test functions (world-age issues). +--- -## Where to find more +## Repository Layout -| Topic | File | -| --- | --- | -| Code philosophy (modules, types/traits, exceptions, docstrings, testing, docs) | [`dev/philosophy/`](dev/philosophy/PHILOSOPHY.md) | -| Operational rules (MCP, doc build, git, output capture) | [`dev/RULES.md`](dev/RULES.md) | -| Plan template | [`dev/PLAN.md`](dev/PLAN.md) | - -## Project structure (quick reference) +Standard control-toolbox package structure. Each directory may contain a `README.md` +with package-specific details — read it before working in that directory. ```text -src/CTBase.jl # top-level manifest — exports nothing -src//.jl # submodule manifests -ext/ # weak-dependency extensions (CoveragePostprocessing, DocumenterReference, TestRunner) -test/suite/ # tests by functionality, not by src layout -docs/ # Documenter.jl site -dev/ # philosophy, rules, planning template (versioned) +src/ # Source code: one submodule per responsibility, no top-level exports +ext/ # Weak-dependency extensions (loaded on demand by Julia) +test/suite/ # Test suite: organised by functionality, not by src/ layout +docs/ # Documentation site (DocumenterVitepress) ``` + +--- + +## Developer Resources + +Design philosophy, operational rules, and plan templates live in the +[control-toolbox Handbook](https://github.com/control-toolbox/Handbook): + +| Topic | Link | +| --- | --- | +| Code philosophy (modules, types/traits, exceptions, docstrings, testing, docs) | [`PHILOSOPHY.md`](https://github.com/control-toolbox/Handbook/blob/main/PHILOSOPHY.md) | +| Operational rules (tests, coverage, docs, git) | [`RULES.md`](https://github.com/control-toolbox/Handbook/blob/main/RULES.md) | +| Plan template | [`PLAN.md`](https://github.com/control-toolbox/Handbook/blob/main/PLAN.md) | + +--- + +## Key Conventions + +- **No top-level exports** — use `Package.Submodule.symbol` everywhere. +- **Qualified imports** — `import Pkg: Pkg`, never bare `using`. +- **Fake types at module top-level** — never inside test functions. +- **Plans before code** — write a plan and confirm with the user before touching files. +- **Docstrings last** — written only after all implementation steps are stable. +- **Never commit or push without explicit user approval.** diff --git a/README.template.md b/README.template.md index 48801575..b3473c14 100644 --- a/README.template.md +++ b/README.template.md @@ -5,7 +5,7 @@ For instructions on how to customize this README.template.md and use the central please see the user guide: https://github.com/orgs/control-toolbox/discussions/67 --> -The CTBase.jl package is part of the [control-toolbox ecosystem](https://github.com/control-toolbox). +CTBase.jl is the **foundational layer** of the [control-toolbox ecosystem](https://github.com/control-toolbox): it provides the shared types, exception infrastructure, extensions scaffolding, and development utilities that all other packages in the ecosystem build on. diff --git a/dev/PHILOSOPHY.md b/dev/PHILOSOPHY.md deleted file mode 100644 index 4a8ace07..00000000 --- a/dev/PHILOSOPHY.md +++ /dev/null @@ -1,64 +0,0 @@ -# Code philosophy - -Design philosophy for the control-toolbox ecosystem, written **abstractly**: the -principles apply to all packages, but the examples and templates stay **generic** -(no package-specific symbols). For *tools and procedures* (tests, docs, git), see -[`RULES.md`](RULES.md). - -The detail files referenced below live in the [`philosophy/`](philosophy/) directory. - -## The tenets on one page - -1. **One module per responsibility.** Each submodule has a single role, its own - directory, its own manifest. The package manifest exports **nothing**. - → [`modules.md`](philosophy/modules.md) - -2. **Everything is qualified.** Import modules, not their symbols; call - `Module.symbol` everywhere. Explicit origin, safe refactors, no shadowing. - → [`modules.md`](philosophy/modules.md) - -3. **One abstract type per *noun*, one trait-parameter per *adjective*.** Conceptual - variants ("is it an X or a Y") are types; orthogonal axes (autonomous?, in-place?, …) - are traits in a type parameter. Dispatch by extracting the trait. - → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) - -4. **Program against abstractions.** Methods live on abstract types as much as - possible; contracts are `NotImplemented` stubs; subtypes honor the contract (LSP). - → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) - -5. **SOLID, DRY, KISS, YAGNI.** Single responsibility, open/closed via dispatch, no - duplication, the simplest thing that works, nothing speculative. - → [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) - -6. **Structured errors.** Seven typed exceptions; sharp rule: single-argument value → - `IncorrectArgument`; relation/state/composition → `PreconditionError`; unimplemented - contract → `NotImplemented`; optional dependency → `ExtensionError`. - → [`exceptions.md`](philosophy/exceptions.md) - -7. **Type stability by default.** Parametric types, no `Any` in hot paths, function - barriers, verified with `@inferred`. - → [`types-traits-interfaces.md`](types-traits-interfaces.md#type-stability) - -8. **Everything is documented.** A docstring on every symbol, fixed templates, - cross-references `@ref`/`@extref`, safe and reproducible examples. - → [`docstrings.md`](philosophy/docstrings.md) - -9. **Tests: module + callable function + everything qualified.** Each test file is a - module with an entry function redefined in the outer scope; fakes at module - top-level; categories unit / integration / contract / error. - → [`testing.md`](philosophy/testing.md) - -10. **Auto-generated API docs, public *and* private; guides separate.** Users reach - internals via qualified paths, so both are documented. - → [`documentation.md`](philosophy/documentation.md) - -## Index - -| File | Content | -| --- | --- | -| [`modules.md`](philosophy/modules.md) | Submodule organization, imports/qualification, DAG, exports | -| [`types-traits-interfaces.md`](philosophy/types-traits-interfaces.md) | Types vs traits, interfaces/contracts, SOLID/DRY/YAGNI, type stability | -| [`exceptions.md`](philosophy/exceptions.md) | The 7 exceptions and the choice rule | -| [`docstrings.md`](philosophy/docstrings.md) | Docstring templates, cross-references, example safety | -| [`testing.md`](philosophy/testing.md) | Categories, fakes/stubs, **module + callable function template** | -| [`documentation.md`](philosophy/documentation.md) | API generation, guides, draft workflow | diff --git a/dev/PLAN.md b/dev/PLAN.md deleted file mode 100644 index 3158d908..00000000 --- a/dev/PLAN.md +++ /dev/null @@ -1,262 +0,0 @@ -# Implementation plan template - -A generic template for structuring a development task. Keep plans simple — the goal is -to align with the human before touching code, not to produce a 50-step document. - -## When to write a plan - -Before any task that: - -- touches more than one file, -- changes a public interface, or -- involves a non-obvious design decision. - -Write the plan, share it, wait for confirmation before starting. - ---- - -## Template - -````markdown -# Plan: - -## What and why -<2-3 sentences: what changes, why now, what it unblocks.> - -## Scope -- Files added: … -- Files modified: … -- Files deleted: … -- Public API changes: yes / no (describe if yes) - -## What changes - -| File | After | -|---|---| -| `src/A/B.jl` | split into `x.jl` + `y.jl` | - -**No behaviour change** / or: **Public API change: describe.** - -## Dependency graph - -```text -Module A - ↓ -Module B ← new - ↓ -Module C -``` - -## Step 0 — Branch - -Use MCP git tools when available: - -- `mcp3_git_checkout` → `develop`; `git pull` -- `mcp3_git_create_branch` → `feat/`; `mcp3_git_checkout` → new branch - -Fallback: - -```bash -git checkout develop && git pull -git checkout -b feat/ -``` - -## Phases - -### Phase A — -Steps: -1. … -2. … -Checkpoint: - -```bash -# MCP (preferred) -get_test_command test_args=["suite/"] -then generate_report - -# Fallback -julia --project=@. -e 'using Pkg; Pkg.test(; test_args=["suite/"])' 2>&1 | tee /tmp/_phaseA.log -grep -E "Error|Fail|Test Summary" /tmp/_phaseA.log -``` - -All green before moving on. - -### Phase B — - -Steps: -1. … -Checkpoint: - -```bash -# MCP (preferred) -get_test_command test_args=["suite/", "suite/"] -then generate_report - -# Final phase only: full suite -julia --project=@. -e 'using Pkg; Pkg.test()' 2>&1 | tee /tmp/_final.log -grep -E "Error|Fail|Test Summary" /tmp/_final.log -``` - -… - -## Human checkpoints - -- ⛔ Ask before first commit. -- ⛔ Ask before pushing to a shared branch. -- ⛔ Ask before deleting files that were not created in this task. -- ⛔ Ask if a design decision arises that was not in the plan. - -## Out of scope - - - -## File summary - -**Added**: … -**Modified**: … -**Deleted**: … -```` - ---- - -## Rules for filling in the template - -**Phases** — one phase = one coherent unit that can be tested independently. A phase -ends with a test checkpoint. Never skip the checkpoint. - -**Test checkpoints** — always use the MCP `get_test_command` tool with `test_args` to -run only the affected test groups, then `generate_report` to parse the result. If MCP -is unavailable, fall back to: - -```bash -# Phase-specific (preferred during intermediate phases) -julia --project=@. -e 'using Pkg; Pkg.test(; test_args=["suite/"])' \ - 2>&1 | tee /tmp/_.log -grep -E "Error|Fail|Test Summary" /tmp/_.log - -# Full suite (mandatory for the last phase only) -julia --project=@. -e 'using Pkg; Pkg.test()' \ - 2>&1 | tee /tmp/_final.log -grep -E "Error|Fail|Test Summary" /tmp/_final.log -``` - -Never run the full suite for intermediate phases — it is too slow and hides errors. - -**Steps** — file-level granularity. "Edit `src/X/y.jl` to add method `f`" not "implement -the feature". Concrete enough that a step is either done or not. - -**Human checkpoints** — mandatory stops. The human decides whether to commit, push, or -proceed after a design change. The ⛔ symbol marks these explicitly so they are not -missed. - -**Docstrings** — always the last step of the last phase, once the API is stable. - -**Code snippets** — include short code snippets directly in the plan to guide the -implementer. This applies to: - -- *Source changes*: show the before/after signature or struct definition. -- *Call-site updates*: show the exact old and new lines when renaming symbols. -- *Test patterns*: show the test structure (fake type + testset skeleton) when the - test pattern is non-obvious. -Snippets should be *concise shapes*, not full implementations. - -**What changes** — use a table (current file → after) for structural tasks (splits, -renames, moves). One row per file. End with a bold statement: "No behaviour change" or -"Public API change: describe." - -**Dependency graph** — include when the task adds or reorders modules. Show the full -dependency chain; mark new nodes with `← new`. Omit if nothing changes. - -**Step 0 — Branch** — always the first executable step. Prefer MCP git tools -(`mcp3_git_checkout`, `mcp3_git_create_branch`). Include the bash fallback for -environments where MCP is unavailable. - -**Out of scope** — explicit. Prevents scope creep and clarifies what the human should -not expect after the plan is executed. - -**File summary** — last section. Three categories: Added / Modified / Deleted. Written -once all steps are finalized so the count is accurate. Skip empty categories. - ---- - -## Concrete example structure (abbreviated) - -````markdown -# Plan: rename content → dynamics trait - -## What and why -Rename AbstractContentTrait and its values to AbstractDynamicsTrait / StateDynamics / -HamiltonianDynamics / AugmentedHamiltonianDynamics. "content" is too neutral; "dynamics" -names the axis. Purely mechanical, no behavior change. - -## Scope -- Files modified: src/Traits/content.jl (rename to dynamics.jl), src/Traits/Traits.jl, - src/Configs/*, src/Solutions/building.jl, docs/api_reference.jl -- Files renamed: test/suite/traits/test_content.jl → test_dynamics.jl -- Public API changes: yes — AbstractContentTrait → AbstractDynamicsTrait (+ values) - -## What changes - -| File | After | -|---|---| -| `src/Traits/content.jl` | renamed → `dynamics.jl`; all symbols renamed | -| `src/Traits/Traits.jl` | updated include + exports | - -**Public API change**: `AbstractContentTrait` → `AbstractDynamicsTrait` (+ values). - -## Step 0 — Branch - -- `mcp3_git_checkout` → `develop`; `git pull` -- `mcp3_git_create_branch` → `feat/rename-dynamics`; `mcp3_git_checkout` → new branch - -## Phases - -### Phase A — Rename Traits module -Steps: -1. Rename src/Traits/content.jl → dynamics.jl; update all names inside. -2. Update include path in src/Traits/Traits.jl; update exports. -Checkpoint: - -```bash -get_test_command test_args=["suite/traits"] # MCP -# fallback: julia --project=@. -e 'using Pkg; Pkg.test(; test_args=["suite/traits"])' 2>&1 | tee /tmp/rename_traits.log -``` - -### Phase B — Adapt consumers (Configs, Solutions) - -Steps: - -1. Update src/Configs/* (content_trait → dynamics_trait, value names). -2. Update src/Solutions/building.jl. -Checkpoint: - -```bash -get_test_command test_args=["suite/configs", "suite/solutions"] # MCP -``` - -### Phase C — Tests + docs - -Steps: - -1. Rename and update test/suite/traits/test_content.jl → test_dynamics.jl. -2. Update docs/api_reference.jl file list. -Checkpoint (full suite — last phase): - -```bash -get_test_command # MCP, no test_args = full suite -# fallback: julia --project=@. -e 'using Pkg; Pkg.test()' 2>&1 | tee /tmp/rename_final.log -``` - -## Human checkpoints - -- ⛔ Ask before commit after Phase C. - -## Out of scope - -Parametrizing AbstractSystem/AbstractFlow by the dynamics trait (see action_plan.md Phase C/D). - -## File summary - -**Modified**: `src/Traits/content.jl` (→ `dynamics.jl`), `src/Traits/Traits.jl`, `src/Configs/*`, `src/Solutions/building.jl`, `docs/api_reference.jl` -**Renamed**: `test/suite/traits/test_content.jl` → `test_dynamics.jl` -```` diff --git a/dev/RULES.md b/dev/RULES.md deleted file mode 100644 index 2b71b56f..00000000 --- a/dev/RULES.md +++ /dev/null @@ -1,194 +0,0 @@ -# RULES — tools and procedures for a development agent - -**Generic** rules (valid for any Julia package in the control-toolbox ecosystem, with -no reference to a specific package's code). They describe *how to work*: run tests, -build docs, handle git, capture output. For *how to design*, see -[`philosophy/`](philosophy/PHILOSOPHY.md). - ---- - -## 1. Running tests — via the `ct-dev-mcp` MCP server - -Do not invent the test command. Use the MCP server that provides it and can parse the -result. - -1. **Get the command**: call the `get_test_command` tool with: - - `cwd` = package root (where `Project.toml` lives) — **required**; - - `test_args` = scope (empty = everything; `["test/suite/"]`; a single file; - a glob `["test/suite/*/*_flow*"]` passed **quoted** — TestRunner expands it, not - the shell). -2. **Run** the returned command verbatim. It already includes - `… 2>&1 | tee /tmp/ct-dev-mcp/__.log`. -3. **Analyze**: call `generate_report()` on the returned log for a structured - Markdown report (summary, first failures, compilation errors). - -Rules: -- **Always `tee` the full log**; never truncate with `tail -N` live — the first error - (often a compilation error) is usually *above* and would be lost, forcing a rerun. -- Inspect the saved log afterwards with `grep`/`rg`/`tail`/`less`, without rerunning. -- Logs go under `/tmp/…`; **never** commit them. -- Run the **targeted** suite first (fast iteration), then the **full** suite before - considering a phase done (regression check). - -### Fallback when `ct-dev-mcp` is unavailable - -If the MCP server cannot be reached, fall back to the standard Julia interface — always -capture with `tee`: - -```bash -# All tests -julia --project -e 'using Pkg; Pkg.test("MyPackage")' 2>&1 | tee /tmp/_all.log - -# Targeted scope -julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["suite//*"])' 2>&1 | tee /tmp/_.log - -# All tests including optional ones -julia --project -e 'using Pkg; Pkg.test("MyPackage"; test_args=["-a"])' 2>&1 | tee /tmp/_all.log -``` - -Use `generate_report` on the captured log once MCP is available again. - ---- - -## 2. Coverage - -Coverage measures which source lines are exercised by the test suite. - -### Prerequisites - -`Coverage.jl` must be installed in the **base Julia environment** (not the project -environment): - -```bash -julia --project=@v1.xx -e 'using Pkg; Pkg.add("Coverage")' -``` - -This is required because coverage post-processing runs outside the package project. - -### Running coverage - -```bash -julia --project=@. -e 'using Pkg; Pkg.test("MyPackage"; coverage=true); include("test/coverage.jl")' 2>&1 | tee /tmp/_coverage.log -``` - -**Outputs** (under `.coverage/`): - -- `lcov.info` — LCOV format, consumed by Codecov/CI. -- `cov_report.md` — human-readable summary of coverage gaps. -- `cov/` — per-file `.cov` detail files. - -### Rules - -- Do **not** commit `.coverage/`; add it to `.gitignore`. -- Run coverage on the **full** suite (not a subset), or results are misleading. -- Read `cov_report.md` to identify uncovered lines; do not chase 100% — cover logic, - not trivial accessors. - ---- - -## 3. Building documentation — "draft first" workflow - -### Build command - -From the package root: - -```bash -julia --project=. docs/make.jl 2>&1 | tee /tmp/_docs.log -``` - -`make.jl` prepends both `docs/` and the package root to `LOAD_PATH`; no `Pkg.develop` -step is needed. - -### Local preview - -Depends on the backend (see [`philosophy/documentation.md`](philosophy/documentation.md)): - -- **DocumenterVitepress** (current ecosystem standard): `npx serve docs/build/1 --listen 5173` - or with LiveServer: `julia --project=docs -e 'using LiveServer; LiveServer.serve(dir="docs/build/1", single_page=true)'` -- **Documenter.jl** (plain HTML): `open docs/build/index.html` (macOS) / - `xdg-open docs/build/index.html` (Linux). - -### Draft-first workflow - -`docs/make.jl` exposes a `draft` switch. In **draft mode**, the Julia code in -`@example`/`@setup` blocks is **not executed**: the build is fast and validates the -structure, the `@ref`/`@extref` links and the paths. In non-draft mode everything runs -(slow but actually checks the examples). - -```julia -# docs/make.jl -draft = true # does not execute Julia cells -``` - -A **single file** can also be forced to non-draft via a meta block, keeping the rest in -draft — to debug one file quickly: - -````markdown -```@meta -Draft = false -``` -```` - -**Recommended procedure**: - -1. **`draft = true`** (global) → build once. Fix all broken **links** (`@ref`/`@extref`) - and paths. Fast. -2. **File by file**: set one file to `Draft = false` (meta block), rebuild, debug its - `@example` blocks, fix. Repeat per page. Faster than running everything each time. -3. **`draft = false`** (global) → final full build, zero execution warnings. - -On every build, **`tee`** the output to `/tmp/…` and filter (`grep -E -"Error|Warning.*failed|MethodError|UndefVar"`). - ---- - -## 4. Git & commits - -- **Never commit or push without explicit user approval.** Even after validated work, - ask before `git commit`/`git push`. -- Do not commit on the default branch: branch off the correct base. -- Interactive flags (`-i`) are not supported in this environment. -- Prefer the `gh` CLI for GitHub operations (PRs, issues). -- End commit messages with the required `Co-Authored-By` trailer; end PR bodies with - the required generation note. -- Do not commit transient files (`/tmp/*.log`, doc-build artifacts). - ---- - -## 5. Editing files - -- Prefer the **dedicated tools** (read/edit/write) over `cat`/`sed`/`awk`/`echo` in the - shell for reading or modifying files. -- **Read before editing**; do not overwrite a file you have not read. -- An edit should match the surrounding code (style, naming, comment density). - ---- - -## 6. Capturing long output - -- Any verbose command (tests, doc build): `… 2>&1 | tee /tmp/_.log`. -- Name the log by package + scope to avoid collisions between sessions. -- Filter the log *afterwards*; do not rerun just to get more context. - ---- - -## 7. Ask vs proceed - -- **Ask** before any hard-to-reverse or outward-facing action (commit, push, sending to - an external service, deleting/overwriting files you did not create). -- **Proceed** on choices with an obvious default (style, conventional location), - stating it, rather than blocking on a question. -- Report outcomes faithfully: if tests fail, say so with the output; if a step was - skipped, say that. - ---- - -## Quick checklist - -- [ ] Tests run via `ct-dev-mcp` (`get_test_command` → run+`tee` → `generate_report`). -- [ ] Targeted suite green, then full suite green. -- [ ] Coverage run on the full suite; `cov_report.md` reviewed. -- [ ] Docs built in draft (links OK), then per file, then full; previewed locally. -- [ ] No git action without explicit approval. -- [ ] Dedicated file tools used; files read before editing. -- [ ] Long output captured via `tee` under `/tmp`. diff --git a/dev/philosophy/docstrings.md b/dev/philosophy/docstrings.md deleted file mode 100644 index bba96d08..00000000 --- a/dev/philosophy/docstrings.md +++ /dev/null @@ -1,136 +0,0 @@ -# Docstrings - -Every exported symbol *and* every internal component carries a docstring. Generic -templates below; use `DocStringExtensions` macros for signatures. - -## Principles - -1. **Completeness** — document every function, struct, abstract type, macro, module. -2. **Accuracy** — describe actual behavior, never aspirational. -3. **Clarity** — for readers fluent in Julia but new to the domain. -4. **Consistency** — follow the templates. - -Placement: **immediately above** the declaration, no blank line in between. - -## Required structure - -1. Signature via `$(TYPEDSIGNATURES)` (functions) or `$(TYPEDEF)` (types). -2. One-sentence summary. -3. Optional detail: behavior, constraints, invariants, edge cases. -4. Sections as applicable: `# Arguments`, `# Fields`, `# Returns`, `# Throws`, - `# Example(s)`, `# Notes`, `# References`, and a `See also:` line. - -## Templates (generic) - -### Function - -```julia -""" -$(TYPEDSIGNATURES) - -One sentence describing what the function does. - -# Arguments -- `a::TypeA`: description. -- `b::TypeB`: description. - -# Returns -- `RetType`: description. - -# Throws -- `ExceptionType`: when and why. - -# Example -\`\`\`julia-repl -julia> using MyPackage.SubA - -julia> do_thing(a, b) -expected_output -\`\`\` - -See also: [`MyPackage.SubA.related`](@ref), [`MyPackage.SubB.Other`](@ref) -""" -function do_thing(a::TypeA, b::TypeB)::RetType - # ... -end -``` - -### Struct - -```julia -""" -$(TYPEDEF) - -One sentence describing what this type represents. - -# Fields -- `field1::Type1`: description and constraints. -- `field2::Type2`: description and constraints. - -See also: [`MyPackage.SubA.related_type`](@ref) -""" -struct Thing{T} - field1::Type1 - field2::T -end -``` - -### Abstract type - -```julia -""" -$(TYPEDEF) - -One sentence describing the abstraction. - -# Interface Requirements - -Subtypes must implement: -- `required_method(::SubType)`: description. - -See also: [`MyPackage.SubA.ConcreteA`](@ref), [`MyPackage.SubA.ConcreteB`](@ref) -""" -abstract type AbstractThing end -``` - -## Cross-references - -- **Internal** (`@ref`): full module path including the root package. - `[`MyPackage.SubA.do_thing`](@ref)` — not `[`do_thing`](@ref)`. -- **External** (`@extref`): symbols from a dependency with its own docs, full path. - `[`OtherPkg.Sub.Sym`](@extref)`. Each must be backed by an `InterLinks` entry in - `make.jl`. - -Use `@ref` for symbols documented in the current package, `@extref` for dependencies. - -## Module-prefix convention in examples - -- Exported symbol after `using MyPackage.SubA`: call it bare (`do_thing(...)`). -- Internal symbol: prefix with the submodule (`SubA.helper(...)`). -- Public path shown in docs is always `RootPackage.SubModule.symbol`. - -## Example safety - -Examples must be safe and reproducible. - -- ✅ pure deterministic computations, constructors with simple inputs, queries on - created objects; start with `using RootPackage.SubModule`. -- ❌ file/network/DB/git operations, randomness without a seed, timing-dependent or - long-running (>1s) code, reliance on external/global state. -- If no safe runnable example exists: use a plain ```julia block (not ```julia-repl) - showing a conceptual usage pattern without claiming output. - -## Macros - -- `$(TYPEDEF)` — type signature for structs/abstract types. -- `$(TYPEDSIGNATURES)` — function signature with types. - -Use these instead of writing signatures by hand. - -## Checklist - -- [ ] Directly above the declaration; uses `$(TYPEDEF)`/`$(TYPEDSIGNATURES)`. -- [ ] One-sentence summary; all args/fields documented; returns and throws when relevant. -- [ ] Example is safe, runnable, and typical. -- [ ] `@ref` for internal, `@extref` for external; full module paths. -- [ ] No invented behavior; consistent terminology. diff --git a/dev/philosophy/documentation.md b/dev/philosophy/documentation.md deleted file mode 100644 index 43f90b58..00000000 --- a/dev/philosophy/documentation.md +++ /dev/null @@ -1,169 +0,0 @@ -# Documentation site - -How docstrings become a published site via Documenter.jl and -`CTBase.automatic_reference_documentation()`. Generic examples. Complements -[`docstrings.md`](docstrings.md) (what to write *inside* docstrings). - -## Principles - -1. **Auto-generated API reference** — never hand-write API pages; generate one per - submodule (and one unified internals page for all private symbols). -2. **Public + private documented, separately** — one flat page per submodule for - exported symbols (`public=true, private=false`); one unified "Internals" page for all - private symbols across all submodules and extensions (`public=false, private=true`). - Users reach internals via qualified paths, so internals are part of the documented - surface — but kept out of the main navigation. -3. **Guides separate from API** — narrative guides are hand-written under - `docs/src//`; the API reference is generated and cleaned up after the build. -4. **Index is the entry point** — `docs/src/index.md`: scope, module table, guide links, - a short Quick Start. -5. **Cross-refs resolve at build time** — every `@extref` is backed by an `InterLinks` - entry in `make.jl`. - -## Backend - -Two backends are used in the ecosystem: - -| Backend | Format | Node.js | -| --- | --- | --- | -| `Documenter.jl` | classic HTML | not required | -| `DocumenterVitepress` | VitePress site | required | - -Selected via the `format` keyword in `makedocs`: - -```julia -# Documenter.jl (classic) -format = Documenter.HTML(...) - -# DocumenterVitepress -format = DocumenterVitepress.MarkdownVitepress(; repo=..., devbranch=..., devurl=...) -``` - -Both backends use the same Julia API (`makedocs`, `with_api_reference`, `InterLinks`, -`draft`). DocumenterVitepress adds a `docs/src/.vitepress/` directory for the VitePress -site configuration and requires `node_modules/` in `docs/` (gitignored; install once -with `npm install` from `docs/`). - -## Layout - -```text -docs/ -├── make.jl # entry point; uses with_api_reference() -├── api_reference.jl # generate_api_reference() + with_api_reference() + cleanup -├── inventories/ # InterLinks fallback inventories (one per dependency) -└── src/ - ├── .vitepress/ # VitePress config (DocumenterVitepress only) - ├── index.md # landing page - ├── / # hand-written guides - └── api/ # auto-generated (removed after build) -``` - -## API generation pattern - -The standard pattern uses a shared `modules_config` list to avoid repeating file lists, -then generates public pages in a loop and one unified Internals page. - -```julia -# ── Shared config: one entry per submodule ──────────────────────────────────── -modules_config = [ - (mod=MyPackage.SubA, title="SubA", filename="suba", files=src( - "SubA/SubA.jl", "SubA/types.jl", "SubA/helpers.jl", - )), - (mod=MyPackage.SubB, title="SubB", filename="subb", files=src( - "SubB/SubB.jl", "SubB/core.jl", - )), -] - -EXCLUDE = Symbol[:include, :eval] - -# ── Public pages: one flat page per submodule ───────────────────────────────── -pages = [ - MyPackage.automatic_reference_documentation(; - subdirectory = "api", - primary_modules = [cfg.mod => cfg.files], - exclude = EXCLUDE, - public = true, - private = false, - title = cfg.title, - filename = cfg.filename, - ) - for cfg in modules_config -] - -# ── Internals: all private symbols in one page, sections by module ──────────── -internals_modules = Any[cfg.mod => cfg.files for cfg in modules_config] - -# Extensions are detected and added conditionally -for (sym, files) in [ - (:MyExt, ext("MyExt/MyExt.jl", "MyExt/helpers.jl")), -] - extmod = Base.get_extension(MyPackage, sym) - isnothing(extmod) || push!(internals_modules, extmod => files) -end - -push!(pages, MyPackage.automatic_reference_documentation(; - subdirectory = "api", - primary_modules = internals_modules, - external_modules_to_document = [MyPackage], - exclude = EXCLUDE, - public = false, - private = true, - title = "Internals", - filename = "internals", -)) -``` - -The resulting navigation is: - -```text -API Reference - SubA ← exported symbols only - SubB ← exported symbols only - Internals ← all private symbols, sections by module (including extensions) -``` - -Keep the per-submodule file lists in sync with `src/` — a missing file silently drops -docstrings and breaks `@ref` links. - -## Cross-reference infrastructure - -```julia -using DocumenterInterLinks -links = InterLinks( - "OtherPkg" => ("https://.../OtherPkg.jl/stable/", - "https://.../OtherPkg.jl/stable/objects.inv", - joinpath(@__DIR__, "inventories", "OtherPkg.toml")), -) -makedocs(; plugins=[links], ...) # makes [`OtherPkg.Sub.Sym`](@extref) resolve -``` - -## The draft switch (development workflow) - -`make.jl` exposes `draft`. In draft mode, `@example`/`@setup` Julia cells are **not -executed** — fast structural/link validation. Per-file override: - -````markdown -```@meta -Draft = false -``` -```` - -Workflow: build with `draft = true` first (fix all links fast), then flip one file at a -time to `Draft = false` to debug its examples, then `draft = false` globally for the -final full build. (Operational details in [`../RULES.md`](../RULES.md).) - -## Index page - -Mandatory pieces of `docs/src/index.md`: a `@meta CurrentModule` block, a one-paragraph -scope statement, info/note/warning admonitions (notably the qualified-access policy), a -module table, guide links via `[@ref]`, and a short Quick Start with qualified calls. - -## Checklist - -- [ ] `make.jl` uses `with_api_reference()`; `api_reference.jl` has generate/with/cleanup. -- [ ] One `automatic_reference_documentation` per submodule (`public=true, private=false`) + one unified Internals page (`public=false, private=true`). -- [ ] Extensions detected via `Base.get_extension` and documented when present. -- [ ] `InterLinks` set up and passed via `plugins=[links]` if `@extref` is used. -- [ ] Guides under `docs/src//`; no hand-written API pages. -- [ ] `index.md` has meta, scope, admonitions, module table, guide links, Quick Start. -- [ ] Built clean: draft (links) → per file → full. diff --git a/dev/philosophy/exceptions.md b/dev/philosophy/exceptions.md deleted file mode 100644 index 120fa837..00000000 --- a/dev/philosophy/exceptions.md +++ /dev/null @@ -1,104 +0,0 @@ -# Exceptions - -Structured, informative errors. The ecosystem provides seven exception types under -`CTException`, imported via `import CTBase.Exceptions`. - -## Principles - -1. **Clear message** — immediately understandable. -2. **Actionable suggestion** — how to fix it. -3. **Rich context** — what was expected, what was received, where. -4. **Typed** — pick the right exception so callers can catch precisely. - -## The seven types - -| Type | Key fields | Use when | -|---|---|---| -| `IncorrectArgument` | `msg, got, expected, suggestion, context` | a single argument's value is out of domain | -| `PreconditionError` | `msg, reason, suggestion, context` | arguments valid individually, but the call's relation/state/timing is forbidden | -| `NotImplemented` | `msg, required_method, suggestion, context` | interface stub a subtype must implement | -| `ParsingError` | `msg, location, suggestion` | DSL / syntax / structure error | -| `AmbiguousDescription` | `description, candidates, suggestion, context` | a symbol-tuple description matches no catalogue entry | -| `ExtensionError` | `weakdeps, feature, context` | an optional (weak) dependency is not loaded | -| `SolverFailure` | `msg, retcode, suggestion, context` | a numerical solver/integrator failed | - -## The choice rule - -> **`IncorrectArgument`** = "*this value* is wrong" (domain of **one** argument). -> **`PreconditionError`** = "*this combination / state / timing* is wrong" (a -> **relational** or contextual contract, each argument valid on its own). - -Operational heuristic: if the message must mention **two things** ("x relative to y", -"this after that"), it is almost always `PreconditionError`. If it mentions **one** -value ("n must be > 0"), it is `IncorrectArgument`. - -Note the fields: `IncorrectArgument` has `got`/`expected` (**no** `reason`); -`PreconditionError` has `reason`. Using the wrong field is a tell that the type is wrong. - -## Examples (generic) - -```julia -import CTBase.Exceptions - -# Single-argument domain violation -throw(Exceptions.IncorrectArgument( - "dimension must be positive"; - got = "n = -1", expected = "n > 0", - suggestion = "pass a positive integer for n", - context = "make_thing", -)) - -# Relational / state violation -throw(Exceptions.PreconditionError( - "cannot combine A with B"; - reason = "A carries x, B carries (x, p): the handoff is undefined", - suggestion = "combine objects of the same kind", - context = "composition", -)) - -# Interface stub on an abstract type -function run(s::AbstractStrategy, x) - throw(Exceptions.NotImplemented( - "run not implemented for $(typeof(s))"; - required_method = "run(::$(typeof(s)), x)", - suggestion = "define run for your strategy type", - context = "AbstractStrategy contract", - )) -end - -# Optional dependency missing (extension stub) -throw(Exceptions.ExtensionError(:SomeBackend; feature = "AD", context = "build")) - -# Numerical failure -throw(Exceptions.SolverFailure( - "integration failed"; retcode = ":Unstable", - suggestion = "reduce the step or check initial conditions", -)) -``` - -## Testing exceptions - -```julia -Test.@test_throws Exceptions.IncorrectArgument bad_call() - -err = try bad_call() catch e; e end -Test.@test err isa Exceptions.IncorrectArgument -Test.@test occursin("must be positive", err.msg) -``` - -## Anti-patterns - -```julia -error("something went wrong") # ❌ untyped, vague -throw(Exceptions.IncorrectArgument("bad")) # ❌ no got/expected/suggestion -throw(Exceptions.IncorrectArgument("already finalized")) # ❌ should be PreconditionError -throw(Exceptions.PreconditionError("n must be > 0")) # ❌ should be IncorrectArgument -``` - -## Checklist - -- [ ] Exception type matches the situation (single value vs relational vs contract vs …). -- [ ] Message clear and specific; `got`/`expected` set when applicable. -- [ ] Actionable `suggestion`; `context` for non-trivial errors. -- [ ] Fields valid for the chosen type (`reason` only on `PreconditionError`). -- [ ] Covered by a `@test_throws`. diff --git a/dev/philosophy/modules.md b/dev/philosophy/modules.md deleted file mode 100644 index 6c3822d4..00000000 --- a/dev/philosophy/modules.md +++ /dev/null @@ -1,155 +0,0 @@ -# Modules and qualification - -Submodule organization and import rules. Generic examples. - -## Principles - -1. **One submodule per responsibility**, in its own directory `src//.jl`. -2. **Package manifest exports nothing**: the top-level file only `include`s the - submodules and `using .Submodule`; it **exports nothing**. -3. **Each submodule exports its public API**; internal helpers stay unexported and are - reached via full qualification. -4. **Qualified external imports**: `using Pkg: Pkg` or `import Pkg.Sub`, never a bare - `using Pkg`. -5. **Qualification everywhere**: call `Sub.function` / `Sub.Type`, never rely on - implicit scope. -6. **One-way dependency flow (DAG)**: a low module never depends on a high one; no - cycles. - -## The submodule manifest - -Canonical structure of `src//.jl`: - -```julia -""" -Module docstring — role, responsibilities, dependencies. -""" -module Name - -# 1. External-package imports (qualified) -import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # macros: symbol import tolerated -import CTBase.Exceptions # qualifies Exceptions.* -using SomePackage: SomePackage # call SomePackage.* - -# 2. Sibling-submodule imports -using ..Lower -import ..Other as OtherAlias -using ..Core: AbstractTag # pervasive symbol only - -# 3. Includes (internal dependency order) -include(joinpath(@__DIR__, "abstract_types.jl")) -include(joinpath(@__DIR__, "concrete.jl")) - -# 4. Exports — public API only -export AbstractThing, Thing -export build_thing - -end # module Name -``` - -Order: docstring → `module` → external imports → sibling imports → `include`s → -`export`s → `end`. - -## Import styles (by preference) - -```julia -using SomePackage: SomePackage # ✅ preferred: only the name enters scope -import CTBase.Exceptions # ✅ qualifies the submodule -import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES # ✅ reserved for macros / pervasive symbols -``` - -Forbidden: - -```julia -using SomePackage # ❌ pollutes scope -using CTBase: AbstractModel, validate # ❌ opaque origin at call sites -``` - -## Sibling imports - -```julia -using ..Lower # call Lower.f(...) -import ..Core as CTCore # alias on conflict or for readability -using ..Core: AbstractTag # single pervasive symbol (often an abstract type) -``` - -## Qualification at call sites - -```julia -# ✅ explicit origin at every call -function Strategies.metadata(::Type{<:Modelers.ADNLP}) - return Strategies.StrategyMetadata( - Strategies.OptionDefinition(name=:backend, type=Symbol, default=:auto), - ) -end - -# ❌ where do StrategyMetadata, OptionDefinition come from? -function metadata(::Type{<:ADNLP}) - return StrategyMetadata(OptionDefinition(name=:backend, type=Symbol)) -end -``` - -Why: visible origin, safe refactors (only the `using ..X` line changes), no accidental -shadowing, same rule for external and sibling symbols. - -## Naming conventions - -Three visibility levels, signalled by the name itself: - -- `symbol` — **public**: exported via `export`; part of the documented API. -- `_symbol` — **private helper**: unexported; internal implementation detail. - Accessible via qualified path (`Module._symbol`) but not advertised. -- `__symbol` — **default value**: unexported; provides a replaceable semantic - default (e.g. `__display() = true`). The double underscore distinguishes it - from a plain helper: it is a *default* that a higher-level layer may override, - not merely a utility function. - -```julia -export build_thing # public - -function _validate(x) # private helper - ... -end - -__verbose()::Bool = false # default value (replaceable by extension or subtype) -``` - -## Two-level exports - -- **Submodule**: `export` for its public API; internals (`_helper`, `__default`) unexported. -- **Package (top-level)**: **no** `export`. Load submodules with `using .Submodule`; - users reach them via `Package.Submodule.sym`. - -```julia -module MyPackage -include(joinpath(@__DIR__, "Core", "Core.jl")); using .Core -include(joinpath(@__DIR__, "Systems", "Systems.jl")); using .Systems -# NO exports here. -end -``` - -User access: - -```julia -using MyPackage # brings no symbols into scope -MyPackage.Systems.AbstractThing # qualified (recommended) - -using MyPackage.Systems # opt-in: brings Systems exports into scope -AbstractThing # unqualified, the user's choice -``` - -## Dependency DAG - -The loading order in the top-level manifest follows a topological sort. A module may -`using ..Lower` only if `Lower` is already loaded. **No cycles**: if two modules need -each other, extract the shared concept into a lower module (`Core`). - -## Checklist - -- [ ] One submodule = one directory + one same-named manifest. -- [ ] Manifest = docstring, `module`, imports, `include`s, `export`s, `end` — nothing else. -- [ ] External imports: `using Pkg: Pkg` / `import Pkg.Sub` / (macros) `import Pkg: m`. -- [ ] Sibling imports: `using ..Sib` / `import ..Sib as A` / `using ..Sib: Sym`. -- [ ] Every external/sibling symbol is qualified at the call site. -- [ ] Acyclic DAG respected by the loading order. -- [ ] Each submodule exports its API; the package top-level exports nothing. diff --git a/dev/philosophy/testing.md b/dev/philosophy/testing.md deleted file mode 100644 index 235b6d2a..00000000 --- a/dev/philosophy/testing.md +++ /dev/null @@ -1,126 +0,0 @@ -# Testing - -How tests are structured and written. Generic examples. For *running* tests, see -[`../RULES.md`](../RULES.md). - -## Principles - -1. **Contract-first** — define and test contracts (interfaces) with fakes/stubs to - verify routing and behavior. Test public APIs and internal logic that matters. -2. **Orthogonality** — test organization follows *functionality*, not the `src/` layout. -3. **Isolation** — unit tests use fakes; integration tests exercise real boundaries. -4. **Determinism** — reproducible, no external state, no execution-order dependence. -5. **Clarity** — intent obvious from names and structure. - -## The file template (module + callable entry + qualified imports) - -This is mandatory. Each test file is a **module**; it imports everything **qualified**; -it defines a single entry function `test_()`; and it **redefines that function in -the outer scope** so the test runner can call it. - -```julia -# File: test/suite//test_.jl -module TestName - -# Qualified imports — bring module names into scope, call Sub.sym everywhere -import Test -import CTBase.Exceptions: Exceptions -import MyPackage.SubA: SubA -import MyPackage.SubB: SubB - -# Test options (verbose / timing), overridable by the runner -const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true -const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true - -# TOP-LEVEL fakes (never inside a function — world-age issues otherwise) -struct FakeThing <: SubA.AbstractThing end -SubA.required_method(::FakeThing) = 42 - -# Single entry function, named exactly like the file -function test_name() - Test.@testset "Name" verbose=VERBOSE showtiming=SHOWTIMING begin - Test.@test SubA.required_method(FakeThing()) == 42 - end -end - -end # module TestName - -# CRITICAL: redefine in the outer scope so the runner can call it -test_name() = TestName.test_name() -``` - -Why each part: -- **Module wrapper** — isolates names per file; avoids clashes across the suite. -- **Qualified imports** (`import X: X`) — the module name is in scope, call sites read - `SubA.sym`; same policy as the source (see [`modules.md`](modules.md)). -- **Top-level fakes** — defining a `struct` inside a function triggers world-age errors. -- **Outer redefinition** — the runner discovers and calls `test_name()` at top level. - -## Test categories - -- **Fake** — minimal struct implementing the required contract methods, to isolate the - unit under test. -- **Stub** — a default method on an abstract type that throws `NotImplemented` / - `ExtensionError`; tested by calling it on a fake type. -- **Mock** (interaction-recording) — not used; fakes suffice. - -### Unit - -Single component in isolation; pure, deterministic, fast (<1ms). Use fakes for -dependencies. - -### Integration - -Several real module boundaries together; fakes only for leaf dependencies. Slower -(up to ~1s) acceptable. - -### Contract - -Verify the interface using fakes that implement only the required methods. Check -routing, defaults, and Liskov substitution (generic code works for every subtype). - -### Error - -Verify stubs and error paths throw the right exception. - -- **Interface stub** (`NotImplemented`): a fake that omits a required method triggers - the abstract type's stub. Safe regardless of loaded extensions. -- **Extension stub** (`ExtensionError` / fallback): **always use a fake type** no - extension knows about — using a real type makes the test depend on whether another - test file loaded the extension. - -Separate categories with section comments inside one `@testset`. - -## Critical rules - -1. **Structs at module top-level** — never inside test functions. -2. **Qualified calls, omit the root for submodules** — `Test.@test SubA.f(x) isa - SubB.T` (not the fully-root-qualified, over-verbose form; not unqualified). -3. **Export verification** — assert submodule exports exist and that the package - top-level re-exports nothing it shouldn't. -4. **Test internal `_` functions** when logic is complex/branchy; otherwise cover them - indirectly. -5. **Independence** — fresh instances per testset; no shared mutable/global state; no - order dependence. - -## Quality - -- Specific assertions (`≈ … atol=…`, `isa T`, `== n`), not `> 0` or `!= nothing`. -- Names describe *what* is tested, not *how*. -- Type-stability tests with `@inferred` on **function calls** (not field access); - allocation tests with `@allocated` for hot paths. - -```julia -Test.@test_nowarn Test.@inferred SubA.accessor(obj) # ✅ a call -# Test.@inferred obj.field # ❌ field access -Test.@test (Test.@allocated SubA.accessor(obj)) == 0 -``` - -## Checklist - -- [ ] File is a module; entry `test_()`; redefined in the outer scope. -- [ ] All imports qualified; all calls qualified (root omitted for submodules). -- [ ] Fakes/stubs defined at module top-level. -- [ ] Categories present and separated (unit / integration / contract / error). -- [ ] Error paths use `@test_throws`; extension stubs use fake types. -- [ ] Each test independent and deterministic; specific assertions; descriptive names. diff --git a/dev/philosophy/types-traits-interfaces.md b/dev/philosophy/types-traits-interfaces.md deleted file mode 100644 index f078dca4..00000000 --- a/dev/philosophy/types-traits-interfaces.md +++ /dev/null @@ -1,166 +0,0 @@ -# Types, traits, interfaces - -How we model variation: abstract types for *nouns*, trait-parameters for *adjectives*, -dispatch via trait extraction. Generic examples. - -## The guiding rule - -> **One abstract type per real *noun*. One trait-parameter per orthogonal *axis*.** -> Concrete types are reserved for genuinely different data layouts. - -- **Abstract type** — good for a *noun* ("what is it"), `is-a` relations, shared - methods. But: single inheritance only → combinatorial explosion across several - orthogonal axes. -- **Trait (as a type parameter)** — good for an *adjective / capability*. Composes - freely, dispatched via an extractor. - -## Traits as type parameters - -A trait is a singleton type under a shared abstract trait. It is carried as a type -parameter and read back with an extractor. - -```julia -abstract type AbstractMode <: AbstractTrait end -struct Eager <: AbstractMode end -struct Lazy <: AbstractMode end - -# The trait is a parameter of the noun's abstract type -abstract type AbstractThing{M<:AbstractMode} end - -# Extractor: returns a type known at compile time -mode(::AbstractThing{M}) where {M} = M - -# Aliases give back the readable per-variant names for dispatch -const AbstractEagerThing = AbstractThing{Eager} -const AbstractLazyThing = AbstractThing{Lazy} -``` - -Adding an axis is adding a parameter, not multiplying the type tree. - -## When an axis becomes a type parameter (vs. stays an extracted trait) - -The criterion is not "is this axis important?" but: - -> **Does the axis change the contract visible to consumers of the abstract type?** -> If yes → type parameter. If no → extracted trait only. - -- An axis that changes the **method signatures** the abstract type promises (e.g. number - or role of arguments) is structural: it propagates up through the hierarchy and belongs - as a type parameter. -- An axis that only affects **how** a computation is performed *inside* a concrete type, - while both variants honour the same abstract contract, stays as an extracted trait on - the concrete type. - -A practical signal: **if an axis never propagates beyond one level of the hierarchy** — -present on concrete types but absent from abstract types and their consumers — it is most -likely an implementation detail, not a structural axis. - -## Dispatch via the trait (Holy trait pattern) - -Extract the trait, then re-dispatch on it: - -```julia -function process(x::AbstractThing) - return _process(mode(x), x) # extract, then re-dispatch -end - -_process(::Type{Eager}, x) = ... # eager branch -_process(::Type{Lazy}, x) = ... # lazy branch -``` - -This is **type-stable** when the trait is a type parameter: `mode(x)` is known at -compile time, so the re-dispatch resolves statically; the extra call is inlined. - -⚠️ Pitfall: extracting a trait from a *runtime value* (not encoded in the type) makes -the re-dispatch dynamic and breaks inference. Keep traits as type parameters. - -## Interfaces and contracts - -Define methods on abstract types; mark required ones with a `NotImplemented` stub so a -subtype that forgets them fails loudly. - -```julia -abstract type AbstractModel end - -"""Contract: every model implements `evaluate`.""" -function evaluate(m::AbstractModel, x) - throw(Exceptions.NotImplemented( - "evaluate not implemented"; - required_method = "evaluate(::$(typeof(m)), x)", - suggestion = "Define evaluate for your concrete model type", - context = "AbstractModel contract", - )) -end - -# Generic code works with any subtype (LSP) -function optimize(m::AbstractModel, x0) - v = evaluate(m, x0) # safe for any model honoring the contract - # ... -end -``` - -Rules: -- **Accept abstractions** in signatures (`build(::AbstractInput)`), not concrete types, - so users can extend without editing core code (OCP/DIP). -- **Subtypes honor the contract** (LSP): same return shape across the hierarchy; test - generic code against all subtypes. -- **Do not add an abstract type for a capability.** A capability (e.g. "supports AD") - is a *trait*, not a node in the hierarchy — adding `AbstractThingWithAD` collides with - the other axes under single inheritance. - -## When concrete types are justified - -Use a concrete type when the *data layout* genuinely differs (different fields), not to -encode an adjective. Two concrete types with identical fields that differ only by a flag -should be one parametric type with a trait parameter + aliases. - -## SOLID / DRY / KISS / YAGNI (condensed) - -- **SRP** — one responsibility per module/function/type. Red flags: names with "and", - functions > ~50 lines, branches handling unrelated concerns. -- **OCP** — extend via new subtypes + dispatch, not by editing `isa`/`typeof` chains. -- **LSP** — subtypes substitutable; consistent return types. -- **ISP** — small focused interfaces; don't force unused methods on a type. -- **DIP** — depend on abstract types; inject dependencies. -- **DRY** — one representation per piece of knowledge; extract shared validation. -- **KISS** — the simplest thing that works. -- **YAGNI** — no speculative fields/features. - -Anti-patterns to avoid: God object, primitive obsession (use domain types), feature -envy (put the method where the data is). - -## Type stability {#type-stability} - -Type-stable = return type inferable from input types at compile time. Typically -10–100× faster than unstable code. - -- **Parametric, concrete fields** — `struct C{T}; data::Vector{T}; end`, never - `Vector{Any}` or an abstract field type. -- **`NamedTuple` over `Dict`** for fixed-key, mixed-type records. -- **No `Any` in hot paths**; avoid conditional return types - (`Union{Int,Nothing}` from one function). -- **Function barriers** — isolate an unavoidable instability behind a typed inner - function. -- **`const Ref(...)`** instead of mutable globals. - -Verify: - -```julia -@testset "type stability" begin - @test_nowarn @inferred f(x) # @inferred only on function calls, not field access - @test (@allocated hot_path(x)) == 0 -end -``` - -Type stability matters on critical paths (inner loops, numerics); it is secondary on -one-time setup, user-facing API, and error paths. - -## Checklist - -- [ ] One abstract type per noun; one trait-parameter per orthogonal axis. -- [ ] Trait extractor + alias per variant; dispatch via extracted trait. -- [ ] Contracts are `NotImplemented` stubs on abstract types. -- [ ] Signatures accept abstractions; subtypes honor the contract. -- [ ] No abstract type encoding a capability (use a trait). -- [ ] Concrete types only for genuinely different data layouts. -- [ ] Parametric concrete fields; no `Any` in hot paths; `@inferred` on critical calls. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..64bea01e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# docs/ + +Documentation site for CTBase.jl, built with +[DocumenterVitepress](https://github.com/LuxDL/DocumenterVitepress.jl). + +Build (from the package root): + +```bash +julia --project=. docs/make.jl +``` + +Preview after build: `npx serve docs/build/1 --listen 5173` + +Documentation conventions and build workflow: +[control-toolbox Handbook](https://github.com/control-toolbox/Handbook). diff --git a/ext/README.md b/ext/README.md new file mode 100644 index 00000000..5b8883e0 --- /dev/null +++ b/ext/README.md @@ -0,0 +1,7 @@ +# ext/ + +Weak-dependency extensions for CTBase.jl. Each extension lives in its own +subdirectory with a manifest file and is loaded automatically by Julia when its +trigger package is available — none are loaded at package import time. + +Conventions: [control-toolbox Handbook](https://github.com/control-toolbox/Handbook). diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..7ab69e21 --- /dev/null +++ b/src/README.md @@ -0,0 +1,8 @@ +# src/ + +Source code of CTBase.jl. Organised as **one submodule per responsibility**: each +subdirectory contains a manifest file (`SubModule.jl`) and its implementation files. +The package top-level (`CTBase.jl`) exports **nothing** — all symbols are accessed via +qualified paths (`CTBase.SubModule.symbol`). + +Conventions: [control-toolbox Handbook](https://github.com/control-toolbox/Handbook). diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..801cfe4a --- /dev/null +++ b/test/README.md @@ -0,0 +1,9 @@ +# test/ + +Test suite for CTBase.jl. Tests live under `suite/` and are organised by +**functionality**, not by `src/` layout. Each test file follows the pattern +`test_.jl`: a module wrapper, a `test_()` entry function, and a +redefinition of that function in the outer scope so the runner can discover it. + +Testing conventions and execution rules: +[control-toolbox Handbook](https://github.com/control-toolbox/Handbook).