
- Claude Code
- Codex
- Copilot
- +1
API Contract Reviewer
Catches breaking REST changes in a PR before merge: diff vs spec, severity table, migration notes.
14 results

Catches breaking REST changes in a PR before merge: diff vs spec, severity table, migration notes.

An eight-point check for the mistakes that get small apps breached in month one, with PASS/FAIL per item.

Use it to score a pricing page or in-app paywall on eleven criteria and get three ranked fixes with rewritten copy and one experiment to run.
Assumption: no n8n-MCP server is connected in this session, so I took the skill's fallback path: I wrote the workflow JSON directly from the bundled rules and checked it by review (a rule-check script plus a dry run of every expression). The real validator (`validate_workflow`) was not run, and nothing was deployed.
How the skill routed this
| Step | Sub-skill used | What it decided |
|---|---|---|
| Shape | `n8n-workflow-patterns` | Webhook processing pattern: Webhook, then Validate, then Action, then Respond |
| Node parameters | `n8n-node-configuration` | Slack post needs a channel and text. `responseMode: "responseNode"`. `options.responseCode` set on every Respond node. UUID node ids, no placeholder credentials |
| Expressions | `n8n-expression-syntax` | Webhook data lives under `.body`. Use `$('Webhook').item.json.body.*` once past the IF node. `=` prefix on every expression field |
| Error paths | `n8n-error-handling` | Slack gets `onError: "continueErrorOutput"` and its `main[1]` is wired to the 500 responder. `retryOnFail` is on. Bad input is caught upstream and gets a 400 |
| Validation | `n8n-validation-expert` | Runtime-profile checks done by review. Security warnings get fixed (header auth on the webhook) |
```
Webhook ──> Validate Input ──true──> Post to #signups ──success (main[0])──> Respond 200 OK
└──false──> Respond 400 Invalid Input
Post to #signups ──error (main[1])────> Respond 500 Slack Failed
```
Validation result (excerpt)
```
PASS Webhook responseMode is responseNode (Respond nodes control the reply)
PASS Every branch (success and each error) ends at a Respond to Webhook node: no hanging branch
PASS 'Post to #signups': onError=continueErrorOutput AND main[1] wired
PASS 'Post to #signups': retryOnFail with maxTries <= 5 and waitBetweenTries <= 5000
PASS Every {{ }} field starts with = (no 'Expression requires = prefix' error)
PASS Webhook fields read from .body, never $json.name / $json.email / $json.plan
…
43 passed, 0 failed
```
Expression dry run against sample webhook items:
```
valid IF -> true (Slack) Slack text: New signup: Ana Lima (ana@example.com) on pro
missing_plan IF -> false (400)
bad_email IF -> false (400)
no_body IF -> false (400)
500 body: {"error":"upstream_error","message":"The signup was received but could not be posted to Slack. Please retry later."}
```
Not verified without the server: the node typeVersions (Webhook 2.2, IF 2.3, Slack 2.3, Respond 1.5) and the Slack v2 parameter names; confirm by opening the nodes after import.
[The full answer also holds the six-node list, the importable workflow JSON and the expressions table with the reason behind each one.]Use it to have your coding agent build, configure and validate n8n workflows without the usual expression and node-config mistakes.
Phase 1: Root cause investigation
Full suite: FAIL test_expired_coupon_rejected_twice (tests/test_checkout.py:16) AssertionError: Coupon(code='OLD50', …expires_at=2020-01-01…) is not None.
In isolation the same test fails at a different line (17): AssertionError: 50.0 != 100.0. So the first lookup in a process is rejected and every later lookup is accepted; in the full suite the first lookup was done by test_expired_coupon_rejected. Cross-test pollution through module-level state.
Cache probe at the boundary: cache before [] → 1st None → cache after ['OLD50'] → 2nd Coupon(OLD50). Docstring says the cache was added in 3f2a1c9 "to cut DB reads": that is the regression point.
Trace (root-cause-tracing.md): apply_coupon → get_coupon → `if code in _CACHE: return _CACHE[code]`. get_coupon writes the coupon to _CACHE before the expiry check, and the cache-hit branch has no expiry check at all.
Phase 2: Pattern analysis
Working path: cache miss → _load → expiry check. Broken path: cache hit. Expiry depends on `now`, passed per call, so it cannot be a cached property; "don't cache expired coupons" alone would still serve a coupon cached while valid after it expires.
Phase 3: Hypothesis
"get_coupon returns cached coupons without re-checking expires_at against the caller's now; the cache was added around the lookup but the validity check stayed only on the miss path." Confirmed by the probe above.
Phase 4: Implementation
Failing test first: test_cached_coupon_rechecked_for_expiry (look up while valid, then after expiry) plus a setUp that clears the module cache. Before the fix: 5 tests, 2 failures, both for the expected reason.
Fix at the source: the cache stores the raw record; the expiry check runs on every lookup, cached or not.
c = _CACHE.get(code)
if c is None:
c = _load(code)
if c is None:
return None
_CACHE[code] = c
if c.expires_at < now:
return None
return c
Verification: python3 -m unittest -v → Ran 5 tests … OK. The originally failing test in isolation, 3 runs: OK, OK, OK. Fix attempts: 1.Stops your coding agent from guessing: root cause first, one hypothesis at a time, failing test before the fix.

