org-knowledge-layer 0.1.0__py3-none-any.whl

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.
Files changed (67) hide show
  1. okl/__init__.py +12 -0
  2. okl/__main__.py +8 -0
  3. okl/bootstrap.py +83 -0
  4. okl/cli.py +484 -0
  5. okl/client.py +160 -0
  6. okl/core.py +223 -0
  7. okl/drift.py +119 -0
  8. okl/mcp_server.py +75 -0
  9. okl/scaffold/MANIFEST.md +59 -0
  10. okl/scaffold/ci/method-gates.yml +32 -0
  11. okl/scaffold/ci/okl-verify.yml +59 -0
  12. okl/scaffold/claude/agents/architecture-reviewer.md +41 -0
  13. okl/scaffold/claude/commands/check-rules.md +24 -0
  14. okl/scaffold/claude/commands/feature-spec.md +37 -0
  15. okl/scaffold/claude/rules/example-area.md +22 -0
  16. okl/scaffold/claude/skills/RECOMMENDED-COMPANIONS.md +40 -0
  17. okl/scaffold/claude/skills/encoding-loop/SKILL.md +48 -0
  18. okl/scaffold/claude/skills/verify-before-claiming/SKILL.md +56 -0
  19. okl/scaffold/evals/README.md +32 -0
  20. okl/scaffold/evals/cases.jsonl +1 -0
  21. okl/scaffold/evals/run_evals.py +109 -0
  22. okl/scaffold/gates/check-canon-size.sh +11 -0
  23. okl/scaffold/gates/check-doc-orphans.sh +19 -0
  24. okl/scaffold/gates/check-retractions.sh +22 -0
  25. okl/scaffold/gates/check-tombstones.sh +22 -0
  26. okl/scaffold/gates/run-gates.sh +31 -0
  27. okl/scaffold/hooks/hooks.json +16 -0
  28. okl/scaffold/hooks/stop-okl-encode.sh +78 -0
  29. okl/scaffold/hooks/userpromptsubmit-okl-check.sh +68 -0
  30. okl/scaffold/plugin/plugin.json +10 -0
  31. okl/scaffold/profiles/dotnet/README.md +12 -0
  32. okl/scaffold/profiles/dotnet/rules/architecture.md +55 -0
  33. okl/scaffold/profiles/dotnet/rules/messaging.md +31 -0
  34. okl/scaffold/profiles/dotnet/rules/performance-and-data.md +36 -0
  35. okl/scaffold/profiles/dotnet/rules/security.md +42 -0
  36. okl/scaffold/profiles/geospatial/README.md +6 -0
  37. okl/scaffold/profiles/geospatial/rules/geospatial-ml.md +38 -0
  38. okl/scaffold/profiles/python-rag/README.md +13 -0
  39. okl/scaffold/profiles/python-rag/rules/fastapi-backend.md +37 -0
  40. okl/scaffold/profiles/python-rag/rules/project-structure.md +28 -0
  41. okl/scaffold/profiles/python-rag/rules/rag-pipeline.md +73 -0
  42. okl/scaffold/profiles/react/README.md +18 -0
  43. okl/scaffold/profiles/react/rules/frontend.md +57 -0
  44. okl/scaffold/registries/RETRACTIONS.md +19 -0
  45. okl/scaffold/registries/tombstones.txt +7 -0
  46. okl/scaffold/root/CLAUDE.md +55 -0
  47. okl/scaffold/root/METHOD.md +64 -0
  48. okl/scaffold_cmd.py +110 -0
  49. okl/seed/dotnet-canon.json +489 -0
  50. okl/seed/dotnet-decisions.json +328 -0
  51. okl/seed/dotnet-defects.json +133 -0
  52. okl/seed/dotnet-review-surfaces.json +147 -0
  53. okl/seed/frontend-canon.json +116 -0
  54. okl/seed/geospatial-deeptime-defects.json +59 -0
  55. okl/seed/geospatial-defects.json +154 -0
  56. okl/seed/geospatial-enforcement-defects.json +121 -0
  57. okl/seed/geospatial-eval-defects.json +25 -0
  58. okl/seed/rag-defects.json +120 -0
  59. okl/seed/react-defects.json +45 -0
  60. okl/seed.py +55 -0
  61. okl/service.py +137 -0
  62. okl/store.py +432 -0
  63. org_knowledge_layer-0.1.0.dist-info/METADATA +475 -0
  64. org_knowledge_layer-0.1.0.dist-info/RECORD +67 -0
  65. org_knowledge_layer-0.1.0.dist-info/WHEEL +4 -0
  66. org_knowledge_layer-0.1.0.dist-info/entry_points.txt +2 -0
  67. org_knowledge_layer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,489 @@
