You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The 2026-05-26 audit surfaced multiple bugs (null supplierCode, null
purchaseOrderLineItems/supplier, bare-except parser patterns) that
routine full client regenerations would have caught. Root cause: agent
briefs in PRs #202 and #216 instructed surgical regens (manually running
openapi-python-client on a patched cached spec and copying 1-2 files)
to keep diffs small — a pattern that bypasses spec download and lets
generated/ drift against the modern generator.
Three harness surfaces now codify the policy:
- CLAUDE.md "Client regeneration policy" section: default is full
pipeline; surgical regens listed under Common Pitfalls.
- .claude/skills/regenerate-client/SKILL.md CRITICAL section: always
full pipeline; deferral protocol mandates tech-debt issue + regen
PR as next work + cross-reference from deferring PR.
- .claude/skills/add-nullable-field/SKILL.md step 5: removed the
"stash + re-run on clean baseline + reapply" guidance that was
encoding the suppress-drift anti-pattern; replaced with absorb-drift
default + pointer to the deferral protocol.
Deferral is OK when truly necessary but never silent — tracking issue
+ prioritized follow-up + PR cross-reference are mandatory.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .claude/skills/add-nullable-field/SKILL.md
+43-14Lines changed: 43 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,17 +13,22 @@ Patch `NULLABLE_FIELDS` in the regeneration script, regenerate, validate, and do
13
13
14
14
## PURPOSE
15
15
16
-
Make a generated model field nullable so the client stops crashing on null API responses — without adding `# type: ignore`.
16
+
Make a generated model field nullable so the client stops crashing on null API responses
17
+
— without adding `# type: ignore`.
17
18
18
19
## CRITICAL
19
20
20
-
-**Never use `# type: ignore` to silence the error** — that's a forbidden shortcut per CLAUDE.md. Add the field to `NULLABLE_FIELDS` and regenerate.
21
-
-**Never edit `stocktrim_public_api_client/generated/` directly** — changes are lost on regeneration.
22
-
-**The fix must be reproducible from the spec** — anyone running `uv run poe regenerate-client` must get the same result.
21
+
-**Never use `# type: ignore` to silence the error** — that's a forbidden shortcut per
22
+
CLAUDE.md. Add the field to `NULLABLE_FIELDS` and regenerate.
23
+
-**Never edit `stocktrim_public_api_client/generated/` directly** — changes are lost on
24
+
regeneration.
25
+
-**The fix must be reproducible from the spec** — anyone running
26
+
`uv run poe regenerate-client` must get the same result.
23
27
24
28
## ASSUMES
25
29
26
-
- You have an actual error message identifying the schema and field (e.g., `PurchaseOrderResponseDto.orderDate` returns null but spec marks it required).
30
+
- You have an actual error message identifying the schema and field (e.g.,
31
+
`PurchaseOrderResponseDto.orderDate` returns null but spec marks it required).
27
32
-`scripts/regenerate_client.py` exists and contains a `NULLABLE_FIELDS` dict.
28
33
29
34
## STANDARD PATH
@@ -53,7 +58,8 @@ NULLABLE_FIELDS = {
53
58
}
54
59
```
55
60
56
-
Use the same comment style as existing entries. Mark with `⚠️ CRITICAL` if null actually crashes (not just wrong types).
61
+
Use the same comment style as existing entries. Mark with `⚠️ CRITICAL` if null actually
62
+
crashes (not just wrong types).
57
63
58
64
### 3. Regenerate
59
65
@@ -67,30 +73,53 @@ uv run poe regenerate-client
67
73
uv run poe check
68
74
```
69
75
70
-
ALL must pass. If a different field is now broken, repeat from step 1 — don't bail with `# type: ignore`.
76
+
ALL must pass. If a different field is now broken, repeat from step 1 — don't bail with
77
+
`# type: ignore`.
71
78
72
-
### 5. Confirm the diff is bounded
79
+
### 5. Inspect the diff and absorb any upstream drift
73
80
74
81
```bash
75
82
git diff --stat
76
83
```
77
84
78
-
You should see:
85
+
You'll see at minimum:
79
86
80
87
-`scripts/regenerate_client.py` (1 small change)
81
-
-`stocktrim_public_api_client/generated/models/<schema>.py` (the field becomes `Optional`)
88
+
-`stocktrim_public_api_client/generated/models/<schema>.py` (the field becomes
89
+
`Optional`)
82
90
- Possibly `client_types.py` if reexports change
83
91
84
-
If the diff is much bigger, regeneration picked up an unrelated upstream change. Stash, run regeneration on a clean baseline, then re-apply your `NULLABLE_FIELDS` change.
92
+
**If the diff is bigger than that, it's because the live StockTrim spec drifted since
93
+
the last regen — this is expected and good.** The regen pulls the latest spec; **default
94
+
is to absorb that drift in the same PR as additional commits.** Adapt downstream code
95
+
(helpers, services, tests, MCP tools) to match any new field shapes, renames, or
96
+
removals. Document the broader changes in the commit body so reviewers see what changed
97
+
in the API surface.
98
+
99
+
If the drift introduces sprawling adaptations that would meaningfully delay your
100
+
nullable-field fix and absolutely cannot ride along, follow the **deferral protocol** in
101
+
`.claude/skills/regenerate-client/SKILL.md` (file `tech-debt` tracking issue + open the
102
+
regen PR next + reference from this PR's description). Deferring without those three
103
+
steps is the anti-pattern that caused the 2026-05 drift bugs.
104
+
105
+
**DO NOT** stash, reset, or otherwise try to suppress the drift to keep the diff small.
106
+
Surgical regens that only touch 1–2 files are an **anti-pattern**; they bypass the live
107
+
spec download and leave the rest of `generated/` frozen against an old generator.
85
108
86
109
### 6. Document evidence (optional)
87
110
88
-
If `docs/contributing/api-feedback.md` exists, append a row noting the schema + field + observed behavior. This is the canonical record for "spec says required, API returns null."
111
+
If `docs/contributing/api-feedback.md` exists, append a row noting the schema + field +
112
+
observed behavior. This is the canonical record for "spec says required, API returns
113
+
null."
89
114
90
115
## EDGE CASES
91
116
92
-
-[Field is in a deeply nested array item] — `NULLABLE_FIELDS` works at the top level of a schema. For nested types, check whether the nested schema is itself a top-level component (it usually is) and add it there.
93
-
-[Field is required in some endpoints but not others] — `NULLABLE_FIELDS` marks the schema field nullable everywhere it's used. Acceptable trade-off; document in the comment.
117
+
-[Field is in a deeply nested array item] — `NULLABLE_FIELDS` works at the top level of
118
+
a schema. For nested types, check whether the nested schema is itself a top-level
119
+
component (it usually is) and add it there.
120
+
-[Field is required in some endpoints but not others] — `NULLABLE_FIELDS` marks the
121
+
schema field nullable everywhere it's used. Acceptable trade-off; document in the
Copy file name to clipboardExpand all lines: .claude/skills/regenerate-client/SKILL.md
+50-21Lines changed: 50 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,23 +10,44 @@ allowed-tools: Bash(uv run poe regenerate-client), Bash(uv run poe check), Bash(
10
10
11
11
# /regenerate-client — Regenerate the StockTrim API Client
12
12
13
-
Run the OpenAPI regeneration pipeline, validate, and commit the result on a feature branch.
13
+
Run the OpenAPI regeneration pipeline, validate, and commit the result on a feature
14
+
branch.
14
15
15
16
## PURPOSE
16
17
17
-
Replace the generated client with a fresh build from the live spec, with all post-processing applied.
18
+
Replace the generated client with a fresh build from the live spec, with all
19
+
post-processing applied.
18
20
19
21
## CRITICAL
20
22
21
-
-**Never hand-edit `stocktrim_public_api_client/generated/` or `client_types.py`** — both are wholly replaced by this skill. Edits are silently lost.
22
-
-**All type/null fixes go in `scripts/regenerate_client.py`** — never add `# type: ignore` to generated code; add the field to `NULLABLE_FIELDS` instead.
23
+
-**ALWAYS use the full pipeline (`uv run poe regenerate-client`).** Surgical regens —
24
+
manually running `openapi-python-client generate` against the cached spec and copying
25
+
1–2 files into `generated/` — are an **ANTI-PATTERN**. They bypass the live spec
26
+
download, leave the rest of `generated/` frozen against the older generator, and
27
+
accumulate drift bugs (stale field types, bare-except patterns, missing nullable
28
+
markers). If you need to regenerate ANYTHING, regenerate EVERYTHING.
29
+
-**Default: absorb upstream drift in the same PR as additional commits.** Keep the
30
+
immediate-fix commit focused, then add follow-up commits adapting
31
+
helpers/services/tests for whatever the regen brought in. Do not stash or reset to
32
+
keep the diff small.
33
+
-**Deferral protocol (only when you absolutely cannot ship the regen in the same PR):**
34
+
ALL of the following are mandatory:
35
+
1. File a `tech-debt` tracking issue noting the regen is owed and why it was deferred.
36
+
1. Open the regen PR as the very next piece of work — no unrelated tasks first.
37
+
1. Reference the tracking issue from the deferring PR's description.
38
+
-**Never hand-edit `stocktrim_public_api_client/generated/` or `client_types.py`** —
39
+
both are wholly replaced by this skill. Edits are silently lost.
40
+
-**All type/null fixes go in `scripts/regenerate_client.py`** — never add
41
+
`# type: ignore` to generated code; add the field to `NULLABLE_FIELDS` instead.
23
42
-**Must be on a feature branch** — never regenerate directly on `main`.
24
-
-**CLAUDE.md zero-tolerance applies** — `uv run poe check` must be green before committing.
43
+
-**CLAUDE.md zero-tolerance applies** — `uv run poe check` must be green before
44
+
committing.
25
45
26
46
## ASSUMES
27
47
28
48
- You're in a git repository on a feature branch (or willing to create one).
29
-
- The live StockTrim spec at `https://api.stocktrim.com/swagger/v1/swagger.yaml` is reachable.
49
+
- The live StockTrim spec at `https://api.stocktrim.com/swagger/v1/swagger.yaml` is
50
+
reachable.
30
51
-`uv` and project deps are installed (`uv sync`).
31
52
32
53
## STANDARD PATH
@@ -49,14 +70,14 @@ uv run poe regenerate-client
49
70
This invokes `scripts/regenerate_client.py` which:
50
71
51
72
1. Downloads the spec
52
-
2. Patches auth (header params → securitySchemes)
53
-
3. Validates with `openapi-spec-validator` and Redocly
54
-
4. Generates the client via `openapi-python-client`
55
-
5. Renames `types.py` → `client_types.py` and rewrites imports
56
-
6. Modernizes `Union[X, Y]` → `X | Y`
57
-
7. Fixes RST docstrings
58
-
8. Applies `NULLABLE_FIELDS` overrides
59
-
9. Runs `ruff --fix`
73
+
1. Patches auth (header params → securitySchemes)
74
+
1. Validates with `openapi-spec-validator` and Redocly
75
+
1. Generates the client via `openapi-python-client`
76
+
1. Renames `types.py` → `client_types.py` and rewrites imports
77
+
1. Modernizes `Union[X, Y]` → `X | Y`
78
+
1. Fixes RST docstrings
79
+
1. Applies `NULLABLE_FIELDS` overrides
80
+
1. Runs `ruff --fix`
60
81
61
82
### 3. Inspect the diff
62
83
@@ -75,9 +96,12 @@ uv run poe check
75
96
76
97
ALL must pass. If failures appear:
77
98
78
-
-**Type error on null field?** → Add to `NULLABLE_FIELDS` in `scripts/regenerate_client.py`, re-run from Step 2. Never add `# type: ignore`.
79
-
-**Helper method broken by renamed model?** → Update the helper. Helpers wrap generated/, so they need to track renames.
80
-
-**MCP tool broken?** → Update the corresponding service in `stocktrim_mcp_server/src/.../services/`.
99
+
-**Type error on null field?** → Add to `NULLABLE_FIELDS` in
100
+
`scripts/regenerate_client.py`, re-run from Step 2. Never add `# type: ignore`.
101
+
-**Helper method broken by renamed model?** → Update the helper. Helpers wrap
102
+
generated/, so they need to track renames.
103
+
-**MCP tool broken?** → Update the corresponding service in
104
+
`stocktrim_mcp_server/src/.../services/`.
81
105
82
106
### 5. Commit on feature branch
83
107
@@ -95,17 +119,22 @@ EOF
95
119
)"
96
120
```
97
121
98
-
Use `feat(client):` scope for client release; add `chore(mcp):` companion commit if MCP services were updated.
122
+
Use `feat(client):` scope for client release; add `chore(mcp):` companion commit if MCP
123
+
services were updated.
99
124
100
125
## EDGE CASES
101
126
102
-
-[Spec download fails] — Check `SPEC_URL` in `scripts/regenerate_client.py`. Network issue? Retry. Permanent? Open an issue.
127
+
-[Spec download fails] — Check `SPEC_URL` in `scripts/regenerate_client.py`. Network
128
+
issue? Retry. Permanent? Open an issue.
103
129
-[Generation breaks for an unrelated schema] — Read DETAIL: Quarantining a Schema
104
-
-[Helper or test fails after regen] — Update the helper/test. Generated code is the source of truth; downstream code adapts.
130
+
-[Helper or test fails after regen] — Update the helper/test. Generated code is the
131
+
source of truth; downstream code adapts.
105
132
106
133
## DETAIL: Quarantining a Schema
107
134
108
-
If a schema generates uncompilable code (rare), patch it in `scripts/regenerate_client.py` before the generation step rather than editing the generated output.
135
+
If a schema generates uncompilable code (rare), patch it in
136
+
`scripts/regenerate_client.py` before the generation step rather than editing the
0 commit comments