Idea to approved design to test-first plan to working code, with an approval gate between each stage.
Scope: app/dashboard/page.tsx and the modules it imports (components/charts, UsageCard, ProjectList, RichTextEditor, lib/api.ts).
| # | Rule | File:line | Impact | Finding |
|---|---|---|---|---|
| 1 | async-dependencies | app/dashboard/page.tsx:8-10 | CRITICAL | getProjects(user.id) and getUsage(user.id) depend only on `user` but are awaited one after the other: 3 sequential round trips before any HTML is sent, where 2 would do. Promise.all them. |
| 2 | bundle-barrel-imports | components/ProjectList.tsx:3 | CRITICAL | `import _ from 'lodash'` in a 'use client' file ships all of lodash for one sortBy. Use lodash/sortBy, or toSorted / localeCompare. |
| 3 | async-suspense-boundaries | app/dashboard/page.tsx:7-19 | HIGH | The whole page, including the static shell and RichTextEditor, waits for all three fetches. Move usage and projects into async children in <Suspense> so the shell streams first. |
| 4 | server-serialization | app/dashboard/page.tsx:16 | HIGH | The raw projects array goes to client ProjectList, which reads only id, name and status. Map to {id, name, status} on the server. |
| 5 | rerender-no-inline-components | components/ProjectList.tsx:7 | HIGH | `Row` is defined inside ProjectList, so every keystroke in the filter remounts every <li>. Interaction cost, not first load. Hoist Row. |
| 6 | rerender-derived-state-no-effect | components/UsageCard.tsx:4-5 | MEDIUM | percent is kept in state and set in useEffect: server HTML renders "0% of plan used" and the real value appears only after hydration. Compute it during render. |
Score: 62 / 100 (100 − 2×10 CRITICAL − 3×5 HIGH − 1×3 MEDIUM). This counts rule findings; it is not a measured load time.
Checked and not scored: the local charts barrel (five tiny stubs, tree-shakeable); RichTextEditor is a <textarea> today, so no dynamic import yet. Not a performance rule but it blocks the build: components/charts/index.ts re-exports names the chart files do not export, and page.tsx reads `params.id` synchronously though params is a Promise in Next 15.
What makes the first load slow, in order:
1. The server waterfall (rows 1 and 3). Parallelizing projects and usage removes one full round trip; Suspense lets the header paint after only getUser.
2. Client JS weight (row 2). A one-line import change.
3. Payload and first-paint correctness (rows 4 and 6).
Row 5 is an interaction cost; fix it after the others. Fixes were not requested, so no files were changed.70 prioritized performance rules for React and Next.js that your agent applies while coding and scores in review.

