skill-systems-thinking 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -25
- package/LICENSE +21 -21
- package/README.md +482 -396
- package/SKILL.md +125 -125
- package/assets/runtime-prompt.txt +21 -21
- package/evals/checks.json +88 -88
- package/init.ps1 +68 -0
- package/package.json +11 -4
- package/references/bounded-control.md +82 -82
- package/references/closed-loop-workflow.md +89 -89
- package/references/discrete-systems.md +79 -79
- package/references/disturbance.md +98 -98
- package/references/modeling.md +87 -87
- package/references/multivariable.md +112 -112
- package/references/original-text.md +58 -58
- package/references/stability.md +97 -97
- package/references/state-and-control.md +84 -84
- package/templates/change-proposal.md +51 -51
- package/templates/debugging-checklist.md +44 -44
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
# Reference: Time-Optimal & Bounded Control
|
|
2
|
-
|
|
3
|
-
Book anchors: §8 (time-optimal / bang-bang control), §9.6 (optimal control under control
|
|
4
|
-
constraints).
|
|
5
|
-
|
|
6
|
-
## Control effort is bounded — always
|
|
7
|
-
|
|
8
|
-
Real actuators saturate: a thruster cannot exceed max thrust, a valve cannot open past
|
|
9
|
-
fully-open. Software analogues are everywhere and just as hard:
|
|
10
|
-
|
|
11
|
-
- retry budget, recursion depth, loop iterations, allocation size, request rate, context
|
|
12
|
-
window, transaction timeout.
|
|
13
|
-
|
|
14
|
-
Linear theory ignores these bounds and is wrong at the edge (§1.5). **Unbounded control →
|
|
15
|
-
actuator saturation → instability** (e.g. retry storm, stack overflow, OOM, rate-limit ban).
|
|
16
|
-
|
|
17
|
-
## LLM behavior pattern: Clamp every actuator
|
|
18
|
-
|
|
19
|
-
> "Every loop must have an explicit bound. No unbounded retry, recursion, or allocation."
|
|
20
|
-
|
|
21
|
-
The LLM must proactively identify actuators in its proposed changes and clamp them:
|
|
22
|
-
- Retry: max attempts, backoff, jitter, circuit breaker
|
|
23
|
-
- Recursion: depth limit
|
|
24
|
-
- Loop: iteration cap or progress guarantee
|
|
25
|
-
- Allocation: size cap
|
|
26
|
-
- Rate: throughput limit with graceful degradation at saturation
|
|
27
|
-
|
|
28
|
-
## Bang-bang where appropriate (§8)
|
|
29
|
-
|
|
30
|
-
To reach a target *fastest* under a bound, the optimal law is often **bang-bang**: drive the
|
|
31
|
-
actuator to its limit in the right direction, then switch. Translated to code:
|
|
32
|
-
|
|
33
|
-
- Make **decisive, maximal, well-scoped** changes to kill a fault — don't nibble at it with
|
|
34
|
-
timid partial fixes that leave the system near the unstable region.
|
|
35
|
-
- Switch decisively when the sign of the error flips (e.g. change strategy, not just magnitude).
|
|
36
|
-
|
|
37
|
-
But note (§9.6): bang-bang is optimal for *time*; for *energy/integral cost* (§9) a softer
|
|
38
|
-
law is better. Pick the criterion to the goal.
|
|
39
|
-
|
|
40
|
-
## Clamp the actuator
|
|
41
|
-
|
|
42
|
-
- Bound every control loop: max retries, max recursion, max batch, max rate. Saturation
|
|
43
|
-
without a clamp is how a bounded disturbance becomes a runaway.
|
|
44
|
-
- When you hit the bound, *degrade gracefully* (drop to a safe mode, shed load, return a
|
|
45
|
-
partial result) rather than oscillating at the limit.
|
|
46
|
-
|
|
47
|
-
## Optimize the integral, not just the endpoint (§9)
|
|
48
|
-
|
|
49
|
-
Often you minimize a **cumulative cost** — total latency, total tokens spent, total energy,
|
|
50
|
-
total user-visible glitches — not just final state. Choose the change that minimizes the
|
|
51
|
-
integral of cost over the whole trajectory, not the one that looks tidiest at the endpoint.
|
|
52
|
-
|
|
53
|
-
## Generality notes
|
|
54
|
-
|
|
55
|
-
These ideas apply identically whether the "plant" is a web service, a data pipeline, a
|
|
56
|
-
compiler pass, or an LLM agent loop: every one has bounded actuators and a cost to minimize
|
|
57
|
-
over time. The physical vocabulary (thrust, valve) is illustrative only — the invariant is
|
|
58
|
-
*bounded, goal-directed control*.
|
|
59
|
-
|
|
60
|
-
## Worked example: uncontrolled recursion → stack overflow
|
|
61
|
-
|
|
62
|
-
**Before (no bound on actuator):**
|
|
63
|
-
```python
|
|
64
|
-
def walk(node):
|
|
65
|
-
for child in node.children:
|
|
66
|
-
walk(child) # actuator: recursion depth — unbounded!
|
|
67
|
-
```
|
|
68
|
-
Circular reference in `node.children` → unbounded recursion → `RecursionError` /
|
|
69
|
-
stack overflow. The actuator (call stack) has a hard saturation, but the code ignores it.
|
|
70
|
-
|
|
71
|
-
**After (clamped actuator):**
|
|
72
|
-
```python
|
|
73
|
-
def walk(node, _depth=0, max_depth=100):
|
|
74
|
-
if _depth >= max_depth:
|
|
75
|
-
raise ValueError(f"Actuator saturated: depth {_depth} >= {max_depth}")
|
|
76
|
-
for child in node.children:
|
|
77
|
-
walk(child, _depth + 1, max_depth)
|
|
78
|
-
```
|
|
79
|
-
- Bound: `max_depth=100` (explicit actuator clamp).
|
|
80
|
-
- Graceful degradation: exception at bound instead of silent crash.
|
|
81
|
-
- Cost: O(n) with bounded cost per call; integral cost stays predictable.
|
|
82
|
-
|
|
1
|
+
# Reference: Time-Optimal & Bounded Control
|
|
2
|
+
|
|
3
|
+
Book anchors: §8 (time-optimal / bang-bang control), §9.6 (optimal control under control
|
|
4
|
+
constraints).
|
|
5
|
+
|
|
6
|
+
## Control effort is bounded — always
|
|
7
|
+
|
|
8
|
+
Real actuators saturate: a thruster cannot exceed max thrust, a valve cannot open past
|
|
9
|
+
fully-open. Software analogues are everywhere and just as hard:
|
|
10
|
+
|
|
11
|
+
- retry budget, recursion depth, loop iterations, allocation size, request rate, context
|
|
12
|
+
window, transaction timeout.
|
|
13
|
+
|
|
14
|
+
Linear theory ignores these bounds and is wrong at the edge (§1.5). **Unbounded control →
|
|
15
|
+
actuator saturation → instability** (e.g. retry storm, stack overflow, OOM, rate-limit ban).
|
|
16
|
+
|
|
17
|
+
## LLM behavior pattern: Clamp every actuator
|
|
18
|
+
|
|
19
|
+
> "Every loop must have an explicit bound. No unbounded retry, recursion, or allocation."
|
|
20
|
+
|
|
21
|
+
The LLM must proactively identify actuators in its proposed changes and clamp them:
|
|
22
|
+
- Retry: max attempts, backoff, jitter, circuit breaker
|
|
23
|
+
- Recursion: depth limit
|
|
24
|
+
- Loop: iteration cap or progress guarantee
|
|
25
|
+
- Allocation: size cap
|
|
26
|
+
- Rate: throughput limit with graceful degradation at saturation
|
|
27
|
+
|
|
28
|
+
## Bang-bang where appropriate (§8)
|
|
29
|
+
|
|
30
|
+
To reach a target *fastest* under a bound, the optimal law is often **bang-bang**: drive the
|
|
31
|
+
actuator to its limit in the right direction, then switch. Translated to code:
|
|
32
|
+
|
|
33
|
+
- Make **decisive, maximal, well-scoped** changes to kill a fault — don't nibble at it with
|
|
34
|
+
timid partial fixes that leave the system near the unstable region.
|
|
35
|
+
- Switch decisively when the sign of the error flips (e.g. change strategy, not just magnitude).
|
|
36
|
+
|
|
37
|
+
But note (§9.6): bang-bang is optimal for *time*; for *energy/integral cost* (§9) a softer
|
|
38
|
+
law is better. Pick the criterion to the goal.
|
|
39
|
+
|
|
40
|
+
## Clamp the actuator
|
|
41
|
+
|
|
42
|
+
- Bound every control loop: max retries, max recursion, max batch, max rate. Saturation
|
|
43
|
+
without a clamp is how a bounded disturbance becomes a runaway.
|
|
44
|
+
- When you hit the bound, *degrade gracefully* (drop to a safe mode, shed load, return a
|
|
45
|
+
partial result) rather than oscillating at the limit.
|
|
46
|
+
|
|
47
|
+
## Optimize the integral, not just the endpoint (§9)
|
|
48
|
+
|
|
49
|
+
Often you minimize a **cumulative cost** — total latency, total tokens spent, total energy,
|
|
50
|
+
total user-visible glitches — not just final state. Choose the change that minimizes the
|
|
51
|
+
integral of cost over the whole trajectory, not the one that looks tidiest at the endpoint.
|
|
52
|
+
|
|
53
|
+
## Generality notes
|
|
54
|
+
|
|
55
|
+
These ideas apply identically whether the "plant" is a web service, a data pipeline, a
|
|
56
|
+
compiler pass, or an LLM agent loop: every one has bounded actuators and a cost to minimize
|
|
57
|
+
over time. The physical vocabulary (thrust, valve) is illustrative only — the invariant is
|
|
58
|
+
*bounded, goal-directed control*.
|
|
59
|
+
|
|
60
|
+
## Worked example: uncontrolled recursion → stack overflow
|
|
61
|
+
|
|
62
|
+
**Before (no bound on actuator):**
|
|
63
|
+
```python
|
|
64
|
+
def walk(node):
|
|
65
|
+
for child in node.children:
|
|
66
|
+
walk(child) # actuator: recursion depth — unbounded!
|
|
67
|
+
```
|
|
68
|
+
Circular reference in `node.children` → unbounded recursion → `RecursionError` /
|
|
69
|
+
stack overflow. The actuator (call stack) has a hard saturation, but the code ignores it.
|
|
70
|
+
|
|
71
|
+
**After (clamped actuator):**
|
|
72
|
+
```python
|
|
73
|
+
def walk(node, _depth=0, max_depth=100):
|
|
74
|
+
if _depth >= max_depth:
|
|
75
|
+
raise ValueError(f"Actuator saturated: depth {_depth} >= {max_depth}")
|
|
76
|
+
for child in node.children:
|
|
77
|
+
walk(child, _depth + 1, max_depth)
|
|
78
|
+
```
|
|
79
|
+
- Bound: `max_depth=100` (explicit actuator clamp).
|
|
80
|
+
- Graceful degradation: exception at bound instead of silent crash.
|
|
81
|
+
- Cost: O(n) with bounded cost per call; integral cost stays predictable.
|
|
82
|
+
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
# Reference: Closed-Loop Workflow & Analysis-before-Synthesis
|
|
2
|
-
|
|
3
|
-
Book anchors: §1.5 (analysis vs synthesis), §3.7 (feedback), §4 (control-system analysis).
|
|
4
|
-
|
|
5
|
-
## Why closed loop
|
|
6
|
-
|
|
7
|
-
An open-loop program — "write it once, hope it works" — has no path to recover from error. A
|
|
8
|
-
closed-loop program *acts → observes → corrects*, and therefore converges (§3.7: feedback
|
|
9
|
-
raises both accuracy and speed of response).
|
|
10
|
-
|
|
11
|
-
Operational rules:
|
|
12
|
-
- After every non-trivial change, **observe the actual state** before declaring success:
|
|
13
|
-
run it, read output, check tests. Do not assume `u` produced the intended `x`.
|
|
14
|
-
- Prefer mechanisms that **measure** outcomes over mechanisms that **assert** they happened.
|
|
15
|
-
- If you cannot observe a quantity, you cannot control it (§1.5): a controlled quantity is
|
|
16
|
-
steered only through a control quantity you are free to change.
|
|
17
|
-
|
|
18
|
-
## Analysis before synthesis (§1.5, §4)
|
|
19
|
-
|
|
20
|
-
> Analysis is understanding the motion of a given system; synthesis is changing that motion to
|
|
21
|
-
> meet a need. Analysis is the foundation; synthesis is the goal — and the higher stage.
|
|
22
|
-
|
|
23
|
-
- **Never skip analysis.** Before fixing or refactoring, characterize current behavior:
|
|
24
|
-
reproduce, measure, locate the fault. You cannot steer what you have not measured.
|
|
25
|
-
- Then synthesize the *minimal* control action that moves state to the target.
|
|
26
|
-
- A change whose effect you cannot predict from analysis is an open-loop guess. Make the
|
|
27
|
-
prediction explicit, then verify it.
|
|
28
|
-
|
|
29
|
-
## The loop, made concrete
|
|
30
|
-
|
|
31
|
-
| Step | Control term | What you actually do |
|
|
32
|
-
|------|--------------|----------------------|
|
|
33
|
-
| Identify | define `x`, `u`, sensors, bounds | Write down what must be correct and what lever you'll pull |
|
|
34
|
-
| Analyze | measure current trajectory | Reproduce the bug / profile the baseline |
|
|
35
|
-
| Stabilize | drive poles to stable region | Stop the divergence/leak/oscillation |
|
|
36
|
-
| Synthesize | choose control law `u = f(ε)` | Make the smallest change that moves `x` to target |
|
|
37
|
-
| Observe | read sensors | Run tests, read logs, check metrics |
|
|
38
|
-
| Correct | update `u` from error `ε = x* − x` | Iterate until `‖ε‖` is bounded and small |
|
|
39
|
-
| Compensate | feedforward + clamp | Pre-handle measurable disturbances; bound the actuator |
|
|
40
|
-
| Optimize | secondary criteria | Only now tune speed/cost/elegance |
|
|
41
|
-
|
|
42
|
-
## How to know the loop converged
|
|
43
|
-
|
|
44
|
-
- Deviation `ε` is **bounded** for the largest perturbation you can throw at it (stability, §4.1).
|
|
45
|
-
- Corrections get **smaller**, not larger, over iterations (no limit cycle, no retry storm).
|
|
46
|
-
- The setpoint is reached within the stated bounds (memory, time, rate limit).
|
|
47
|
-
|
|
48
|
-
## Worked example: analysis before synthesis — cache stampede
|
|
49
|
-
|
|
50
|
-
**Symptom:** under sudden traffic spike, database CPU spikes to 100%, API returns 503.
|
|
51
|
-
|
|
52
|
-
**Open-loop (skip analysis):**
|
|
53
|
-
```python
|
|
54
|
-
@app.get("/data/{id}")
|
|
55
|
-
def get_data(id):
|
|
56
|
-
data = cache.get(id) or db.query(f"SELECT * FROM t WHERE id={id}")
|
|
57
|
-
return data
|
|
58
|
-
```
|
|
59
|
-
Naive fix (synthesis without analysis): "just add a cache"
|
|
60
|
-
|
|
61
|
-
**Closed-loop (analysis first):**
|
|
62
|
-
```
|
|
63
|
-
Analyze:
|
|
64
|
-
x = db CPU utilization; target < 70%
|
|
65
|
-
u = cache TTL + eviction policy
|
|
66
|
-
Model: thundering-herd on cache miss → spike proportional to missing-key rate
|
|
67
|
-
Validity: model holds for any cache with TTL; breaks for in-process unbounded cache
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**Stabilize first:**
|
|
71
|
-
```python
|
|
72
|
-
@app.get("/data/{id}")
|
|
73
|
-
def get_data(id):
|
|
74
|
-
data = cache.get(id)
|
|
75
|
-
if data is None:
|
|
76
|
-
with lock: # serialize misses per key
|
|
77
|
-
data = cache.get(id) or db.query(...)
|
|
78
|
-
cache.set(id, data, ttl=60)
|
|
79
|
-
return data
|
|
80
|
-
```
|
|
81
|
-
- Lock prevents thundering herd (damps the oscillation).
|
|
82
|
-
- TTL bounds memory.
|
|
83
|
-
|
|
84
|
-
**Then optimize:**
|
|
85
|
-
- Only after CPU is bounded <70%, consider increasing TTL or adding tiered caching.
|
|
86
|
-
|
|
87
|
-
Key point: adding a cache *without* the lock in the first patch would have shifted but not
|
|
88
|
-
eliminated the oscillation. Analysis → stabilize → then synthesize = closed loop.
|
|
89
|
-
|
|
1
|
+
# Reference: Closed-Loop Workflow & Analysis-before-Synthesis
|
|
2
|
+
|
|
3
|
+
Book anchors: §1.5 (analysis vs synthesis), §3.7 (feedback), §4 (control-system analysis).
|
|
4
|
+
|
|
5
|
+
## Why closed loop
|
|
6
|
+
|
|
7
|
+
An open-loop program — "write it once, hope it works" — has no path to recover from error. A
|
|
8
|
+
closed-loop program *acts → observes → corrects*, and therefore converges (§3.7: feedback
|
|
9
|
+
raises both accuracy and speed of response).
|
|
10
|
+
|
|
11
|
+
Operational rules:
|
|
12
|
+
- After every non-trivial change, **observe the actual state** before declaring success:
|
|
13
|
+
run it, read output, check tests. Do not assume `u` produced the intended `x`.
|
|
14
|
+
- Prefer mechanisms that **measure** outcomes over mechanisms that **assert** they happened.
|
|
15
|
+
- If you cannot observe a quantity, you cannot control it (§1.5): a controlled quantity is
|
|
16
|
+
steered only through a control quantity you are free to change.
|
|
17
|
+
|
|
18
|
+
## Analysis before synthesis (§1.5, §4)
|
|
19
|
+
|
|
20
|
+
> Analysis is understanding the motion of a given system; synthesis is changing that motion to
|
|
21
|
+
> meet a need. Analysis is the foundation; synthesis is the goal — and the higher stage.
|
|
22
|
+
|
|
23
|
+
- **Never skip analysis.** Before fixing or refactoring, characterize current behavior:
|
|
24
|
+
reproduce, measure, locate the fault. You cannot steer what you have not measured.
|
|
25
|
+
- Then synthesize the *minimal* control action that moves state to the target.
|
|
26
|
+
- A change whose effect you cannot predict from analysis is an open-loop guess. Make the
|
|
27
|
+
prediction explicit, then verify it.
|
|
28
|
+
|
|
29
|
+
## The loop, made concrete
|
|
30
|
+
|
|
31
|
+
| Step | Control term | What you actually do |
|
|
32
|
+
|------|--------------|----------------------|
|
|
33
|
+
| Identify | define `x`, `u`, sensors, bounds | Write down what must be correct and what lever you'll pull |
|
|
34
|
+
| Analyze | measure current trajectory | Reproduce the bug / profile the baseline |
|
|
35
|
+
| Stabilize | drive poles to stable region | Stop the divergence/leak/oscillation |
|
|
36
|
+
| Synthesize | choose control law `u = f(ε)` | Make the smallest change that moves `x` to target |
|
|
37
|
+
| Observe | read sensors | Run tests, read logs, check metrics |
|
|
38
|
+
| Correct | update `u` from error `ε = x* − x` | Iterate until `‖ε‖` is bounded and small |
|
|
39
|
+
| Compensate | feedforward + clamp | Pre-handle measurable disturbances; bound the actuator |
|
|
40
|
+
| Optimize | secondary criteria | Only now tune speed/cost/elegance |
|
|
41
|
+
|
|
42
|
+
## How to know the loop converged
|
|
43
|
+
|
|
44
|
+
- Deviation `ε` is **bounded** for the largest perturbation you can throw at it (stability, §4.1).
|
|
45
|
+
- Corrections get **smaller**, not larger, over iterations (no limit cycle, no retry storm).
|
|
46
|
+
- The setpoint is reached within the stated bounds (memory, time, rate limit).
|
|
47
|
+
|
|
48
|
+
## Worked example: analysis before synthesis — cache stampede
|
|
49
|
+
|
|
50
|
+
**Symptom:** under sudden traffic spike, database CPU spikes to 100%, API returns 503.
|
|
51
|
+
|
|
52
|
+
**Open-loop (skip analysis):**
|
|
53
|
+
```python
|
|
54
|
+
@app.get("/data/{id}")
|
|
55
|
+
def get_data(id):
|
|
56
|
+
data = cache.get(id) or db.query(f"SELECT * FROM t WHERE id={id}")
|
|
57
|
+
return data
|
|
58
|
+
```
|
|
59
|
+
Naive fix (synthesis without analysis): "just add a cache"
|
|
60
|
+
|
|
61
|
+
**Closed-loop (analysis first):**
|
|
62
|
+
```
|
|
63
|
+
Analyze:
|
|
64
|
+
x = db CPU utilization; target < 70%
|
|
65
|
+
u = cache TTL + eviction policy
|
|
66
|
+
Model: thundering-herd on cache miss → spike proportional to missing-key rate
|
|
67
|
+
Validity: model holds for any cache with TTL; breaks for in-process unbounded cache
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Stabilize first:**
|
|
71
|
+
```python
|
|
72
|
+
@app.get("/data/{id}")
|
|
73
|
+
def get_data(id):
|
|
74
|
+
data = cache.get(id)
|
|
75
|
+
if data is None:
|
|
76
|
+
with lock: # serialize misses per key
|
|
77
|
+
data = cache.get(id) or db.query(...)
|
|
78
|
+
cache.set(id, data, ttl=60)
|
|
79
|
+
return data
|
|
80
|
+
```
|
|
81
|
+
- Lock prevents thundering herd (damps the oscillation).
|
|
82
|
+
- TTL bounds memory.
|
|
83
|
+
|
|
84
|
+
**Then optimize:**
|
|
85
|
+
- Only after CPU is bounded <70%, consider increasing TTL or adding tiered caching.
|
|
86
|
+
|
|
87
|
+
Key point: adding a cache *without* the lock in the first patch would have shifted but not
|
|
88
|
+
eliminated the oscillation. Analysis → stabilize → then synthesize = closed loop.
|
|
89
|
+
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
# Reference: Discrete / Sampled Systems & Test Cadence
|
|
2
|
-
|
|
3
|
-
Book anchor: §10 (discrete control systems — difference equations, sampled data).
|
|
4
|
-
|
|
5
|
-
## Software runs in discrete steps
|
|
6
|
-
|
|
7
|
-
Continuous control theory assumes you can act and observe at every instant. Software acts and
|
|
8
|
-
observes only at **sample instants**: function calls, test runs, deploys, metric scrapes,
|
|
9
|
-
human reviews. Between samples the state is *unobserved*.
|
|
10
|
-
|
|
11
|
-
Consequences:
|
|
12
|
-
- **Sample often enough.** A feedback loop sampled too coarsely (tests only at the end,
|
|
13
|
-
manual checks weeks apart) cannot stabilize a fast-moving fault. Tighten the loop:
|
|
14
|
-
unit → integration → typecheck → run, frequently and automatically.
|
|
15
|
-
- **Guard the gap.** Between samples, anything can happen: timeout, partial write, concurrent
|
|
16
|
-
mutation, crash. Design for the unobserved interval — idempotency, transactions, rollback,
|
|
17
|
-
heartbeat/lease.
|
|
18
|
-
- **Aliasing instability.** A bug that appears only at certain intervals or scales is a
|
|
19
|
-
*sampled-system alias* — the true dynamics are faster than your sampling. Vary the
|
|
20
|
-
sample/scale when testing (small vs large input, fast vs slow clock).
|
|
21
|
-
|
|
22
|
-
## LLM behavior pattern: Treat cadence as a controlled variable
|
|
23
|
-
|
|
24
|
-
> "Your feedback cadence is a design variable. Tighten it when you need stability; loosen it only when the system is already robust."
|
|
25
|
-
|
|
26
|
-
The LLM should recognize that its own test/review cadence is a *sampling rate* that affects
|
|
27
|
-
stability margin. It should not propose changes that rely on coarse-grained verification for
|
|
28
|
-
systems with fast dynamics.
|
|
29
|
-
|
|
30
|
-
- Tight cadence (fast tests, CI on every commit) → can use higher gain (more aggressive
|
|
31
|
-
fixes) and still stay stable.
|
|
32
|
-
- Loose cadence (nightly, manual) → must use lower gain (conservative, well-reviewed changes)
|
|
33
|
-
or it will overshoot.
|
|
34
|
-
|
|
35
|
-
## Discrete instability signatures (§10)
|
|
36
|
-
|
|
37
|
-
- **Limit cycle at the sample period**: a fault that recurs every N runs because the
|
|
38
|
-
correction and the next sample line up badly.
|
|
39
|
-
- **Chattering**: a value flips every sample between two states (Zeno-like behavior in
|
|
40
|
-
continuous terms) — add hysteresis / deadband so small errors don't trigger action.
|
|
41
|
-
- **Deadbeat vs sluggish**: too much gain overshoots and oscillates; too little never
|
|
42
|
-
converges. Tune the *discrete* gain (e.g. test frequency, retry backoff curve).
|
|
43
|
-
|
|
44
|
-
## Anti-pattern: Coarse sampling masks instability
|
|
45
|
-
|
|
46
|
-
> "A system that passes only when tested weekly may be oscillating daily. The LLM must probe at multiple scales before declaring stability."
|
|
47
|
-
|
|
48
|
-
The LLM must vary test scale and frequency when assessing stability. A single golden-path
|
|
49
|
-
test at low frequency is not observability; it is a sample that may miss the true dynamics.
|
|
50
|
-
|
|
51
|
-
## Worked example: weekly test passes, daily test reveals oscillation
|
|
52
|
-
|
|
53
|
-
**Symptom:** a data pipeline passes end-to-end test when run manually, but downstream teams
|
|
54
|
-
report stale data every morning.
|
|
55
|
-
|
|
56
|
-
```python
|
|
57
|
-
# pipeline runs once a day; test runs manually on demand
|
|
58
|
-
def run_pipeline():
|
|
59
|
-
...
|
|
60
|
-
```
|
|
61
|
-
- Sampling rate: 1× per day (coarse)
|
|
62
|
-
- True dynamics: cache TTL = 1 hour; data freshness degrades within hours → oscillation
|
|
63
|
-
between "fresh" and "stale" every hour
|
|
64
|
-
- Weekly manual test catches only one sample point — coincidentally fresh — masking the
|
|
65
|
-
daily limit cycle
|
|
66
|
-
|
|
67
|
-
**Fix (tighten the loop):**
|
|
68
|
-
```python
|
|
69
|
-
# Hourly CI job + explicit freshness assertion
|
|
70
|
-
def run_pipeline():
|
|
71
|
-
...
|
|
72
|
-
assert freshness_hours() < 2, f"Data stale: {freshness_hours()}h"
|
|
73
|
-
|
|
74
|
-
# Hourly cron in CI; alert on assertion failure
|
|
75
|
-
```
|
|
76
|
-
- Sample rate increased from daily to hourly.
|
|
77
|
-
- True limit cycle (1-hour) now falls within the sampling window and is detectable.
|
|
78
|
-
- Observability: assertion converts invisible state (freshness) into a measurable signal.
|
|
79
|
-
|
|
1
|
+
# Reference: Discrete / Sampled Systems & Test Cadence
|
|
2
|
+
|
|
3
|
+
Book anchor: §10 (discrete control systems — difference equations, sampled data).
|
|
4
|
+
|
|
5
|
+
## Software runs in discrete steps
|
|
6
|
+
|
|
7
|
+
Continuous control theory assumes you can act and observe at every instant. Software acts and
|
|
8
|
+
observes only at **sample instants**: function calls, test runs, deploys, metric scrapes,
|
|
9
|
+
human reviews. Between samples the state is *unobserved*.
|
|
10
|
+
|
|
11
|
+
Consequences:
|
|
12
|
+
- **Sample often enough.** A feedback loop sampled too coarsely (tests only at the end,
|
|
13
|
+
manual checks weeks apart) cannot stabilize a fast-moving fault. Tighten the loop:
|
|
14
|
+
unit → integration → typecheck → run, frequently and automatically.
|
|
15
|
+
- **Guard the gap.** Between samples, anything can happen: timeout, partial write, concurrent
|
|
16
|
+
mutation, crash. Design for the unobserved interval — idempotency, transactions, rollback,
|
|
17
|
+
heartbeat/lease.
|
|
18
|
+
- **Aliasing instability.** A bug that appears only at certain intervals or scales is a
|
|
19
|
+
*sampled-system alias* — the true dynamics are faster than your sampling. Vary the
|
|
20
|
+
sample/scale when testing (small vs large input, fast vs slow clock).
|
|
21
|
+
|
|
22
|
+
## LLM behavior pattern: Treat cadence as a controlled variable
|
|
23
|
+
|
|
24
|
+
> "Your feedback cadence is a design variable. Tighten it when you need stability; loosen it only when the system is already robust."
|
|
25
|
+
|
|
26
|
+
The LLM should recognize that its own test/review cadence is a *sampling rate* that affects
|
|
27
|
+
stability margin. It should not propose changes that rely on coarse-grained verification for
|
|
28
|
+
systems with fast dynamics.
|
|
29
|
+
|
|
30
|
+
- Tight cadence (fast tests, CI on every commit) → can use higher gain (more aggressive
|
|
31
|
+
fixes) and still stay stable.
|
|
32
|
+
- Loose cadence (nightly, manual) → must use lower gain (conservative, well-reviewed changes)
|
|
33
|
+
or it will overshoot.
|
|
34
|
+
|
|
35
|
+
## Discrete instability signatures (§10)
|
|
36
|
+
|
|
37
|
+
- **Limit cycle at the sample period**: a fault that recurs every N runs because the
|
|
38
|
+
correction and the next sample line up badly.
|
|
39
|
+
- **Chattering**: a value flips every sample between two states (Zeno-like behavior in
|
|
40
|
+
continuous terms) — add hysteresis / deadband so small errors don't trigger action.
|
|
41
|
+
- **Deadbeat vs sluggish**: too much gain overshoots and oscillates; too little never
|
|
42
|
+
converges. Tune the *discrete* gain (e.g. test frequency, retry backoff curve).
|
|
43
|
+
|
|
44
|
+
## Anti-pattern: Coarse sampling masks instability
|
|
45
|
+
|
|
46
|
+
> "A system that passes only when tested weekly may be oscillating daily. The LLM must probe at multiple scales before declaring stability."
|
|
47
|
+
|
|
48
|
+
The LLM must vary test scale and frequency when assessing stability. A single golden-path
|
|
49
|
+
test at low frequency is not observability; it is a sample that may miss the true dynamics.
|
|
50
|
+
|
|
51
|
+
## Worked example: weekly test passes, daily test reveals oscillation
|
|
52
|
+
|
|
53
|
+
**Symptom:** a data pipeline passes end-to-end test when run manually, but downstream teams
|
|
54
|
+
report stale data every morning.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# pipeline runs once a day; test runs manually on demand
|
|
58
|
+
def run_pipeline():
|
|
59
|
+
...
|
|
60
|
+
```
|
|
61
|
+
- Sampling rate: 1× per day (coarse)
|
|
62
|
+
- True dynamics: cache TTL = 1 hour; data freshness degrades within hours → oscillation
|
|
63
|
+
between "fresh" and "stale" every hour
|
|
64
|
+
- Weekly manual test catches only one sample point — coincidentally fresh — masking the
|
|
65
|
+
daily limit cycle
|
|
66
|
+
|
|
67
|
+
**Fix (tighten the loop):**
|
|
68
|
+
```python
|
|
69
|
+
# Hourly CI job + explicit freshness assertion
|
|
70
|
+
def run_pipeline():
|
|
71
|
+
...
|
|
72
|
+
assert freshness_hours() < 2, f"Data stale: {freshness_hours()}h"
|
|
73
|
+
|
|
74
|
+
# Hourly cron in CI; alert on assertion failure
|
|
75
|
+
```
|
|
76
|
+
- Sample rate increased from daily to hourly.
|
|
77
|
+
- True limit cycle (1-hour) now falls within the sampling window and is detectable.
|
|
78
|
+
- Observability: assertion converts invisible state (freshness) into a measurable signal.
|
|
79
|
+
|