1
+ {
2
+ "_comment": "The .NET-platform CANON import (curated from the origin repo: CLAUDE.md 2026-07 snapshot, .claude/tombstones.txt, docs/decisions/). Complements dotnet-defects.json (war stories) with the RULES/DECISIONS/TOMBSTONES layer. Deliberately NOT named *-defects.json so a directory-wide `okl seed seed/` skips it — review scope+tags first, then `okl seed seed/dotnet-canon.json`. Org-scoped rules are portable engineering canon (tag-filtered by repo interests); Wolverine/Aspire operational quirks and all tombstoned identifiers stay repo:dotnet-microservices. NOT yet imported: .claude/audits/ (26 dated article-review verdicts -> future Claim/PriorArt batch), frontend/CLAUDE.md beyond the 3 react-defects nodes, docs/project-decisions.md long tail.",
3
+ "nodes": [
4
+ {
5
+ "key": "r_dbcontext_direct",
6
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
7
+ "found_by": "CLAUDE.md 'Data access' + the simplicity refactor (five repositories deleted)",
8
+ "tags": "dotnet",
9
+ "title": "DbContext directly in handlers — no IFooRepository wrappers; integration tests replace mocks",
10
+ "body": "DbContext already IS Unit-of-Work and DbSet<T> already IS Repository; a wrapper interface adds a layer without capability. The mocking defense fails because EF-touching handlers are properly tested with integration tests against real Testcontainers DBs, not unit mocks. Reads project to DTOs inside the IQueryable (AsNoTracking + Select); writes load the aggregate tracked and SaveChangesAsync.",
11
+ "symptom": "a new IFooRepository interface wrapping DbContext, justified by 'testability'",
12
+ "fix": "take DbContext (or IDbContextFactory<T>) directly; test with Testcontainers integration tests"
13
+ },
14
+ {
15
+ "key": "r_arch_enforcement_ladder",
16
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
17
+ "found_by": "CLAUDE.md 'Promotion signal' + docs/vsa-vs-clean-architecture.md",
18
+ "tags": "dotnet,method",
19
+ "title": "Enforce the dependency rule by escalation: convention → architecture tests → project split",
20
+ "body": "Two things wear the name Clean Architecture: the dependency rule (Domain references nothing — always in force, never complexity-gated) and the multi-project split (only buys compile-time enforcement). The middle rung most teams skip: an architecture test (NetArchTest/ArchUnitNET) asserting 'Domain references no Infrastructure namespaces' enforces the same boundary in CI without project ceremony. Split projects only when the compiler must hold the line or deploy/versioning units genuinely differ.",
21
+ "symptom": "a proposal to split Domain/Application/Infrastructure csprojs 'to enforce boundaries'",
22
+ "fix": "add an architecture test first; split projects only on 5+ aggregates with cross-cutting rules AND Domain/ outgrowing Features/"
23
+ },
24
+ {
25
+ "key": "r_rate_limiter_scaleout",
26
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
27
+ "found_by": "CLAUDE.md 'Rate Limiting'",
28
+ "tags": "security,dotnet",
29
+ "title": "In-memory rate limiters silently weaken to N× the limit at N instances",
30
+ "body": "Framework rate limiters (ASP.NET AddFixedWindowLimiter etc.) count in-process; each instance grants a fresh allowance, so scaling out multiplies the effective limit with no error. At 2+ instances move affected endpoints to a shared-store limiter (Redis); the INCR+EXPIRE pair is two operations with a race window — make it atomic with a Lua script.",
31
+ "symptom": "a rate-limited endpoint deployed to a second instance/replica",
32
+ "fix": "swap to a Redis-backed limiter with an atomic (Lua) increment+TTL before scaling out"
33
+ },
34
+ {
35
+ "key": "r_request_naming",
36
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
37
+ "found_by": "CLAUDE.md 'Coding Standards'",
38
+ "tags": "dotnet,method",
39
+ "title": "Generic *Dto on a request model hides intent — name messages by role",
40
+ "body": "CQRS messages are *Command (write) / *Query (read); RPC contracts are *Request/*Response pairs; *Dto is reserved for read-side projections only, where it correctly signals a transfer shape. 'CreateOrderDto' says nothing about direction, pipeline, or intent.",
41
+ "symptom": "a *Dto-suffixed type used as a command/request input",
42
+ "fix": "rename by role: Command/Query for CQRS messages, Request/Response for RPC, Dto only for read projections"
43
+ },
44
+ {
45
+ "key": "r_comments_durable_docs",
46
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
47
+ "found_by": "CLAUDE.md 'Commenting Convention' (the STATUS.md dangling-pointer incident)",
48
+ "tags": "method",
49
+ "title": "Code comments link durable docs, never rolling-state files",
50
+ "body": "Rolling state files (STATUS.md, sprint notes) churn every few weeks, so a code comment pointing at them dangles silently — the .NET platform's test-factory comments pointed at a STATUS.md entry that had moved. Point comments at the stable home (a docs/*.md section, an issue number); the rolling file may point at code, never the reverse.",
51
+ "symptom": "a code comment referencing a status/progress/planning doc",
52
+ "fix": "repoint the comment at the durable doc or issue; let STATUS-style files point at code instead"
53
+ },
54
+ {
55
+ "key": "r_surprise_in_writing",
56
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
57
+ "found_by": "CLAUDE.md 'Debugging Discipline'",
58
+ "tags": "method",
59
+ "title": "If the failure mode would surprise the next person who hits it, the surprise belongs in writing — before moving on",
60
+ "body": "When debugging surfaces a non-obvious failure (framework version trap, config silently overriding config, ordering gotcha, API contradicting its docs), capture it the same session: a one-liner in the always-on canon, the why in the relevant doc, deferred remainders in the open-issues list. The bar is not 'document every bug fix'; trivial typos don't qualify, rules-discovered-the-hard-way always do.",
61
+ "symptom": "a hard-won debugging insight living only in the fix commit",
62
+ "fix": "encode the lesson in canon + docs in the same session as the fix"
63
+ },
64
+ {
65
+ "key": "r_file_move_discipline",
66
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
67
+ "found_by": "CLAUDE.md 'File-move discipline' (Dockerfile.catalog broken for months after PR #31)",
68
+ "tags": "method",
69
+ "title": "Deleting or renaming a file requires grepping for the old path in the same PR",
70
+ "body": "References live in docs, inline comments, Dockerfile COPY lines, project references, CI run: blocks, and review-bot config — none of which the compiler checks. the .NET platform's Dockerfile.catalog referenced a collapsed project path for months, broken silently until a redeploy attempt. Enforce in layers: a post-move hook printing stale-ref candidates, a CI broken-link audit, review-bot path instructions.",
71
+ "symptom": "git mv / git rm without a repo-wide grep for the old path",
72
+ "fix": "grep and sweep every reference to the old path in the same PR; wire a hook + CI link audit as the mechanical catch"
73
+ },
74
+ {
75
+ "key": "r_grep_to_zero",
76
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
77
+ "found_by": "CLAUDE.md 'Identifier-move discipline' (15+ docs taught Azure Service Bus as current after the RabbitMQ swap)",
78
+ "tags": "method",
79
+ "title": "An identifier sweep is done when grep returns zero — not when the docs you remembered are updated",
80
+ "body": "The compiler catches stale identifiers in code; nothing catches them in prose. Memory-based sweeps left 15+ docs teaching the removed transport as current, plus a metric documented as THE alarm that nothing incremented. Completion criterion is mechanical: git grep of the tombstoned identifier returns zero non-allowlisted hits, and the identifier joins a tombstone registry so CI fails any resurfacing.",
81
+ "symptom": "a removal PR that updates 'the docs I remembered' with no grep-to-zero check",
82
+ "fix": "grep every removed public identifier to zero across md/comments/CI/scripts; tombstone it so CI holds the line"
83
+ },
84
+ {
85
+ "key": "r_docs_review_surface",
86
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
87
+ "found_by": "CLAUDE.md 'Doc-and-diagram discipline'",
88
+ "tags": "method",
89
+ "title": "Docs and diagrams are the review surface, not byproducts — stale ones make every review reason against a fiction",
90
+ "body": "Reviewers (human or bot) observe the system through the architecture doc and diagrams; if those lag the code, drift is invisible until production. When a change affects what a doc/diagram depicts, the doc updates in the same PR or the PR names the deferred follow-up. Keep editable diagram sources paired with their rendered artifacts and audit the pairing in CI.",
91
+ "symptom": "a topology/pattern-changing PR with no doc or diagram delta",
92
+ "fix": "update the depicting doc/diagram in the same PR, or name the deferred issue in the PR body"
93
+ },
94
+ {
95
+ "key": "r_presence_in_loop",
96
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
97
+ "found_by": "CLAUDE.md 'Presence in the loop, not approval at the gate'",
98
+ "tags": "method,agent-safety",
99
+ "title": "For non-pattern-conforming AI work, be present during implementation — reviewing the finished diff is not presence",
100
+ "body": "By the time you review an AI agent's finished diff, it has already filled gaps you didn't notice. For novel work (new bounded context, transport, security model, multi-step refactor) check intermediate state and course-correct before the diff is too large to read honestly; for pattern-conforming work (CRUD matching an existing shape) canon + automated review at the gate is sufficient. 'I'll review when it's done' is how three days of rework happens.",
101
+ "symptom": "an architecturally significant AI-implemented change reviewed only at PR time",
102
+ "fix": "classify the change first; stay in the loop for non-pattern-conforming work, gate-review the rest"
103
+ },
104
+ {
105
+ "key": "r_experiment_sunset",
106
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
107
+ "found_by": "CLAUDE.md 'Continue is the verb that gets you in trouble'",
108
+ "tags": "method",
109
+ "title": "Continue is the verb that gets you in trouble; build is not — every experiment gets a budget and a sunset up front",
110
+ "body": "Cheap building is leverage; the discipline is the stop, not the start. Set a token/cost budget and a stop-time before an experiment begins; when either runs out the default is 'we learned what we needed; we don't continue.' Nobody ever approves the drift from prototype to unowned product — it approves itself one month at a time, and carry-debt (maintained, secured code nobody needed) is real.",
111
+ "symptom": "an experimental branch or prototype with no defined end condition",
112
+ "fix": "declare budget + stop-time at start; at expiry, default to stop and write down what was learned"
113
+ },
114
+ {
115
+ "key": "r_encoding_loop",
116
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
117
+ "found_by": "CLAUDE.md 'Continuous Rule Encoding'",
118
+ "tags": "method",
119
+ "title": "Encode every 'never again / always when' finding to the smallest surface that catches it — a fix PR without the rule is a half-finished job",
120
+ "body": "Findings from any review surface get encoded the same session, into the smallest applicable layer: always-on canon only if every session needs it (keep it lean — size-budgeted in CI), else file-scoped review-bot instructions, a reviewer-agent checklist item, a skill/procedure, or a supporting doc. The fix lives in the PR; the rule lives in the encoding surfaces; the next instance of the antipattern slips through if only the fix lands.",
121
+ "symptom": "a merged fix for a recurring antipattern with no corresponding rule/gate change",
122
+ "fix": "land the rule with the fix (same or paired PR); track deferred encodings as labeled placeholders, never as the encoding itself"
123
+ },
124
+ {
125
+ "key": "r_durability_not_replay",
126
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
127
+ "found_by": "CLAUDE.md 'Durability ≠ replay' + docs/messaging-transport-selection.md",
128
+ "tags": "messaging",
129
+ "title": "Durability ≠ replay — don't reach for a stream just to avoid losing messages",
130
+ "body": "Message loss depends on whether the subscription is durable, not on queue-vs-stream; the misconception comes from Redis Pub/Sub specifically (fire-and-forget). Durable pub/sub (RabbitMQ durable queues, ASB topics, SNS→SQS) persists per-subscriber until ack; with a transactional outbox on the publish side, at-least-once delivery makes the real risk duplication, not loss — hence idempotent handlers. Reach for a stream (Kafka/Event Hubs/Redis Streams) only for replay-from-offset, long retention, an ordered log, or N independent re-readers.",
131
+ "symptom": "'use Kafka so we don't lose messages' with no replay/retention requirement",
132
+ "fix": "outbox + durable queue + idempotent handlers covers loss; adopt a stream only for what a durable queue can't do"
133
+ },
134
+ {
135
+ "key": "r_batch_beats_whenall",
136
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
137
+ "found_by": "CLAUDE.md 'Performance Rules' (PlaceOrder fan-out superseded by batch gRPC, issue #71)",
138
+ "tags": "dotnet,messaging",
139
+ "title": "Parallelize independent awaits — but when N calls target the SAME service, a batch endpoint beats client-side fan-out",
140
+ "body": "Sequential awaits serialize latency for free; Task.WhenAll is right for independent calls to different services (one context per task — DbContext is not thread-safe). When the fan-out all hits one service, a batch endpoint wins: one round-trip instead of N, and the server can make the batch atomic — that is why the WhenAll reference shape was superseded by batch ValidateLines/ReserveLines.",
141
+ "symptom": "a loop of awaits, or a WhenAll fanning N calls into one downstream service",
142
+ "fix": "WhenAll for independent cross-service calls; a batch endpoint when the target is a single service"
143
+ },
144
+ {
145
+ "key": "r_202_long_running",
146
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
147
+ "found_by": "CLAUDE.md 'Performance Rules'",
148
+ "tags": "messaging,method",
149
+ "title": "Long-running work belongs on the message bus — reshape >~1s write paths as 202 Accepted",
150
+ "body": "Validate, persist a tracking row (the aggregate being created can BE the tracking row), publish a message, return 202 immediately. The same rule applies inside message handlers: minutes-scale work moves to a follow-up message. Fan-out over a recipient list is the same shape — one message per recipient/batch with bounded parallelism, never an inline loop holding the request open.",
151
+ "symptom": "a synchronous HTTP handler (or message handler) doing multi-second work inline",
152
+ "fix": "persist a tracking row + publish + return 202; throttled per-item messages for fan-out"
153
+ },
154
+ {
155
+ "key": "r_guid_v7",
156
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
157
+ "found_by": "CLAUDE.md 'Performance Rules'",
158
+ "tags": "dotnet",
159
+ "title": "Entity IDs use time-ordered GUIDs (v7) — random v4 PKs fragment the B-tree; but v7 leaks mint time",
160
+ "body": "Guid.CreateVersion7() appends to the PK index instead of fragmenting it; apply in aggregate factories. Trade-off: the v7 timestamp is decodable from the ID, so don't use it where mint time is sensitive (security tokens, admin-only refs). v4 and v7 coexist fine in the same column.",
161
+ "symptom": "Guid.NewGuid() for a clustered/indexed primary key, or a v7 ID used as a security token",
162
+ "fix": "v7 for entity PKs via the factory; v4 (or a real token) where mint time must stay hidden"
163
+ },
164
+ {
165
+ "key": "r_secret_fakes",
166
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
167
+ "found_by": "CLAUDE.md 'Testing' (gitleaks 8.24.x allowlist attempt, tried and removed)",
168
+ "tags": "security,method",
169
+ "title": "Test-fixture fake credentials: prefer low-entropy fakes; high-entropy ones need inline scanner markers — and secret scanners scan the whole PR range",
170
+ "body": "A protocol-syntax-valid low-entropy fake (guest:guest@localhost) never trips the scanner, so no suppressions accumulate. If a parser demands a high-entropy fake, make the literal self-labeling (decodes to 'fake-...-for-testing-only') and suppress with an inline line-scoped marker, not a repo-level scanner config (version-fragile; ORs where you expect ANDs). Diff-range residue trap: the scanner walks every commit in the range — a later fix commit doesn't suppress an earlier unmarked one; squash if hit. Never reproduce a high-entropy literal in docs prose.",
171
+ "symptom": "a secret-scanner finding on a test fixture, or a repo-level allowlist growing to cover fakes",
172
+ "fix": "use low-entropy self-evidently-fake values; inline line-scoped markers for unavoidable high-entropy fakes; squash residue"
173
+ },
174
+ {
175
+ "key": "r_aspire_waitfor",
176
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
177
+ "found_by": "CLAUDE.md 'Package Management' (services showing 'Finished' instead of 'Running')",
178
+ "tags": "dotnet",
179
+ "title": "Orchestrator reference-injection is not readiness — every WithReference on real infra needs a matching WaitFor",
180
+ "body": "Aspire's WithReference only injects connection strings; the service starts as soon as env vars resolve, races a still-warming container (RabbitMQ, SQL Server take tens of seconds on first run), and dies with connection-refused. Hard rule: every WithReference on a non-trivial dependency (DB, broker, identity, peer service) gets .WaitFor(x). The generalization: wiring is not readiness in any orchestrator.",
181
+ "symptom": "services exiting at startup with connection-refused while infra containers warm up",
182
+ "fix": "pair every WithReference with WaitFor (and generally: gate service start on dependency health, not env-var presence)"
183
+ },
184
+ {
185
+ "key": "r_wolverine_lockstep",
186
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
187
+ "found_by": "CLAUDE.md 'Package Management' (WolverineFx.RabbitMQ stranded at 6.8.0 by #175)",
188
+ "tags": "dotnet,messaging",
189
+ "title": "All WolverineFx.* packages move together — and a package added after a dependabot group PR opened gets left behind",
190
+ "body": "Core + transports release in lockstep; a transport N minors behind core references changed core APIs — unsupported combination that builds clean (runtime/behavior risk only). The dependabot group normally holds the family together, but a package added to the manifest after a group PR was opened is skipped by that PR. After adding any WolverineFx.* package: grep the versions file and verify family-version match.",
191
+ "symptom": "mixed WolverineFx.* versions in Directory.Packages.props after a dependency-bot merge",
192
+ "fix": "bump the whole family as one change; after adding a new member, verify it matches the family version"
193
+ },
194
+ {
195
+ "key": "r_wolverine_inbox",
196
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
197
+ "found_by": "CLAUDE.md 'Package Management' (fixed in #169)",
198
+ "tags": "dotnet,messaging",
199
+ "title": "Wolverine durability is per-direction — durable outbox on senders does nothing for listeners",
200
+ "body": "Default listeners are buffered: the broker is acked as messages enter the in-memory buffer, before handlers run, so a crashing consumer loses in-flight messages (a stopped one resumes). Store-backed services use UseDurableInboxOnAllListeners(); store-less services use ProcessInline() per listener (ack after handler). Every new ListenToRabbitQueue gets one of the two — a bare listener silently reintroduces the loss window.",
201
+ "symptom": "a new queue listener declared with neither durable-inbox policy nor inline processing",
202
+ "fix": "durable inbox for store-backed services, ProcessInline for store-less ones — never a bare listener"
203
+ },
204
+ {
205
+ "key": "r_aspire_versions",
206
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
207
+ "found_by": "CLAUDE.md 'Package Management'",
208
+ "tags": "dotnet",
209
+ "title": "Aspire SDK and runtime packages must match to the minor — mismatches fail at two different layers",
210
+ "body": "Major mismatches surface at build/startup as TypeLoadException on internal types; minor mismatches surface at runtime as DCP rejecting startup ('newer version of the AppHost package is required'). Bump the AppHost SDK declaration and the Aspire.Hosting.* package versions together as one change.",
211
+ "symptom": "TypeLoadException at startup, or DCP refusing to launch after a partial Aspire bump",
212
+ "fix": "bump SDK + packages together; SDK ≥ packages always"
213
+ },
214
+ {
215
+ "key": "r_no_nplus1",
216
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
217
+ "found_by": "CLAUDE.md 'Performance Rules'",
218
+ "tags": "dotnet",
219
+ "title": "No N+1 — never query inside a loop over another query's results",
220
+ "body": "Use Include or (better) projection so the database answers in one round-trip. A loop of per-item queries is the canonical serialized-latency bug and is invisible in tests against tiny datasets.",
221
+ "symptom": "a query executed inside a foreach over results from another query",
222
+ "fix": "collapse into one query via Include or a projection"
223
+ },
224
+ {
225
+ "key": "r_sargable",
226
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
227
+ "found_by": "CLAUDE.md 'Performance Rules' + SearchProducts.cs documented trade-off",
228
+ "tags": "dotnet,data-quality",
229
+ "title": "Non-sargable predicates defeat indexes — fix at write time, not query time",
230
+ "body": "A WHERE that wraps the column in a function (lower(email) = x) can't use a B-tree index. Normalize on insert/update (a normalized column populated by the aggregate factory) or use a case-insensitive collation. Leading-wildcard substring search needs a real text index (tsvector/Elasticsearch/Meilisearch) when load justifies it — until then, document the trade-off at the call site.",
231
+ "symptom": "a Where clause applying a function to the column, or LIKE '%text%'",
232
+ "fix": "normalize at write time / use collation; adopt a text index only when load demands"
233
+ },
234
+ {
235
+ "key": "r_pagination_cap",
236
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
237
+ "found_by": "CLAUDE.md 'Performance Rules'",
238
+ "tags": "dotnet",
239
+ "title": "Every list endpoint paginates with a server-side size cap; keyset pagination for deep offsets",
240
+ "body": "An unpaginated list endpoint is an unbounded query a client can weaponize by accident. Cap page size server-side (≤100 here); use keyset (seek) pagination where offsets grow large, since OFFSET N scans N rows to discard them.",
241
+ "symptom": "a list endpoint with no page-size cap, or deep OFFSET pagination on a large table",
242
+ "fix": "server-side cap on page size; keyset pagination past shallow offsets"
243
+ },
244
+ {
245
+ "key": "r_bulk_ops",
246
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
247
+ "found_by": "CLAUDE.md 'Performance Rules'",
248
+ "tags": "dotnet",
249
+ "title": "Bulk mutations use set-based operations — never load thousands of rows to change them",
250
+ "body": "ExecuteUpdateAsync/ExecuteDeleteAsync (or raw set-based SQL) mutate in the database; materializing entities to loop-and-save multiplies memory, round-trips, and change-tracking cost by row count.",
251
+ "symptom": "a loop loading entities to update/delete them en masse",
252
+ "fix": "one set-based ExecuteUpdate/ExecuteDelete statement"
253
+ },
254
+ {
255
+ "key": "r_optimistic_concurrency",
256
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
257
+ "found_by": "CLAUDE.md 'Performance Rules' + docs/performance-and-data-correctness.md",
258
+ "tags": "dotnet,data-quality",
259
+ "title": "Every updatable aggregate carries a concurrency token — last-write-wins is not acceptable",
260
+ "body": "Without a token (row-version column, Postgres xmin), two concurrent editors silently overwrite each other and the loss is undetectable after the fact. The write path handles the concurrency-conflict exception with a bounded retry; conflicts are a normal outcome, not an error to eliminate.",
261
+ "symptom": "an updatable entity with no row-version/xmin token, or an unhandled concurrency exception",
262
+ "fix": "add the token; handle the conflict with bounded retry in the write pipeline"
263
+ },
264
+ {
265
+ "key": "r_logging_hygiene",
266
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
267
+ "found_by": "CLAUDE.md 'Performance Rules' + 'Observability' (MA0002)",
268
+ "tags": "dotnet,method",
269
+ "title": "Structured logging: message templates with placeholders, summaries not per-item lines, no null scope keys",
270
+ "body": "Templates ('User {UserId} logged in') keep logs queryable and are required for correlation scopes; interpolation/concatenation destroys both. Tight loops log a summary count, not per-item lines. Scope dictionaries never take null/empty values, and dictionaries are constructed with an ordinal comparer.",
271
+ "symptom": "interpolated log strings, per-item logging in a loop, or nullable values added to log scopes",
272
+ "fix": "templates + placeholders; summary logs; guard scope keys and use ordinal comparers"
273
+ },
274
+ {
275
+ "key": "r_connection_hold",
276
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
277
+ "found_by": "CLAUDE.md 'Performance Rules'",
278
+ "tags": "dotnet",
279
+ "title": "DB connection hold time: open → query → dispose; never await unrelated work while a connection is open",
280
+ "body": "Awaiting an HTTP call or message publish while holding a pooled connection starves the pool under load — the outage presents as connection-timeout errors far from the offending code. Do the IO, release, then continue.",
281
+ "symptom": "an HTTP/messaging await between a query and its connection's disposal",
282
+ "fix": "finish and dispose the DB work before awaiting unrelated IO"
283
+ },
284
+ {
285
+ "key": "r_cache_write_path",
286
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
287
+ "found_by": "CLAUDE.md 'Performance Rules'",
288
+ "tags": "dotnet",
289
+ "title": "Cache invalidation happens in the write path — the mutating handler invalidates, not 'later' or via TTL",
290
+ "body": "A handler that mutates a cached entity updates or invalidates the cache in the same handler; deferring to TTL expiry or a separate process leaves a window where reads serve data known to be stale. (The frontend mutation-invalidation rule is this same rule client-side.)",
291
+ "symptom": "a write handler touching a cached entity with no cache update in the same handler",
292
+ "fix": "invalidate/update the cache in the mutating handler itself"
293
+ },
294
+ {
295
+ "key": "r_migrations_immutable",
296
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
297
+ "found_by": "CLAUDE.md 'Performance Rules'",
298
+ "tags": "dotnet,data-quality",
299
+ "title": "Migrations are immutable once applied anywhere; destructive changes need a multi-step plan",
300
+ "body": "Editing an applied migration desynchronizes environments that already ran it — the schema history lies. Drop/rename/NOT-NULL-on-existing-column changes ship as expand-migrate-contract sequences, never as a single destructive step.",
301
+ "symptom": "a diff editing an already-applied migration, or a one-shot destructive schema change",
302
+ "fix": "add a new migration; stage destructive changes across deploys"
303
+ },
304
+ {
305
+ "key": "r_measure_first",
306
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
307
+ "found_by": "CLAUDE.md 'Performance Rules'",
308
+ "tags": "method,dotnet",
309
+ "title": "Measure before optimizing — no caching, compiled queries, ValueTask, span tricks, or split queries on intuition",
310
+ "body": "Benchmarks for code paths, counters/load tools for system behavior, the ORM's query-string dump for SQL shape. Micro-optimizations like AsSpan-over-Substring apply only on profiled synchronous hot paths (spans can't cross awaits anyway). Unmeasured optimization adds complexity with unknown sign.",
311
+ "symptom": "a performance-motivated change with no profile/benchmark attached",
312
+ "fix": "profile first; keep the receipt with the change"
313
+ },
314
+ {
315
+ "key": "r_dapper_escape",
316
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
317
+ "found_by": "CLAUDE.md 'Performance Rules'",
318
+ "tags": "dotnet",
319
+ "title": "Raw-SQL escape hatches are sanctioned, bounded, and share the ORM's connection — not a peer abstraction",
320
+ "body": "Reach past the ORM (Dapper here) only when: the SQL is provider-specific and doesn't translate, profiling proves the ORM is the bottleneck, or LINQ obscures a SQL aggregation. Always use the ORM's own connection so the escape hatch shares the ambient transaction. Writes still go through aggregates + the ORM.",
321
+ "symptom": "a raw-SQL data path introduced for convenience, or one opening its own connection",
322
+ "fix": "meet one of the three conditions, share the ORM connection, keep writes in the ORM"
323
+ },
324
+ {
325
+ "key": "r_exceptions_canon",
326
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
327
+ "found_by": "CLAUDE.md 'Key Conventions' (Anton Martyniuk VSA audit, .claude/audits/2026-06-08)",
328
+ "tags": "dotnet,method",
329
+ "title": "Pick ONE error-flow canon and name the flip triggers — here: exceptions at the handler boundary, no Result<T>",
330
+ "body": "Expected business errors throw at the handler boundary and a global handler translates to RFC 7807 ProblemDetails. The Result/OneOf/ErrorOr pattern is defensible (exhaustiveness, allocation-cheap) but mixing both styles is the real failure. The named triggers to flip: profiled exception cost on a hot path, or a step with high expected-failure rate where exception-as-control-flow reads wrong. Neither has surfaced.",
331
+ "symptom": "a Result<T>-style return introduced into an exception-canon codebase (or vice versa)",
332
+ "fix": "follow the declared canon; flip only when a named trigger fires, repo-wide"
333
+ },
334
+ {
335
+ "key": "r_problemdetails_traceid",
336
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
337
+ "found_by": "CLAUDE.md 'Security Requirements' (GlobalExceptionHandler)",
338
+ "tags": "security,dotnet",
339
+ "title": "Error responses: generic detail + trace ID only — the full W3C traceparent leaks server call structure",
340
+ "body": "Clients get RFC 7807 ProblemDetails with a correlation trace ID; internal state, entity IDs, and stack traces stay in server logs. Subtlety: expose Activity.TraceId (32 hex chars), NOT Activity.Id — the full traceparent embeds the span ID, which leaks server-side handler call structure to clients.",
341
+ "symptom": "an error body carrying exception details, entity IDs, or a full traceparent string",
342
+ "fix": "generic ProblemDetails + bare trace ID; details to server logs keyed by that ID"
343
+ },
344
+ {
345
+ "key": "r_https_metadata_failclosed",
346
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
347
+ "found_by": "CLAUDE.md 'Security Requirements' (JWT validation)",
348
+ "tags": "security,dotnet",
349
+ "title": "OIDC metadata over plaintext HTTP must fail loudly outside dev — a silent scheme-derived default is a MITM door",
350
+ "body": "Plaintext OIDC/JWKS fetch lets a man-in-the-middle inject signing keys — game over for token validation. RequireHttpsMetadata is therefore fail-closed outside Development, never silently derived from the authority URL's scheme; a legitimate internal-http deployment opts out explicitly and the opt-out logs a warning.",
351
+ "symptom": "an http:// identity authority accepted without an explicit, logged opt-out",
352
+ "fix": "fail startup on plaintext OIDC outside dev; explicit config opt-out with a warning"
353
+ },
354
+ {
355
+ "key": "r_idp_policy_pinned",
356
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
357
+ "found_by": "CLAUDE.md 'Security Requirements' (Keycloak realm) + docs/project-decisions.md §7",
358
+ "tags": "security",
359
+ "title": "Identity-provider token policy is pinned explicitly in exported config — never rely on realm/tenant defaults",
360
+ "body": "Access-token lifespan, refresh-token rotation and single-use, session idle/max are pinned in the committed realm config (here: 5-min access tokens, rotated single-use refresh, 30m/10h sessions). Defaults drift across IdP versions and environments; and dependent settings must move together — if the access-token lifespan changes, the JWT ClockSkew rationale changes with it.",
361
+ "symptom": "an IdP realm/tenant running on default token lifetimes, or a lifespan changed without its dependents",
362
+ "fix": "pin the policy in exported config under version control; update coupled settings as one change"
363
+ },
364
+ {
365
+ "key": "r_idempotent_handlers",
366
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
367
+ "found_by": "CLAUDE.md 'Key Conventions' + 'Durability ≠ replay'",
368
+ "tags": "messaging",
369
+ "title": "At-least-once delivery makes duplication, not loss, the real risk — every state-mutating handler is idempotent",
370
+ "body": "With an outbox on the publish side and durable queues on the consume side, messages don't get lost — they get redelivered. Every handler that mutates state must tolerate replay (natural idempotency via state checks, or a processed-message guard). Store-less sinks may be duplicate-tolerant instead (a re-sent notification is benign) — but that's a decision to record, not an accident.",
371
+ "symptom": "a state-mutating message handler with no idempotency guard",
372
+ "fix": "make redelivery a no-op via state check or dedup guard; document duplicate-tolerance where chosen"
373
+ },
374
+ {
375
+ "key": "r_rich_domain_when_observed",
376
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
377
+ "found_by": "CLAUDE.md 'Domain-Driven Design' (NotificationService precedent)",
378
+ "tags": "dotnet,method",
379
+ "title": "The aggregate pattern only earns its keep when someone observes the invariant — otherwise it's ceremony",
380
+ "body": "Persisted entities with non-trivial invariants get factories, private setters, and method-guarded state changes. But an in-memory, single-use object discarded after the handler returns needs none of that — inline the validation or use a request validator. Corollary: a service with no domain entities needs no Domain project; its ports live in the application layer (NotificationService: a stateless event-to-email pump, three projects, no Domain).",
381
+ "symptom": "a factory + private setters + status enum on a type nothing persists or re-reads",
382
+ "fix": "reserve the aggregate shape for persisted, invariant-bearing entities; inline validation elsewhere"
383
+ },
384
+ {
385
+ "key": "r_aaa_narrative",
386
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
387
+ "found_by": "CLAUDE.md 'Testing'",
388
+ "tags": "method",
389
+ "title": "Tests are Arrange-Act-Assert with story comments — a junior reads the test and understands the contract without the SUT",
390
+ "body": "Each phase explains what's set up and why it matters, what's called, and what each assertion guards. Multi-invariant asserts are numbered with the why (especially security boundaries, idempotency guards, ordering-sensitive operations). Trivial happy paths can be terse; security/concurrency/idempotency tests get the full story.",
391
+ "symptom": "a security or concurrency test whose assertions don't say what failure they guard against",
392
+ "fix": "narrate the phases; number and justify multi-invariant assertions"
393
+ },
394
+ {
395
+ "key": "r_authz_test_required",
396
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
397
+ "found_by": "CLAUDE.md 'Testing' (the original GET /orders/{id} IDOR survived the codebase lifetime)",
398
+ "tags": "security,method",
399
+ "title": "Authorization behavior is only proven by an authorization-FAILURE test — clean builds and passing unit tests say nothing",
400
+ "body": "Every endpoint returning or mutating a scoped entity requires an integration test that authenticates as user X, requests user Y's resource, and asserts 404 (not 200, not 403). The absence of exactly this test is how the original IDOR survived undetected for the lifetime of the codebase.",
401
+ "symptom": "a scoped-entity endpoint shipped without a cross-user access-denied test",
402
+ "fix": "add the X-cannot-read-Y integration test asserting 404 with every scoped endpoint"
403
+ },
404
+ {
405
+ "key": "r_feature_file_cap",
406
+ "type": "Rule", "scope": "org", "repo": "dotnet-microservices", "verified": true,
407
+ "found_by": "CLAUDE.md 'Project Structure' (Anton Martyniuk VSA audit)",
408
+ "tags": "dotnet,method",
409
+ "title": "Co-located feature files carry a soft size cap (~300 lines) — split into sibling files, not into layers",
410
+ "body": "One-file-per-use-case (command + validator + handler) is the canon, and 'giant file per slice' is its known failure mode. Past the cap, extract the validator or record types into sibling files in the same feature folder — the cap is on size, not on file count per slice, and it never justifies reintroducing layer folders.",
411
+ "symptom": "a feature file well past ~300 lines, or a size complaint used to argue for layer folders",
412
+ "fix": "split into sibling files within the feature; keep the slice co-located"
413
+ },
414
+ {
415
+ "key": "r_middleware_order",
416
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
417
+ "found_by": "CLAUDE.md 'Observability' (MapDefaultEndpoints canonical order)",
418
+ "tags": "dotnet",
419
+ "title": "HTTP middleware order is strict: exception → authentication → correlation → authorization",
420
+ "body": "CorrelationIdMiddleware runs AFTER UseAuthentication (it reads context.User to stamp UserId) and BEFORE UseAuthorization (so 401/403 denials log with the user attached). Reordering silently degrades logs rather than failing.",
421
+ "symptom": "denial logs missing user identity, or correlation entries with empty UserId",
422
+ "fix": "keep the canonical order in the shared endpoint setup; don't inline-reorder per service"
423
+ },
424
+ {
425
+ "key": "r_wolverine_instance_middleware",
426
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
427
+ "found_by": "CLAUDE.md 'Observability'",
428
+ "tags": "dotnet,messaging",
429
+ "title": "Wolverine middleware discovery requires instance methods — static methods throw at host startup",
430
+ "body": "AddMiddleware<T>() only discovers Before/After/Finally (and Async variants) as instance methods on a public class with a public constructor; statics raise InvalidWolverineMiddlewareException at startup. Suppress the analyzer's 'should be static' with a justification referencing this rule.",
431
+ "symptom": "InvalidWolverineMiddlewareException at host startup after adding middleware",
432
+ "fix": "instance methods on a public class; suppress S2325 with justification"
433
+ },
434
+ {
435
+ "key": "r_aspire_azure_fallback",
436
+ "type": "Rule", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
437
+ "found_by": "CLAUDE.md 'Package Management'",
438
+ "tags": "dotnet",
439
+ "title": "Azure-only Aspire resources are gated on publish mode — no local emulator means dev must skip them",
440
+ "body": "AddAzureApplicationInsights has no local emulator; ungated, the resource pane shows 'Missing subscription configuration' and every service WithReference-ing it fails to start. Gate on ExecutionContext.IsPublishMode and skip in dev.",
441
+ "symptom": "local AppHost failing to start services over a missing Azure subscription",
442
+ "fix": "gate Azure-only resources on publish mode"
443
+ },
444
+ {
445
+ "key": "dec_sixth_surface",
446
+ "type": "Decision", "scope": "org", "repo": "dotnet-microservices", "verified": true, "status": "live",
447
+ "found_by": "docs/decisions/2026-07-17-sixth-surface-okl.md",
448
+ "tags": "method,retrieval-design",
449
+ "title": "The 6th surface was built as the OKL — a cross-repo, fail-closed knowledge layer, not in-repo microagents",
450
+ "body": "'Two readers, one canon' was really N readers, N canons: every repo re-learned the same lessons. The tracked-but-unbuilt 6th surface (trigger-loaded microagents) became a standalone org knowledge layer instead, because microagents in a repo's config are still per-repo and optional context was never the gap — enforcement + cross-repo reach was. Measured: held-fixed A/B with blind judge≠generator showed defect reproduction 50%→6% on covered tasks (75%→8% on baseline-failing tasks); both misses were coverage gaps, so the check doubles as a coverage detector. The five in-repo surfaces are unchanged; the OKL holds the cross-repo subset."
451
+ },
452
+ {
453
+ "key": "dec_asb_removed",
454
+ "type": "Decision", "scope": "repo", "repo": "dotnet-microservices", "verified": true, "status": "live",
455
+ "found_by": "docs/full-saga-deployment-plan.md (D3) + #148/#159",
456
+ "tags": "dotnet,messaging",
457
+ "title": "Messaging transport is RabbitMQ everywhere; Azure Service Bus was evaluated and removed",
458
+ "body": "ASB's local emulator cannot run the saga (subscription admin returns HTTP 500 and Wolverine's system queues can't auto-provision against it) and no Azure deployment exists, so carrying a second wiring earned nothing — dev now matches prod. The transport remains swappable behind Wolverine (~5 lines per service). Re-add ASB only if Azure becomes a real deployment target; its identifiers are tombstoned meanwhile."
459
+ },
460
+ {
461
+ "key": "ts_azure_service_bus",
462
+ "type": "Tombstone", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
463
+ "found_by": ".claude/tombstones.txt [azure-service-bus] (removed in #159)",
464
+ "tags": "dotnet,messaging",
465
+ "title": "Retired: Azure Service Bus identifiers (azureservicebus, Azure.Messaging.ServiceBus, servicebus.windows.net, *-sub subscription names)",
466
+ "body": "RabbitMQ is the transport. Any doc, comment, or config presenting ASB as current is drift — 15+ docs taught it as current after the swap until the grep-to-zero sweep. Old subscription names (payment-orders-sub, notify-orders-sub, order-payments-sub, shipping-payments-sub, notify-payments-sub, order-shipping-sub, notify-shipping-sub) were renamed to queue names without the -sub suffix. CI tombstone audit fails any resurfacing."
467
+ },
468
+ {
469
+ "key": "ts_send_notification_queue",
470
+ "type": "Tombstone", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
471
+ "found_by": ".claude/tombstones.txt [send-notification-queue] (removed in #170)",
472
+ "tags": "dotnet,messaging",
473
+ "title": "Retired: SendNotificationCommand / send-notification queue — dead-end wiring, never resurrect",
474
+ "body": "Nothing published the command and it had no handler; the working path is the in-process cascade of the internal SendNotificationRequest. Documenting the queue as publishable was actively harmful: a conforming publisher's message would arrive unhandleable and be silently discarded."
475
+ },
476
+ {
477
+ "key": "ts_dead_observability",
478
+ "type": "Tombstone", "scope": "repo", "repo": "dotnet-microservices", "verified": true,
479
+ "found_by": ".claude/tombstones.txt [dead-observability-artifacts] (removed in #171)",
480
+ "tags": "dotnet",
481
+ "title": "Retired: messages.abandoned / MessagesAbandoned / AppMetrics / App.Messaging — dead instruments, replaced by Wolverine's own meter",
482
+ "body": "Declared/registered but never incremented after the processors feeding them died in the transport migration — a working-looking alarm that never fires. Wolverine's own meter (registered via AddMeter) provides the real instruments. Do not reintroduce the names in code OR prose."
483
+ }
484
+ ],
485
+ "edges": [
486
+ {"src": "ts_dead_observability", "rel": "ENCODES", "dst": "seed:dotnet-defects:nc_dead_metric"},
487
+ {"src": "ts_azure_service_bus", "rel": "DEFINED_IN", "dst": "dec_asb_removed"}
488
+ ]
489
+ }