Audit a live page that isn't converting: a 7-point scorecard, quick wins, test ideas and before/after copy.

24 named design moves for your coding agent: bolder, quieter, typeset, distill, polish, plus a heuristic critique and a technical audit.

Your agent interviews you about a plan until nothing is assumed, then writes the spec and tracer-bullet tickets.

One-line brief in, full design system out: page pattern, style, colour tokens, font pairing and anti-patterns from a local database.

Strips AI tells from a draft without inventing a new voice: quoted flags, a clean rewrite, a second-pass check.
Short answer: ChatGPT can't recommend Ledgerly, because nothing it can retrieve says Ledgerly exists. Even after launch, a single landing page is not enough. A "best X for Y" shortlist comes from web-wide consensus (reviews, roundups, forums, press), not from your own site.
Visibility ladder (retrieved → cited → mentioned → recommended)
| Rung | Ledgerly today | What governs it |
|---|---|---|
| 1. Retrieved | No: not live, not in Bing's index (ChatGPT search draws from a Bing-based index) | Being indexed and crawlable |
| 2. Cited | No | Extractable, factual, fresh pages |
| 3. Mentioned | No | Entity recognition; the web talking about you |
| 4. Recommended | No | Aggregate third-party consensus, largely independent of your own content |
Source pool (one web search for the exact query, 2026-11-07; not a ChatGPT answer): only third-party roundups and affiliate lists (NerdWallet, business.org, Zapier, Fueler, Techjockey, Aspire). The same five apps recur: Wave, Zoho Books, FreshBooks, QuickBooks, Xero. I did not run ChatGPT, so I'm not reporting what ChatGPT says; run the query list yourself.
Extractability check on site/index.html: clear definition in the first paragraph, partial (the H1 is a slogan); self-contained answer blocks, fail; statistics with sources, fail; comparison table for "vs" queries, fail; FAQ, partial (good answers hidden in collapsed <details>, none answers "Is Ledgerly good for freelancers?"); schema markup, fail; author or founder, fail; freshness date, fail; query-shaped headings, fail.
Top fixes, by impact against effort
1. Launch, then get indexed in Bing and Google. robots.txt with a wildcard allow plus explicit allows for OAI-SearchBot, PerplexityBot, Claude-SearchBot and Bingbot; decide training crawlers (GPTBot, ClaudeBot) separately.
2. Seed third-party consensus: G2 and Capterra listings with the first 20–30 real reviews; pitch the roundups that already rank for the query with a specific angle ("$12/month, no per-transaction fees, automatic invoice chasing"). Test: if a model ignored your own domain, would the web still put you on the list? Today, no.
3. Rewrite the hero into an extractable definition (below).
4. Add /vs/freshbooks, then QuickBooks and Wave, in the "fair source" pattern. Do not publish a self-ranked "best bookkeeping apps" listicle: in the skill's data 69% of such citations recommended competitors.
5. Publish /pricing.md and /llms.txt; fix the Studio button ("Talk to us" on a plan with a public price reads as a sales wall to an agent).
6. JSON-LD: Organization, SoftwareApplication with Offer per plan, FAQPage.
7. Buyer-intent FAQs rendered open in the HTML. 8. Named founder, "Last updated", one dated sourced number. 9. Measure monthly: 8 queries, 3–5 runs per engine, log rung and framing.
Hero answer block (57 words, every fact from the existing page):
> Ledgerly is a bookkeeping app for freelancers and two-person studios. It connects to your bank through Plaid or TrueLayer (US, UK, EU), auto-categorises each transaction by category and client, chases unpaid invoices on day 3, 7 and 14, and exports tax-ready P&L, VAT and quarterly estimates. Free plan available; Pro is $12/month with no per-transaction fees. […]Get cited in AI Overviews, ChatGPT and Perplexity: visibility audit, crawler check, ranked fixes, answer-block rewrites.

Turns the commits since your last tag into user-facing release notes, grouped and in your voice.