Skip to content

Commit 04fbb0d

Browse files
committed
Improve documentation and code quality
- Update guide documentation with clearer explanations and examples - Enhance strategy metadata and describe_registry implementations - Improve error display formatting in Exceptions - Update parameter contract helpers and coverage tests - Refine registry API for better introspection
1 parent 4fbfc4f commit 04fbb0d

13 files changed

Lines changed: 461 additions & 346 deletions

docs/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,22 @@ Preview after build: `npx serve docs/build/1 --listen 5173`
1313

1414
Documentation conventions and build workflow:
1515
[control-toolbox Handbook](https://github.com/control-toolbox/Handbook).
16+
17+
## `@repl` vs `@example` — ANSI color rule
18+
19+
DocumenterVitepress processes code block output differently depending on how it is produced:
20+
21+
| Block type | Output fence | ANSI codes | Result |
22+
| --- | --- | --- | --- |
23+
| `@example` | ` ```ansi ` | processed by Shiki | **colors render correctly** |
24+
| `@repl` | ` ```julia ` | parsed as Julia syntax | **raw escape sequences — ugly** |
25+
26+
**Rules:**
27+
28+
- Use **`@example`** when the output comes from a `show` method that uses ANSI colors
29+
(strategy instances, `StrategyOptions`, `StrategyMetadata`, `StrategyRegistry`, …).
30+
- Use **`@repl`** only when the output is a plain scalar value with no custom colored `show`
31+
(integers, booleans, symbols, plain strings, tuples of symbols, types).
32+
- Use **`@repl` + `try/catch # hide` + `showerror(IOContext(stdout, :color => false), e) # hide`**
33+
to display exceptions — the `showerror` call produces plain text that renders cleanly inside a
34+
`julia`-fenced block.

docs/src/guide/implementing-a-strategy.md

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
# Implementing a Strategy
2+
13
```@meta
24
CurrentModule = CTBase
35
```
46

5-
# Implementing a Strategy
6-
77
This guide walks you through implementing a complete strategy family using the `AbstractStrategy` contract. We use **Collocation** and **DirectShooting** discretizers as concrete examples.
88

99
!!! tip "Prerequisites"
@@ -21,7 +21,7 @@ Every strategy implements a **two-level contract** that separates static metadat
2121

2222
```text
2323
Type-Level (no instantiation needed)
24-
├─ id(::Type{<:S}) → Symbol (routing, registry lookup)
24+
├─ id(::Type{<:S}) → Symbol (routing, registry lookup)
2525
└─ metadata(::Type{<:S}) → StrategyMetadata (option specs + validation rules)
2626
2727
▼ routing, validation
@@ -48,6 +48,7 @@ nothing # hide
4848
```
4949

5050
This type enables:
51+
5152
- Grouping discretizers in a `StrategyRegistry` by family
5253
- Dispatching on the family in option routing
5354
- Adding methods common to all discretizers
@@ -56,7 +57,7 @@ This type enables:
5657

5758
### Step 1 — Define the struct
5859

59-
A strategy struct needs exactly one field: `options::Strategies.StrategyOptions`.
60+
A strategy struct needs a field: `options::Strategies.StrategyOptions`.
6061

6162
```@example strategy
6263
struct Collocation <: AbstractOptimalControlDiscretizer
@@ -110,7 +111,7 @@ nothing # hide
110111

111112
Let's verify the metadata:
112113

113-
```@repl strategy
114+
```@example strategy
114115
Strategies.metadata(Collocation)
115116
```
116117

@@ -137,30 +138,29 @@ nothing # hide
137138

138139
Now let's create instances and inspect them:
139140

140-
```@repl strategy
141+
```@example strategy
141142
c = Collocation()
142143
```
143144

144-
```@repl strategy
145+
```@example strategy
145146
c = Collocation(grid_size = 500, scheme = :trapeze)
146147
```
147148

148-
### Step 7 — Verify the contract
149-
150-
Use `validate_strategy_contract` to check that all contract methods are correctly implemented:
151-
152-
```@repl strategy
153-
Strategies.validate_strategy_contract(Collocation)
149+
```@example strategy
150+
describe(Collocation)
154151
```
155152

156-
### Step 8 — Access options
153+
### Step 7 — Access options
157154

158155
The `StrategyOptions` object tracks both values and their provenance. You can access options in two ways:
159156

160157
**Via the `options` getter:**
161158

162-
```@repl strategy
159+
```@example strategy
163160
c = Collocation(grid_size = 100)
161+
```
162+
163+
```@example strategy
164164
Strategies.options(c)
165165
```
166166

@@ -238,23 +238,19 @@ nothing # hide
238238
!!! note "Same option name, different definitions"
239239
Both `Collocation` and `DirectShooting` define a `:grid_size` option, but with different defaults (250 vs 100) and descriptions. Each strategy has its own independent `OptionDefinition` set.
240240

241-
```@repl strategy
242-
Strategies.validate_strategy_contract(DirectShooting)
243-
```
244-
245-
```@repl strategy
241+
```@example strategy
246242
DirectShooting()
247243
```
248244

249-
```@repl strategy
245+
```@example strategy
250246
DirectShooting(grid_size = 50)
251247
```
252248

253249
## Registering the Family
254250

255251
A `StrategyRegistry` maps abstract family types to their concrete strategies. This enables lookup by symbol and automated construction.
256252

257-
```@repl strategy
253+
```@example strategy
258254
registry = Strategies.create_registry(
259255
AbstractOptimalControlDiscretizer => (Collocation, DirectShooting),
260256
)
@@ -272,11 +268,11 @@ Strategies.type_from_id(:collocation, AbstractOptimalControlDiscretizer, registr
272268

273269
Build a strategy from the registry:
274270

275-
```@repl strategy
271+
```@example strategy
276272
Strategies.build_strategy(:collocation, AbstractOptimalControlDiscretizer, registry; grid_size = 300)
277273
```
278274

279-
```@repl strategy
275+
```@example strategy
280276
Strategies.build_strategy(:direct_shooting, AbstractOptimalControlDiscretizer, registry; grid_size = 50)
281277
```
282278

@@ -291,11 +287,9 @@ Strategies.extract_id_from_method(method, AbstractOptimalControlDiscretizer, reg
291287

292288
Build a strategy directly from a method tuple:
293289

294-
```@repl strategy
295-
Strategies.build_strategy_from_method(
296-
method, AbstractOptimalControlDiscretizer, registry;
297-
grid_size = 500, scheme = :trapeze,
298-
)
290+
```@example strategy
291+
id = Strategies.extract_id_from_method(method, AbstractOptimalControlDiscretizer, registry)
292+
Strategies.build_strategy(id, AbstractOptimalControlDiscretizer, registry; grid_size = 500, scheme = :trapeze)
299293
```
300294

301295
See [Orchestration & Routing](@ref) for the full multi-strategy routing system.
@@ -334,12 +328,50 @@ Strategies.option_description(Collocation, :grid_size)
334328

335329
Use `mode = :permissive` to accept backend-specific options that are not declared in the metadata:
336330

337-
```@repl strategy
331+
```@example strategy
338332
Collocation(grid_size = 500, custom_backend_param = 42; mode = :permissive)
339333
```
340334

341335
Unknown options are stored with `:user` source but bypass type validation. Known options are still fully validated.
342336

337+
### Bypass Validation for Specific Options
338+
339+
Use `bypass(val)` (or its alias `force(val)`) to skip validation for a **single option value** while keeping strict mode for everything else.
340+
341+
**Unknown option** — accepted silently, no warning:
342+
343+
```@example strategy
344+
Collocation(grid_size = 500, custom_backend_param = bypass(42))
345+
```
346+
347+
**Known option with wrong type** — normally rejected, accepted with `bypass`:
348+
349+
```@repl strategy
350+
try # hide
351+
Collocation(grid_size = "oops") # type error: grid_size expects Int
352+
catch e; showerror(IOContext(stdout, :color => false), e) end # hide
353+
```
354+
355+
```@example strategy
356+
Collocation(grid_size = bypass("oops")) # no error: validation skipped
357+
```
358+
359+
This is more surgical than `mode = :permissive`:
360+
361+
| Approach | Scope | Unknown option names | Type validation |
362+
|--------------------------------|-------------|-----------------------|-----------------------------|
363+
| `mode = :permissive` | all options | accepted with warning | skipped for unknowns |
364+
| `bypass(val)` / `force(val)` | one value | accepted silently | skipped for that value only |
365+
366+
`force` is an alias for `bypass` — choose the name that fits your mental model:
367+
368+
```julia
369+
Collocation(grid_size = force("oops")) # same as bypass("oops")
370+
```
371+
372+
!!! warning "Use with care"
373+
Bypassed values are not type-checked, even for declared options. A wrong type or invalid value will only surface as a backend-level error.
374+
343375
### Option Aliases
344376

345377
An `OptionDefinition` can declare aliases — alternative names that resolve to the primary name:

docs/src/guide/options-system.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ println("Value: ", opt.value)
179179
println("Source: ", opt.source)
180180
```
181181

182-
## Accessing Option Properties (Getters)
182+
## Accessing Option Properties
183183

184184
Use the getters in `Options` to access `OptionDefinition` and `OptionValue` fields instead of reading struct fields directly. This keeps encapsulation intact and aligns with Strategies overrides.
185185

@@ -210,7 +210,7 @@ Options.is_default(opt2)
210210
Options.is_computed(opt2)
211211
```
212212

213-
## StrategyMetadata Overview (Strategies)
213+
## StrategyMetadata Overview
214214

215215
`StrategyMetadata` is a collection of `OptionDefinition` objects that describes all configurable options for a strategy. It is returned by `Strategies.metadata(::Type)`.
216216

0 commit comments

Comments
 (0)