@booklib/skills 1.5.1 → 1.6.0
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/CONTRIBUTING.md +23 -1
- package/README.md +55 -0
- package/benchmark/devto-post.md +178 -0
- package/benchmark/order-processing.original.js +158 -0
- package/benchmark/order-processing.pr-toolkit.js +181 -0
- package/benchmark/order-processing.skill-router.js +271 -0
- package/benchmark/review-report.md +129 -0
- package/bin/skills.js +327 -69
- package/commands/animation-at-work.md +10 -0
- package/commands/clean-code-reviewer.md +10 -0
- package/commands/data-intensive-patterns.md +10 -0
- package/commands/data-pipelines.md +10 -0
- package/commands/design-patterns.md +10 -0
- package/commands/domain-driven-design.md +10 -0
- package/commands/effective-java.md +10 -0
- package/commands/effective-kotlin.md +10 -0
- package/commands/effective-python.md +10 -0
- package/commands/effective-typescript.md +10 -0
- package/commands/kotlin-in-action.md +10 -0
- package/commands/lean-startup.md +10 -0
- package/commands/microservices-patterns.md +10 -0
- package/commands/programming-with-rust.md +10 -0
- package/commands/refactoring-ui.md +10 -0
- package/commands/rust-in-action.md +10 -0
- package/commands/skill-router.md +10 -0
- package/commands/spring-boot-in-action.md +10 -0
- package/commands/storytelling-with-data.md +10 -0
- package/commands/system-design-interview.md +10 -0
- package/commands/using-asyncio-python.md +10 -0
- package/commands/web-scraping-python.md +10 -0
- package/docs/index.html +268 -44
- package/package.json +4 -1
- package/scripts/gen-og.mjs +142 -0
- package/skills/skill-router/SKILL.md +23 -0
package/CONTRIBUTING.md
CHANGED
|
@@ -95,7 +95,27 @@ Aim for 3–5 evals per skill covering:
|
|
|
95
95
|
2. A subtle or intermediate case
|
|
96
96
|
3. Already-good code (the agent should recognize it and not manufacture issues)
|
|
97
97
|
|
|
98
|
-
### 5.
|
|
98
|
+
### 5. Run evals and commit results
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ANTHROPIC_API_KEY=your-key npx @booklib/skills eval <name>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This runs each eval **with and without** the skill and writes `evals/results.json`. Commit this file — it is how CI and readers verify the skill actually works.
|
|
105
|
+
|
|
106
|
+
**Quality thresholds** (calibrated to `claude-haiku-4-5` as judge):
|
|
107
|
+
|
|
108
|
+
| Metric | Minimum | Good | Excellent |
|
|
109
|
+
|--------|---------|------|-----------|
|
|
110
|
+
| Pass rate (with skill) | ≥ 80% | ≥ 85% | ≥ 90% |
|
|
111
|
+
| Delta over baseline | ≥ 20pp | ≥ 25pp | ≥ 30pp |
|
|
112
|
+
| Baseline (without skill) | any | < 70% | < 60% |
|
|
113
|
+
|
|
114
|
+
A high delta matters as much as a high pass rate — it proves the skill is doing real work, not just measuring what Claude already knows. A skill with 90% pass rate and 5pp delta is less valuable than one with 85% and 30pp delta.
|
|
115
|
+
|
|
116
|
+
The 80% threshold is calibrated to the judge model's own noise floor. Consistently hitting 80%+ with haiku as judge means the skill reliably catches what it claims to catch.
|
|
117
|
+
|
|
118
|
+
### 6. Submit a PR
|
|
99
119
|
|
|
100
120
|
```bash
|
|
101
121
|
git checkout -b skill/book-name
|
|
@@ -111,6 +131,8 @@ PR checklist:
|
|
|
111
131
|
- [ ] SKILL.md is under 500 lines
|
|
112
132
|
- [ ] `examples/before.md` and `examples/after.md` exist
|
|
113
133
|
- [ ] `evals/evals.json` has at least 3 test cases
|
|
134
|
+
- [ ] `evals/results.json` committed (run `npx @booklib/skills eval <name>`)
|
|
135
|
+
- [ ] Pass rate ≥ 80% and delta ≥ 20pp in results.json
|
|
114
136
|
- [ ] README.md skills table updated
|
|
115
137
|
|
|
116
138
|
## Requesting a skill
|
package/README.md
CHANGED
|
@@ -118,6 +118,8 @@ User: "Review my order processing service"
|
|
|
118
118
|
|
|
119
119
|
This means skills compose: `skill-router` acts as an orchestrator that picks the right specialist skills for the context, without requiring the user to know the library upfront.
|
|
120
120
|
|
|
121
|
+
**See it in action:** The [`benchmark/`](./benchmark/) folder contains a head-to-head comparison — same buggy Node.js file reviewed by the native PR toolkit vs. `skill-router` routing to `clean-code-reviewer` + `design-patterns`. The skill-router pipeline finds ~47% more unique issues and adds a full refactor roadmap with pattern sequence.
|
|
122
|
+
|
|
121
123
|
## Skills
|
|
122
124
|
|
|
123
125
|
| Skill | Description |
|
|
@@ -145,6 +147,59 @@ This means skills compose: `skill-router` acts as an orchestrator that picks the
|
|
|
145
147
|
| 🔄 [using-asyncio-python](./skills/using-asyncio-python/) | Asyncio practices from Caleb Hattingh's *Using Asyncio in Python* — coroutines, event loop, tasks, and signal handling |
|
|
146
148
|
| 🕷️ [web-scraping-python](./skills/web-scraping-python/) | Web scraping practices from Ryan Mitchell's *Web Scraping with Python* — BeautifulSoup, Scrapy, and data storage |
|
|
147
149
|
|
|
150
|
+
## Quality
|
|
151
|
+
|
|
152
|
+
Skills are evaluated against 6–15 test cases each, run both **with** and **without** the skill using `claude-haiku-4-5` as model and judge. The delta over baseline is the key signal — it measures how much the skill actually improves Claude's output beyond what it can do unaided.
|
|
153
|
+
|
|
154
|
+
**Thresholds:** pass rate ≥ 80% · delta ≥ 20pp · baseline < 70%
|
|
155
|
+
|
|
156
|
+
<!-- quality-table-start -->
|
|
157
|
+
| Skill | Pass Rate | Baseline | Delta | Evals | Last Run |
|
|
158
|
+
|-------|-----------|----------|-------|-------|----------|
|
|
159
|
+
| animation-at-work | — | — | — | — | — |
|
|
160
|
+
| clean-code-reviewer | — | — | — | — | — |
|
|
161
|
+
| data-intensive-patterns | — | — | — | — | — |
|
|
162
|
+
| data-pipelines | — | — | — | — | — |
|
|
163
|
+
| design-patterns | — | — | — | — | — |
|
|
164
|
+
| domain-driven-design | — | — | — | — | — |
|
|
165
|
+
| effective-java | — | — | — | — | — |
|
|
166
|
+
| effective-kotlin | — | — | — | — | — |
|
|
167
|
+
| effective-python | — | — | — | — | — |
|
|
168
|
+
| effective-typescript | — | — | — | — | — |
|
|
169
|
+
| kotlin-in-action | — | — | — | — | — |
|
|
170
|
+
| lean-startup | — | — | — | — | — |
|
|
171
|
+
| microservices-patterns | — | — | — | — | — |
|
|
172
|
+
| programming-with-rust | — | — | — | — | — |
|
|
173
|
+
| refactoring-ui | — | — | — | — | — |
|
|
174
|
+
| rust-in-action | — | — | — | — | — |
|
|
175
|
+
| skill-router | — | — | — | — | — |
|
|
176
|
+
| spring-boot-in-action | — | — | — | — | — |
|
|
177
|
+
| storytelling-with-data | — | — | — | — | — |
|
|
178
|
+
| system-design-interview | — | — | — | — | — |
|
|
179
|
+
| using-asyncio-python | — | — | — | — | — |
|
|
180
|
+
| web-scraping-python | — | — | — | — | — |
|
|
181
|
+
<!-- quality-table-end -->
|
|
182
|
+
|
|
183
|
+
Results are stored in each skill's `evals/results.json` and updated by running `npx @booklib/skills eval <name>`.
|
|
184
|
+
|
|
185
|
+
## Contributing a skill
|
|
186
|
+
|
|
187
|
+
If you've read a book that belongs here, you can add it. The bar is lower than you think:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# 1. Copy an existing skill as a template
|
|
191
|
+
cp -r skills/clean-code-reviewer skills/your-book-name
|
|
192
|
+
|
|
193
|
+
# 2. Edit SKILL.md, examples/, and evals/
|
|
194
|
+
|
|
195
|
+
# 3. Validate before opening a PR
|
|
196
|
+
npx @booklib/skills check your-book-name
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The `check` command runs all evals and reports what passes and fails — you get a quality signal before anyone else sees the PR. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide.
|
|
200
|
+
|
|
201
|
+
**Books with open issues** (tagged `good first issue`): [The Pragmatic Programmer](https://github.com/booklib-ai/skills/issues/2) · [Clean Architecture](https://github.com/booklib-ai/skills/issues/3) · [A Philosophy of Software Design](https://github.com/booklib-ai/skills/issues/4) · [Accelerate](https://github.com/booklib-ai/skills/issues/8) · [and more →](https://github.com/booklib-ai/skills/issues?q=is%3Aopen+label%3A%22good+first+issue%22)
|
|
202
|
+
|
|
148
203
|
## License
|
|
149
204
|
|
|
150
205
|
MIT
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# How I Route AI Agents to the Right Code Review Context
|
|
2
|
+
|
|
3
|
+
You gave Claude Code a Clean Code checklist. It reviewed your order processing service and told you to rename `proc` to `processOrder` and split a 22-line function into three.
|
|
4
|
+
|
|
5
|
+
Meanwhile, the actual problem — your aggregate boundary is wrong and you're leaking domain logic into the API layer — went completely unnoticed.
|
|
6
|
+
|
|
7
|
+
This isn't an AI failure. It's a routing failure. The agent applied the wrong lens.
|
|
8
|
+
|
|
9
|
+
## The Problem: Context Collapse
|
|
10
|
+
|
|
11
|
+
If you give an AI agent a broad set of review instructions, two things happen:
|
|
12
|
+
|
|
13
|
+
**Token waste** — the agent reads through hundreds of lines of principles that don't apply to the file at hand.
|
|
14
|
+
|
|
15
|
+
**Wrong focus** — a Clean Code reviewer will nitpick naming on a file where the real issue is a broken domain model. A DDD reviewer will talk about bounded contexts on a utility function that just needs cleaner variable names.
|
|
16
|
+
|
|
17
|
+
This is what one Hacker News commenter called context collapse: "Clean Code was written for Java in 2008. DDIA is about distributed systems at scale. If you apply the Clean Code reviewer to a 50-line Python script, you'll get pedantic nonsense about function length when the actual problem might be that the data model is wrong."
|
|
18
|
+
|
|
19
|
+
The criticism is valid. The fix isn't to abandon structured review — it's to pick the right structure for the file in front of you.
|
|
20
|
+
|
|
21
|
+
## The Approach: A Router That Picks the Reviewer
|
|
22
|
+
|
|
23
|
+
I've been building a collection of "skills" — structured instruction sets distilled from classic software engineering books (Clean Code, DDIA, Effective Java, DDD, etc.). Each one is a focused lens that an AI agent uses during code review or code generation.
|
|
24
|
+
|
|
25
|
+
The key piece is a `skill-router`: a meta-skill that runs before any review happens. It inspects:
|
|
26
|
+
|
|
27
|
+
- File type and language — Kotlin? Python? Infrastructure config?
|
|
28
|
+
- Domain signals — is this a service layer? A repository? A controller?
|
|
29
|
+
- Work type — code review, refactoring, greenfield design, or bug fix?
|
|
30
|
+
|
|
31
|
+
Based on that, it selects the 1–2 most relevant skills and explicitly skips the rest.
|
|
32
|
+
|
|
33
|
+
## Example in Practice
|
|
34
|
+
|
|
35
|
+
User: "Review my order processing service"
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Router decision:
|
|
39
|
+
✅ Primary: domain-driven-design — domain model design (Aggregates, Value Objects)
|
|
40
|
+
✅ Secondary: microservices-patterns — service boundaries and inter-service communication
|
|
41
|
+
⛔ Skip: clean-code-reviewer — premature at design stage; apply later on implementation code
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The router doesn't just pick — it explains why it skipped alternatives. That rationale is important: it makes the selection auditable, and you can override it if you disagree.
|
|
45
|
+
|
|
46
|
+
## Why Not Just Use One Giant Prompt?
|
|
47
|
+
|
|
48
|
+
You could stuff everything into one system prompt. I tried. Here's what happens:
|
|
49
|
+
|
|
50
|
+
**Attention dilution** — the model tries to apply everything at once and produces shallow, generic feedback.
|
|
51
|
+
|
|
52
|
+
**Conflicting advice** — Clean Code says "extract small functions." Some microservices patterns say "prefer cohesive, slightly larger functions over deep call stacks." The model hedges between both.
|
|
53
|
+
|
|
54
|
+
**Token budget** — if you're working in Claude Code or Cursor, every token of instructions competes with your actual code context.
|
|
55
|
+
|
|
56
|
+
Routing means the agent reads ~200 focused lines of instructions instead of ~2000 unfocused ones.
|
|
57
|
+
|
|
58
|
+
## The Alternative Criticism: "LLMs Already Know These Books"
|
|
59
|
+
|
|
60
|
+
This is the most common pushback I get. And it's partially true — LLMs have read Clean Code. But they apply that knowledge inconsistently and at low confidence.
|
|
61
|
+
|
|
62
|
+
Giving the model an explicit lens — "review this against Clean Code heuristics C1–C36" — concentrates attention and dramatically reduces hallucinated or off-topic feedback. It's the difference between asking someone "what do you think?" vs. "evaluate this against these specific criteria."
|
|
63
|
+
|
|
64
|
+
Think of it like unit tests: the runtime can execute your code correctly without them. But tests make correctness explicit, repeatable, and auditable. Skills do the same for AI review.
|
|
65
|
+
|
|
66
|
+
## How the Routing Actually Works
|
|
67
|
+
|
|
68
|
+
The router skill is a structured prompt with a decision tree:
|
|
69
|
+
|
|
70
|
+
1. Parse the request — what file(s), what task
|
|
71
|
+
2. Match against skill metadata — each skill declares its applicable languages, domains, and work types
|
|
72
|
+
3. Rank by relevance — primary (strongest match) and secondary (complementary perspective)
|
|
73
|
+
4. Conflict resolution — if two skills would give contradictory advice, prefer the one matching the higher abstraction level of the task
|
|
74
|
+
5. Return selection with rationale
|
|
75
|
+
|
|
76
|
+
There's no ML model or embedding search involved. It's structured prompting — the LLM acts as the routing engine using routing rules baked into the router's own instructions. Language signals, domain signals, and conflict resolution are all declared explicitly inside the router skill, not inferred at runtime. The trade-off: it's fast and predictable, but adding a new skill requires updating the router manually.
|
|
77
|
+
|
|
78
|
+
## Levels of Review (a Pattern Worth Stealing)
|
|
79
|
+
|
|
80
|
+
One of the most useful ideas that came from community feedback: separate your review into levels of critique:
|
|
81
|
+
|
|
82
|
+
1. A fast "lint" pass — formatting, obvious bugs, missing tests
|
|
83
|
+
2. A domain pass — does the code correctly model the business logic?
|
|
84
|
+
3. A "counterexample" pass — propose at least one concrete failing scenario and how to reproduce it
|
|
85
|
+
|
|
86
|
+
The skill library maps roughly to these levels — Clean Code for level 1, DDD for level 2 — but you have to invoke them separately with the right framing. The router picks based on what the code *is*, not which level you're at. Explicit level-based routing isn't built yet. The counterexample pass is harder and something I'm still figuring out.
|
|
87
|
+
|
|
88
|
+
## Try It Yourself
|
|
89
|
+
|
|
90
|
+
The skills and the router are open source: [github.com/booklib-ai/skills](https://github.com/booklib-ai/skills)
|
|
91
|
+
|
|
92
|
+
You can use them with Claude Code, Cursor, or any agent that supports SKILL.md files. The quickest way to try it — install everything and let the router decide:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx @booklib/skills add --all
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Or globally, so it's available in every project:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx @booklib/skills add --all --global
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then just ask your agent to review a file — the router picks the right skill automatically. You don't need to know the library upfront.
|
|
105
|
+
|
|
106
|
+
## Benchmark: Routed Skills vs. Native Review
|
|
107
|
+
|
|
108
|
+
Theory is nice. Does it actually find more issues?
|
|
109
|
+
|
|
110
|
+
I took a deliberately terrible 157-line Node.js order processing module — god function, SQL injection on every query, global mutable state, `eval()` for no reason — and ran it through two pipelines in parallel:
|
|
111
|
+
|
|
112
|
+
- **Native:** Claude's built-in `pr-review-toolkit:code-reviewer`
|
|
113
|
+
- **skill-router:** `skill-router` → `clean-code-reviewer` + `design-patterns`
|
|
114
|
+
|
|
115
|
+
### What the router chose
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Primary: clean-code-reviewer — god function, cryptic names, magic numbers
|
|
119
|
+
Secondary: design-patterns — duplicated payment blocks → Strategy pattern
|
|
120
|
+
Skipped: domain-driven-design — implementation level, not model design stage
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Issue detection
|
|
124
|
+
|
|
125
|
+
| | Native | skill-router |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Critical/High issues | 7 | 8 |
|
|
128
|
+
| Important/Improvement | 10 | 14 |
|
|
129
|
+
| Suggestions | 0 | 5 |
|
|
130
|
+
| **Total unique issues** | **19** | **~28** |
|
|
131
|
+
|
|
132
|
+
~89% of what Claude's native reviewer found, skill-router also found. But skill-router found ~9 additional issues that the native reviewer missed entirely.
|
|
133
|
+
|
|
134
|
+
A few that stood out:
|
|
135
|
+
|
|
136
|
+
> **`formatMoney` has a floating-point rounding bug** — `0.1 + 0.2` arithmetic, not `Math.round`. Native didn't flag it; clean-code-reviewer caught it via the G-series heuristics.
|
|
137
|
+
|
|
138
|
+
> **The stubs always return `true`** — they're lying to callers. Native missed it; clean-code-reviewer flagged it as a lying comment / false contract.
|
|
139
|
+
|
|
140
|
+
> **skill-router surfaced 7 pattern opportunities** — places where a known pattern could reduce complexity (Strategy for payments, State for order lifecycle, Singleton for the broken global state). It explains the problem each one solves and suggests a fix sequence, but leaves the decision to you. Native produced no architectural guidance at all.
|
|
141
|
+
|
|
142
|
+
### Where each approach wins
|
|
143
|
+
|
|
144
|
+
| Situation | Use |
|
|
145
|
+
|---|---|
|
|
146
|
+
| Pre-merge PR review, security audit | **Native** — pre-merge gate: fast, confidence-filtered, adapts to your CLAUDE.md project conventions |
|
|
147
|
+
| Larger refactor, architecture planning | **skill-router** — patterns, principles, refactor roadmap |
|
|
148
|
+
| Both together | ~95% total issue coverage vs. ~80% for either alone |
|
|
149
|
+
|
|
150
|
+
**One honest loss for skill-router:** Card data was being logged to stdout — a clear PCI violation. Claude's built-in reviewer flagged it at 92% confidence. skill-router didn't. Security compliance isn't in any book-based skill's scope, and the router has no way to know it should care. If compliance is the priority, the native reviewer is the right tool.
|
|
151
|
+
|
|
152
|
+
After looking closely at how both tools are built, the difference in purpose becomes clear.
|
|
153
|
+
|
|
154
|
+
The native reviewer runs **6 parallel sub-agents**, each focused on one category: code quality, silent failures, type design, test coverage, comment accuracy, and security. It defaults to reviewing only the current `git diff` — not the whole file. Before starting, it reads your `CLAUDE.md` to pick up project conventions. And it discards any finding below 80% confidence, so output arrives pre-filtered. That's a purpose-built pre-merge gate: narrow scope, parallel specialists, high signal-to-noise.
|
|
155
|
+
|
|
156
|
+
skill-router does the opposite: one agent, one deeply focused skill, applied to the whole module. It trades breadth and speed for depth and principle grounding.
|
|
157
|
+
|
|
158
|
+
They target different moments in the development lifecycle, which is why using both gives ~95% coverage.
|
|
159
|
+
|
|
160
|
+
One gap this benchmark exposed was the noise filtering: Claude's native reviewer discards anything below 80% confidence; skill-router had no equivalent. Since writing this, the router has been updated to instruct selected skills to classify every finding as HIGH / MEDIUM / LOW and skip LOW-tier findings on standard reviews — same idea, book-grounded framing instead of a confidence score.
|
|
161
|
+
|
|
162
|
+
The full before/after code and comparison report are in the repo under [`/benchmark/`](https://github.com/booklib-ai/skills/tree/main/benchmark).
|
|
163
|
+
|
|
164
|
+
## Open Questions
|
|
165
|
+
|
|
166
|
+
I don't have everything figured out. A few things I'm still exploring:
|
|
167
|
+
|
|
168
|
+
**Sub-agent architecture** — the native pr-review-toolkit runs 6 parallel sub-agents (tests, types, silent failures, comments, etc.), each a focused specialist. skill-router takes the opposite approach: one agent, one focused skill, narrow scope. Both work, but for different reasons. The open question is whether a *generate-then-evaluate* loop — one agent produces code using a skill's patterns, a second agent checks it against the same skill's rubric — would catch more issues than a single-pass review. My current answer is no for code review, maybe for code generation. If you've tried this pattern, I'd like to know what you found.
|
|
169
|
+
|
|
170
|
+
**Feedback loops** — the benchmark above is one data point. How do you systematically measure whether routing improves review quality across different codebases and languages?
|
|
171
|
+
|
|
172
|
+
**Domain-specific routing** — healthcare code, fintech code, and game code each have very different "what matters most" priorities. Should routing consider the project domain, not just the file?
|
|
173
|
+
|
|
174
|
+
If you've been working on similar problems — structured AI review, skill selection, multi-agent evaluation — I'd love to hear what's working for you.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
*Currently covering: Clean Code, Domain-Driven Design, Effective Java, Effective Kotlin, Microservices Patterns, System Design Interview, Storytelling with Data, and more. Skills are community-contributed and new books are welcome.*
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// order processing thing
|
|
2
|
+
// ORIGINAL — intentionally bad code. Do not use in production.
|
|
3
|
+
|
|
4
|
+
var db = require('./db')
|
|
5
|
+
var mailer = require('./mailer')
|
|
6
|
+
|
|
7
|
+
var discount = 0.1
|
|
8
|
+
var TAX = 0.23
|
|
9
|
+
var items = []
|
|
10
|
+
var total = 0
|
|
11
|
+
var usr = null
|
|
12
|
+
|
|
13
|
+
function process(o, u, pay, s) {
|
|
14
|
+
usr = u
|
|
15
|
+
if (u != null) {
|
|
16
|
+
if (u.active == true) {
|
|
17
|
+
if (o != null) {
|
|
18
|
+
if (o.items != null && o.items.length > 0) {
|
|
19
|
+
var t = 0
|
|
20
|
+
for (var i = 0; i < o.items.length; i++) {
|
|
21
|
+
var item = o.items[i]
|
|
22
|
+
if (item.qty > 0) {
|
|
23
|
+
if (item.price > 0) {
|
|
24
|
+
t = t + (item.qty * item.price)
|
|
25
|
+
items.push(item)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (u.type == "premium") {
|
|
30
|
+
t = t - (t * 0.1)
|
|
31
|
+
}
|
|
32
|
+
if (u.type == "vip") {
|
|
33
|
+
t = t - (t * 0.2)
|
|
34
|
+
}
|
|
35
|
+
if (u.type == "staff") {
|
|
36
|
+
t = t - (t * 0.5)
|
|
37
|
+
}
|
|
38
|
+
total = t + (t * TAX)
|
|
39
|
+
if (pay == "card") {
|
|
40
|
+
var res = chargeCard(u.card, total)
|
|
41
|
+
if (res == true) {
|
|
42
|
+
var q = "INSERT INTO orders VALUES ('" + o.id + "', '" + u.id + "', " + total + ", 'paid')"
|
|
43
|
+
db.query(q)
|
|
44
|
+
mailer.send(u.email, "Order confirmed", "ur order is confirmed lol total: " + total)
|
|
45
|
+
s.orders++
|
|
46
|
+
s.revenue = s.revenue + total
|
|
47
|
+
return true
|
|
48
|
+
} else {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (pay == "paypal") {
|
|
53
|
+
var res2 = chargePaypal(u.paypal, total)
|
|
54
|
+
if (res2 == true) {
|
|
55
|
+
var q2 = "INSERT INTO orders VALUES ('" + o.id + "', '" + u.id + "', " + total + ", 'paid')"
|
|
56
|
+
db.query(q2)
|
|
57
|
+
mailer.send(u.email, "Order confirmed", "ur order is confirmed lol total: " + total)
|
|
58
|
+
s.orders++
|
|
59
|
+
s.revenue = s.revenue + total
|
|
60
|
+
return true
|
|
61
|
+
} else {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (pay == "crypto") {
|
|
66
|
+
// TODO: implement this someday
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function chargeCard(card, amt) {
|
|
84
|
+
// just assume it works
|
|
85
|
+
console.log("charging card " + card + " for " + amt)
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function chargePaypal(pp, amt) {
|
|
90
|
+
console.log("paypal " + pp + " " + amt)
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// get user orders
|
|
95
|
+
function getOrds(uid) {
|
|
96
|
+
var q = "SELECT * FROM orders WHERE user_id = '" + uid + "'"
|
|
97
|
+
return db.query(q)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// cancel
|
|
101
|
+
function cancel(oid, uid, rsn) {
|
|
102
|
+
var q = "SELECT * FROM orders WHERE id = '" + oid + "'"
|
|
103
|
+
var ord = db.query(q)
|
|
104
|
+
if (ord != null) {
|
|
105
|
+
if (ord.user_id == uid) {
|
|
106
|
+
if (ord.status != "cancelled") {
|
|
107
|
+
if (ord.status != "shipped") {
|
|
108
|
+
if (ord.status != "delivered") {
|
|
109
|
+
var q2 = "UPDATE orders SET status = 'cancelled', reason = '" + rsn + "' WHERE id = '" + oid + "'"
|
|
110
|
+
db.query(q2)
|
|
111
|
+
mailer.send(usr.email, "Cancelled", "ok cancelled")
|
|
112
|
+
return true
|
|
113
|
+
} else {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// stats thing used everywhere
|
|
128
|
+
var stats = {
|
|
129
|
+
orders: 0,
|
|
130
|
+
revenue: 0,
|
|
131
|
+
cancelled: 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getStats() {
|
|
135
|
+
return eval("stats")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// some random util shoved in here
|
|
139
|
+
function formatMoney(n) {
|
|
140
|
+
return "$" + Math.round(n * 100) / 100
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// also does refunds i guess
|
|
144
|
+
function refund(oid) {
|
|
145
|
+
var q = "SELECT * FROM orders WHERE id = '" + oid + "'"
|
|
146
|
+
var ord = db.query(q)
|
|
147
|
+
if (ord.status == "paid") {
|
|
148
|
+
// refund the money somehow
|
|
149
|
+
console.log("refunding " + ord.total)
|
|
150
|
+
var q2 = "UPDATE orders SET status = 'refunded' WHERE id = '" + oid + "'"
|
|
151
|
+
db.query(q2)
|
|
152
|
+
stats.revenue = stats.revenue - ord.total
|
|
153
|
+
stats.cancelled++
|
|
154
|
+
mailer.send(usr.email, "Refund", "u got ur money back: " + formatMoney(ord.total))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { process, getOrds, cancel, getStats, refund }
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// code-after-native.js
|
|
2
|
+
// Rewritten applying all findings from: pr-review-toolkit:code-reviewer
|
|
3
|
+
// Fixes applied (by issue #):
|
|
4
|
+
// #1 Parameterised queries — no SQL injection
|
|
5
|
+
// #2 eval("stats") → return stats
|
|
6
|
+
// #3 No module-level mutable state — all state is local or injected
|
|
7
|
+
// #4 items array removed from module scope
|
|
8
|
+
// #5 null check before ord.status in refund()
|
|
9
|
+
// #6 Card data not logged to console (PCI)
|
|
10
|
+
// #7 Unknown payment method throws, not silently returns undefined
|
|
11
|
+
// #8 Shared finalizeOrder() — no duplicated block
|
|
12
|
+
// #9 discount variable removed; named constants used
|
|
13
|
+
// #10 === / !== throughout, no loose equality
|
|
14
|
+
// #11/#12 cancel/refund look up user email from order, not global usr
|
|
15
|
+
// #13 Single stats object — no two-object inconsistency
|
|
16
|
+
// #14 cancel() updates stats.cancelled on success
|
|
17
|
+
// #15 try/catch around db and mailer calls
|
|
18
|
+
// #16 const/let throughout, no var
|
|
19
|
+
// #17 Descriptive parameter names
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const db = require('./db');
|
|
24
|
+
const mailer = require('./mailer');
|
|
25
|
+
|
|
26
|
+
const TAX_RATE = 0.23;
|
|
27
|
+
const DISCOUNT_PREMIUM = 0.10;
|
|
28
|
+
const DISCOUNT_VIP = 0.20;
|
|
29
|
+
const DISCOUNT_STAFF = 0.50;
|
|
30
|
+
const NON_CANCELLABLE = ['cancelled', 'shipped', 'delivered'];
|
|
31
|
+
|
|
32
|
+
// Module-level stats — single source of truth (#13)
|
|
33
|
+
const stats = { orders: 0, revenue: 0, cancelled: 0 };
|
|
34
|
+
|
|
35
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
// Renamed from process() — avoids shadowing Node's global `process` (#17, #13)
|
|
38
|
+
async function placeOrder(order, user, paymentMethod, statsRef) {
|
|
39
|
+
// Guard clauses replace pyramid nesting (#10)
|
|
40
|
+
if (!user || !user.active) return false;
|
|
41
|
+
if (!order || !order.items || order.items.length === 0) return false;
|
|
42
|
+
|
|
43
|
+
// Local variables — no module-level state (#3, #4)
|
|
44
|
+
const validItems = order.items.filter(item => item.qty > 0 && item.price > 0);
|
|
45
|
+
if (validItems.length === 0) return false;
|
|
46
|
+
|
|
47
|
+
let subtotal = validItems.reduce((sum, item) => sum + item.qty * item.price, 0);
|
|
48
|
+
|
|
49
|
+
// Named constants replace magic numbers (#9)
|
|
50
|
+
if (user.type === 'premium') subtotal *= (1 - DISCOUNT_PREMIUM);
|
|
51
|
+
else if (user.type === 'vip') subtotal *= (1 - DISCOUNT_VIP);
|
|
52
|
+
else if (user.type === 'staff') subtotal *= (1 - DISCOUNT_STAFF);
|
|
53
|
+
|
|
54
|
+
const total = subtotal * (1 + TAX_RATE);
|
|
55
|
+
|
|
56
|
+
// Dispatch — unknown method throws, not silently undefined (#7)
|
|
57
|
+
const charged = await chargePayment(paymentMethod, user, total);
|
|
58
|
+
if (!charged) return false;
|
|
59
|
+
|
|
60
|
+
// Shared confirmation — duplicated block eliminated (#8)
|
|
61
|
+
await finalizeOrder(order, user, total, statsRef || stats);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getOrders(userId) {
|
|
66
|
+
// Parameterised query (#1)
|
|
67
|
+
return db.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function cancelOrder(orderId, userId, reason) {
|
|
71
|
+
try {
|
|
72
|
+
// Parameterised (#1)
|
|
73
|
+
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
|
|
74
|
+
|
|
75
|
+
// Null check (#5); strict equality (#10)
|
|
76
|
+
if (!order) return false;
|
|
77
|
+
if (order.user_id !== userId) return false;
|
|
78
|
+
if (NON_CANCELLABLE.includes(order.status)) return false;
|
|
79
|
+
|
|
80
|
+
await db.query(
|
|
81
|
+
'UPDATE orders SET status = $1, reason = $2 WHERE id = $3',
|
|
82
|
+
['cancelled', reason, orderId],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Use order's own email — not global usr (#11)
|
|
86
|
+
mailer.send(order.user_email, 'Order cancelled', 'Your order has been cancelled.');
|
|
87
|
+
|
|
88
|
+
// Stats update on cancel (#14)
|
|
89
|
+
stats.cancelled++;
|
|
90
|
+
stats.revenue -= order.total;
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Error handling (#15)
|
|
95
|
+
console.error('cancelOrder failed:', err.message);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function refundOrder(orderId) {
|
|
101
|
+
try {
|
|
102
|
+
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
|
|
103
|
+
|
|
104
|
+
// Null check added — was missing, caused TypeError crash (#5)
|
|
105
|
+
if (!order) return false;
|
|
106
|
+
if (order.status !== 'paid') return false;
|
|
107
|
+
|
|
108
|
+
await db.query(
|
|
109
|
+
'UPDATE orders SET status = $1 WHERE id = $2',
|
|
110
|
+
['refunded', orderId],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
stats.revenue -= order.total;
|
|
114
|
+
stats.cancelled++;
|
|
115
|
+
|
|
116
|
+
// Use order's own email — not global usr (#12)
|
|
117
|
+
mailer.send(
|
|
118
|
+
order.user_email,
|
|
119
|
+
'Refund processed',
|
|
120
|
+
`Your refund of ${formatMoney(order.total)} is on its way.`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error('refundOrder failed:', err.message);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getStats() {
|
|
131
|
+
return { ...stats }; // eval("stats") → just return the object (#2); defensive copy (#13)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Private helpers ─────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
async function chargePayment(method, user, total) {
|
|
137
|
+
if (method === 'card') return chargeCard(user.card, total);
|
|
138
|
+
if (method === 'paypal') return chargePaypal(user.paypal, total);
|
|
139
|
+
if (method === 'crypto') throw new Error('Crypto payments are not yet supported');
|
|
140
|
+
// Unknown method throws — no silent undefined return (#7)
|
|
141
|
+
throw new Error(`Unknown payment method: ${method}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Shared — eliminates the duplicated card/paypal block (#8)
|
|
145
|
+
async function finalizeOrder(order, user, total, statsRef) {
|
|
146
|
+
// Parameterised INSERT (#1)
|
|
147
|
+
await db.query(
|
|
148
|
+
'INSERT INTO orders (id, user_id, total, status) VALUES ($1, $2, $3, $4)',
|
|
149
|
+
[order.id, user.id, total, 'paid'],
|
|
150
|
+
);
|
|
151
|
+
mailer.send(
|
|
152
|
+
user.email,
|
|
153
|
+
'Order confirmed',
|
|
154
|
+
`Your order has been confirmed. Total: ${formatMoney(total)}`,
|
|
155
|
+
);
|
|
156
|
+
statsRef.orders++;
|
|
157
|
+
statsRef.revenue += total;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function chargeCard(cardToken, amount) {
|
|
161
|
+
// Do NOT log card details — PCI-DSS (#6)
|
|
162
|
+
console.log(`Charging card (ending ...${String(cardToken).slice(-4)}) for ${formatMoney(amount)}`);
|
|
163
|
+
return true; // TODO: integrate real payment gateway
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function chargePaypal(account, amount) {
|
|
167
|
+
console.log(`Charging PayPal ${account} for ${formatMoney(amount)}`);
|
|
168
|
+
return true; // TODO: integrate PayPal SDK
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatMoney(amount) {
|
|
172
|
+
return `$${(Math.round(amount * 100) / 100).toFixed(2)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
placeOrder,
|
|
177
|
+
getOrders,
|
|
178
|
+
cancelOrder,
|
|
179
|
+
refundOrder,
|
|
180
|
+
getStats,
|
|
181
|
+
};
|