skilldrop-cli 0.16.20 → 0.16.22
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/bin/skilldrop.js +5 -3
- package/catalogue.json +1 -0
- package/model-routing.json +6 -2
- package/package.json +1 -1
- package/packs/career/skills/role-shortlist/SKILL.md +23 -12
- package/packs/career/skills/role-shortlist/evals/evals.json +30 -0
- package/packs/career/skills/role-shortlist/manifest.json +1 -1
- package/packs/solution-architect/skills/interactive-diagram/SKILL.md +132 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/eval_queries.json +12 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/evals.json +74 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/README.md +11 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/api/app.py +43 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/docker-compose.yml +20 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/web/checkout.js +9 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/1/worker/worker.py +23 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/3/schema.sql +35 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/README.md +26 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/config/default.json +29 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/clients/transcription.js +25 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/db.js +16 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/handlers/storageWebhook.js +43 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/jobs/transcribeEpisode.js +28 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/mailer.js +18 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/server.js +32 -0
- package/packs/solution-architect/skills/interactive-diagram/evals/files/4/src/worker.js +32 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.html +186 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.json +48 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/checkout-map.md +31 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/api/checkout.py +41 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/docker-compose.yml +20 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/inputs/checkout/worker/fulfil.py +23 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.html +186 -0
- package/packs/solution-architect/skills/interactive-diagram/examples/leave-approval.json +28 -0
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +39 -0
- package/packs/solution-architect/skills/interactive-diagram/reference.md +151 -0
- package/packs/solution-architect/skills/interactive-diagram/scripts/build_diagram.py +1682 -0
- package/packs/solution-architect/skills/reverse-architecture/evals/files/2/worker-requirements.txt +1 -1
package/bin/skilldrop.js
CHANGED
|
@@ -485,14 +485,16 @@ function scanSkill(cat, s) {
|
|
|
485
485
|
try { body = fs.readFileSync(file, "utf8"); } catch (e) { continue; }
|
|
486
486
|
const rules = isScript ? SCRIPT_RULES : PROSE_RULES;
|
|
487
487
|
// Eval fixtures are benchmark input: a sample repo, a risky script for a reviewer to catch.
|
|
488
|
-
//
|
|
489
|
-
|
|
488
|
+
// Example inputs are the material a worked example was made from. The skill never runs or
|
|
489
|
+
// follows either, so they are listed, not raised.
|
|
490
|
+
const relPosix = rel.split(path.sep).join("/");
|
|
491
|
+
const fixture = relPosix.startsWith("evals/files/") || relPosix.startsWith("examples/inputs/");
|
|
490
492
|
body.split(/\r?\n/).forEach((line, i) => {
|
|
491
493
|
if (line.length > 400) return; // minified/data line — not reviewable prose or code
|
|
492
494
|
for (const r of rules) {
|
|
493
495
|
if (r.re.test(line))
|
|
494
496
|
findings.push({ sev: fixture ? "⚪" : r.sev, id: r.id, fixture,
|
|
495
|
-
why: fixture ? `in an eval fixture, which the skill never runs: ${r.why}` : r.why,
|
|
497
|
+
why: fixture ? `in ${relPosix.startsWith("examples/") ? "an example's input" : "an eval fixture"}, which the skill never runs: ${r.why}` : r.why,
|
|
496
498
|
owasp: r.owasp, file: rel, line: i + 1, text: line.trim().slice(0, 120) });
|
|
497
499
|
}
|
|
498
500
|
});
|
package/catalogue.json
CHANGED
package/model-routing.json
CHANGED
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"claude-code": {
|
|
21
21
|
"label": "Claude Code (Anthropic)",
|
|
22
22
|
"models": {
|
|
23
|
-
"light": "claude-haiku-
|
|
23
|
+
"light": "claude-haiku-5-5",
|
|
24
24
|
"standard": "claude-sonnet-5",
|
|
25
|
-
"heavy": "claude-opus-5"
|
|
25
|
+
"heavy": "claude-opus-5-5"
|
|
26
26
|
},
|
|
27
27
|
"select_via": "`/model <model-id>` in the session, or hand the skill to the model-router agent for automatic dispatch.",
|
|
28
28
|
"automation": ".claude/agents/model-router.md"
|
|
@@ -278,6 +278,10 @@
|
|
|
278
278
|
"tier": "standard",
|
|
279
279
|
"rationale": "Structured synthesis from a content list into groups, labels and a nav model against fixed IA practice. Grouping needs judgment, but the method and the validation plan bound it."
|
|
280
280
|
},
|
|
281
|
+
"interactive-diagram": {
|
|
282
|
+
"tier": "standard",
|
|
283
|
+
"rationale": "Tracing a path through code and citing each part is careful reading, not adversarial judgement; the layout and the checks are done by the script."
|
|
284
|
+
},
|
|
281
285
|
"job-fit-analysis": {
|
|
282
286
|
"tier": "standard",
|
|
283
287
|
"rationale": "Matches a posting's requirements to resume lines and reports the gaps. The rule is mechanical: no line in the resume, no credit."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.22",
|
|
4
4
|
"description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
|
@@ -19,7 +19,8 @@ Two things follow from the rule:
|
|
|
19
19
|
registration, a degree the posting says is required, a minimum number of years, the right
|
|
20
20
|
to work in a country, a location or shift, a clearance, a language. One of those unmet
|
|
21
21
|
outweighs everything else that fits, so the role goes under **Skip** with that requirement
|
|
22
|
-
named, however good the rest looks.
|
|
22
|
+
named, however good the rest looks. Only the posting decides what is required: an item it
|
|
23
|
+
calls preferred, desirable, a plus or nice to have never takes a role out.
|
|
23
24
|
- **One role is one role.** The same job posted in three cities, or reposted with a new
|
|
24
25
|
reference number, is one entry with its locations listed. A posting marked closed, or past
|
|
25
26
|
its closing date, is noted and not ranked.
|
|
@@ -37,9 +38,12 @@ Two things follow from the rule:
|
|
|
37
38
|
the kind of role they want.
|
|
38
39
|
|
|
39
40
|
3. **Check each posting for what could screen them out first.** For every role, find the
|
|
40
|
-
requirements
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
requirements the posting makes a condition: what it calls required, essential or a must,
|
|
42
|
+
and the facts of the job itself (where it is, the hours or shift, a licence the work needs).
|
|
43
|
+
Mark whether the person's material meets them. Any unmet one sends the role to **Skip**,
|
|
44
|
+
named in a few words: "requires HCPC registration". An item the posting calls preferred,
|
|
45
|
+
desirable or a plus is not one of these, even a degree or a licence; it is a gap to weigh in
|
|
46
|
+
step 4, and the role stays in.
|
|
43
47
|
|
|
44
48
|
4. **Rank the rest from the posting's own requirements.** For each remaining role, compare
|
|
45
49
|
the required and preferred items with the person's evidence, and say in one line why it
|
|
@@ -48,7 +52,9 @@ Two things follow from the rule:
|
|
|
48
52
|
requires is met only if the person's material names it.
|
|
49
53
|
|
|
50
54
|
5. **Give the shortlist, answer first.** Open with the two or three roles worth applying for
|
|
51
|
-
and why, in a sentence or two. Then
|
|
55
|
+
and why, in a sentence or two. Then the roles under these groups, using only the groups that
|
|
56
|
+
have a role in them. A heading with nothing under it, or with "none", is not shown: when every
|
|
57
|
+
posting was read and none is closed, there is no Not read or Closed group.
|
|
52
58
|
- **Apply**: roles where nothing screening is unmet and most of what's required is met.
|
|
53
59
|
- **Maybe**: roles worth applying for if one named question has the right answer ("if your
|
|
54
60
|
forklift licence is current").
|
|
@@ -63,24 +69,27 @@ Two things follow from the rule:
|
|
|
63
69
|
the one or two questions whose answers would move a role between groups, and which unread
|
|
64
70
|
postings to fetch.
|
|
65
71
|
|
|
66
|
-
7. **Fit the size and format they asked for.** "Just the top
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
7. **Fit the size and format they asked for.** "Just the top two" gets two, each with its
|
|
73
|
+
reason and anything to check first, then the rest in a line or two: which were ruled out and
|
|
74
|
+
by what, and any that couldn't be read. No groups and no headings; the person asked for two
|
|
75
|
+
names, not a sorted list. A list for a spreadsheet gets one row per role. The whole answer
|
|
76
|
+
is in the reply unless they ask for a file.
|
|
69
77
|
|
|
70
78
|
8. **Deliver from what you have.** With postings and a few lines about the person, shortlist
|
|
71
|
-
against those and say a resume would settle the rest.
|
|
72
|
-
|
|
79
|
+
against those and say a resume would settle the rest. When each role is only a line or two,
|
|
80
|
+
say the picks rest on those lines and the full postings come next. With a resume and no
|
|
81
|
+
postings, ask for the postings in one line.
|
|
73
82
|
|
|
74
83
|
## Quality bar
|
|
75
84
|
|
|
76
85
|
- **No role is ranked without its posting text having been read.** Unread roles are listed as
|
|
77
86
|
unread, with no verdict.
|
|
78
87
|
- **A role with an unmet screening requirement is never under Apply,** and the requirement is
|
|
79
|
-
named.
|
|
88
|
+
named. An item the posting calls preferred is never treated as one.
|
|
80
89
|
- **Every role given appears exactly once;** duplicates are merged, closed ones are noted.
|
|
81
90
|
- **Every "fits" rests on the person's own material,** and a similar thing is not counted as
|
|
82
91
|
the named one.
|
|
83
|
-
- **The answer opens the reply,** and the size is the one asked for.
|
|
92
|
+
- **The answer opens the reply,** and the size is the one asked for, with no empty groups.
|
|
84
93
|
|
|
85
94
|
## When to use this skill
|
|
86
95
|
|
|
@@ -99,6 +108,8 @@ Two things follow from the rule:
|
|
|
99
108
|
- ❌ **Ranking from titles.** "Operations Lead sounds like a good fit" with the posting unread.
|
|
100
109
|
- ❌ **Guessing an unread posting** from what the employer or the job title usually means.
|
|
101
110
|
- ❌ **A strong fit over an unmet licence.** Screening requirements decide first.
|
|
111
|
+
- ❌ **A preferred item as a requirement.** "Degree preferred" is a gap to weigh, not a reason to skip.
|
|
112
|
+
- ❌ **Empty groups.** A "Not read: none" heading, or the full five groups for a top-two answer.
|
|
102
113
|
- ❌ **Counting a duplicate twice,** or ranking a closed posting.
|
|
103
114
|
- ❌ **Dropping roles silently.** A list that covers 9 of 12 postings without saying so.
|
|
104
115
|
- ❌ **Crediting the usual.** "As a nurse you'll have used Epic" is an invention.
|
|
@@ -70,6 +70,36 @@
|
|
|
70
70
|
"It points out that the Lantern Room wants someone to start as soon as possible while the person can't start until after 15 November, as something to raise or weigh, rather than treating the start date as fine",
|
|
71
71
|
"It does not credit the person with anything they didn't say, such as a car or driving licence, a level 3 food safety certificate, supervisory or chef de partie experience, or more than three years in kitchens, and it does not claim to know what the Brindle Café posting says"
|
|
72
72
|
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": 6,
|
|
76
|
+
"held_out": "2026-10-18",
|
|
77
|
+
"prompt": "which 2 of these should i go for? bit about me: 3 yrs as a nursery assistant at a day nursery, level 3 childcare diploma, enhanced DBS. i dont drive. want term time / school hours, no weekends, 30ish hrs a week or more. based in Harwick.\n\n- Teaching Assistant, Elmfield Primary, Harwick - Year 2 reading and small groups, term time 8:30-3:30. Level 3 childcare or TA qualification required. Phonics experience a plus.\n- SEN Teaching Assistant, Brookside Academy, Harwick - 1:1 support for a Reception pupil with autism, term time 8:45-3:15. Must have experience in an early years or school setting. Makaton desirable.\n- Nursery Practitioner, Little Acorns, Harwick - full time, 7:30-6 shifts, working alternate Saturdays is a requirement. Level 3 required.\n- Forest School Assistant, Greenway Primary - outdoor learning across 3 sites, term time. Full driving licence and own vehicle essential.\n- Midday Supervisor, Harwick Junior School - 11:45-1:15 term time, lunchtime play and dinner hall. No experience needed.\n- Teaching Assistant, Ashby Lane Primary, Harwick - Year 4 maths and reading groups, term time 8:30-3:30, permanent. Previous TA experience in KS2 essential.",
|
|
78
|
+
"assertions": [
|
|
79
|
+
"Exactly two roles are given as picks, and they are the Elmfield Primary Teaching Assistant and the Brookside Academy SEN Teaching Assistant; no third role is offered as a pick",
|
|
80
|
+
"The Little Acorns Nursery Practitioner role is named as out because working alternate Saturdays is a requirement and she said no weekends, even though she meets its Level 3 requirement",
|
|
81
|
+
"The Greenway Primary Forest School Assistant role is named as out because a full driving licence and own vehicle are essential and she does not drive",
|
|
82
|
+
"The Ashby Lane Primary Teaching Assistant role is named as out or not met because previous KS2 TA experience is essential and her experience is as a nursery assistant, not a KS2 teaching assistant",
|
|
83
|
+
"Neither pick is ruled out or dropped over a preferred item: Makaton (desirable) at Brookside and phonics experience (a plus) at Elmfield may be noted as gaps or unknowns, but not as reasons she cannot apply",
|
|
84
|
+
"The Harwick Junior School Midday Supervisor role is not a pick, and the reason given is its hours (about 1.5 hours a day) against the 30-ish hours a week she asked for, not a missing qualification",
|
|
85
|
+
"The reply is short and contains no heading, section or line for unread, closed or duplicate roles (no 'Unread: none', 'Duplicates: none' or similar placeholder)",
|
|
86
|
+
"It says it judged from the short blurbs rather than full postings, does not say or assume she has phonics experience, Makaton, a driving licence or KS2 experience, and gives no percentage, score or match rating for any role"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": 7,
|
|
91
|
+
"held_out": "2026-10-18",
|
|
92
|
+
"prompt": "I would be grateful for your advice on which one of the following vacancies I should prioritise. I hold a BSc (Hons) in Biomedical Science, awarded with a 2:1 in 2025, and completed a twelve-month placement year in a hospital microbiology laboratory before my final year. I am not yet HCPC registered. I live in Corlow, am seeking a permanent position, and owing to caring responsibilities I am able to work day shifts only, not nights.\n\n1. Biomedical Scientist (Band 5), Corlow General Hospital, Microbiology. Permanent; days with a weekend rota. HCPC registration as a Biomedical Scientist is essential.\n2. Laboratory Technician, Fenwick Water Services, Corlow. Permanent; Monday to Friday, days. Microbiological testing of drinking water samples. A degree in a biological science is required; experience in a UKAS ISO 17025-accredited laboratory is desirable.\n3. Medical Laboratory Assistant, Corlow General Hospital, Blood Sciences. Permanent. Sample reception and processing; a rotating shift pattern including nights is a requirement of the post.\n4. QC Microbiology Analyst, Treloar Pharmaceuticals, Corlow. Permanent; days. Environmental monitoring in a GMP facility. A minimum of two years' post-graduate experience in a GMP quality control laboratory is essential.\n5. Research Technician, University of Corlow, Cell Biology. Fixed-term contract of six months; days. Tissue culture support. A degree in a life science is required.",
|
|
93
|
+
"assertions": [
|
|
94
|
+
"Exactly one role is given as the recommendation, and it is the Laboratory Technician post at Fenwick Water Services",
|
|
95
|
+
"The Fenwick Water Services post is not ruled out or demoted because ISO 17025-accredited laboratory experience is not shown; that item is treated as desirable and may be noted as a gap or something to address, not as a bar",
|
|
96
|
+
"The Corlow General Hospital Biomedical Scientist post is named as out because HCPC registration is essential and she is not yet registered, even though it matches her microbiology placement",
|
|
97
|
+
"The Corlow General Hospital Medical Laboratory Assistant post is named as out because a rotating pattern including nights is required and she can work days only",
|
|
98
|
+
"The Treloar Pharmaceuticals QC Microbiology Analyst post is named as out because two years' post-graduate GMP QC experience is essential; the reply does not count her pre-final-year placement as post-graduate GMP experience",
|
|
99
|
+
"The University of Corlow Research Technician post is not the recommendation, and the reason given is that it is a six-month fixed-term contract when she asked for a permanent position",
|
|
100
|
+
"The reply is concise and contains no heading, section or line for unread, closed or duplicate roles (no 'Unread: none', 'Duplicates: none' or similar placeholder)",
|
|
101
|
+
"It says it worked from brief summaries rather than full postings, does not say or assume she has ISO 17025, GMP or accredited-laboratory experience, and gives no percentage, score or match rating for any role"
|
|
102
|
+
]
|
|
73
103
|
}
|
|
74
104
|
]
|
|
75
105
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "role-shortlist",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Read every open role on an employer's careers page, a job-board export or a folder of saved postings, and say which ones one person should apply for — each role judged only from its own posting text, roles that couldn't be read listed as unread instead of guessed from their titles, a role whose screening requirement the person doesn't meet named as out whatever else fits, and the same role posted twice counted once. Works for any occupation. Use when the user asks \"which of these jobs should I apply for\", \"go through this company's openings\", \"shortlist these postings against my CV\", or has more postings than they want to check one by one.",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: interactive-diagram
|
|
3
|
+
description: Turn a codebase, a system description, a process or a database schema into one self-contained interactive HTML diagram (architecture, workflow, data flow, lifecycle, ERD or sequence) where every box and arrow cites the file and line or the words it came from, anything inferred is drawn dashed as an assumption, and each cited line is checked against the repository before the page is written. Laid out automatically, opens offline with no CDN, and exports SVG, PNG and Mermaid. Use when the user asks to "visualize", "map" or "make an interactive diagram of" a repo, a request path, a pipeline, an approval process or a set of tables, or wants a diagram they can click through and share as one file.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# interactive-diagram
|
|
7
|
+
|
|
8
|
+
**The rule: nothing goes on the map that the material doesn't show.** A diagram is trusted
|
|
9
|
+
more than prose, so an invented box misleads more than an invented sentence. Asked to draw a
|
|
10
|
+
repo, the agent's habit is to draw the system such repos usually have: a load balancer, an
|
|
11
|
+
auth service, the Kafka the README says is planned. Here every part and every connection
|
|
12
|
+
either cites where it came from (`src/api.py:40-52`, or the person's own words) or is drawn
|
|
13
|
+
dashed and listed as an assumption. The script checks each cited file and line before it
|
|
14
|
+
writes the page, and refuses a spec that cites a line that isn't there.
|
|
15
|
+
|
|
16
|
+
Two things follow:
|
|
17
|
+
|
|
18
|
+
- **One question per map.** Draw what answers the question asked ("what happens when an order
|
|
19
|
+
is placed"), not every module in the repo. A part that doesn't help answer it stays off,
|
|
20
|
+
and the reply says in one line what was left out.
|
|
21
|
+
- **You write the meaning; the script does the layout.** Never write coordinates. List the
|
|
22
|
+
parts and connections in the order a reader meets them, and the script places them, routes
|
|
23
|
+
every arrow around the boxes, and draws groups as boxes that don't overlap.
|
|
24
|
+
|
|
25
|
+
## How to respond
|
|
26
|
+
|
|
27
|
+
1. **Name the question and pick the type.** One sentence: what will the reader understand
|
|
28
|
+
after looking? Then the type:
|
|
29
|
+
|
|
30
|
+
| Type | For |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `architecture` | What a system is made of and what talks to what |
|
|
33
|
+
| `dataflow` | Where data or money or documents go, stage by stage |
|
|
34
|
+
| `workflow` | A process with steps, decisions and hand-offs, technical or everyday |
|
|
35
|
+
| `lifecycle` | The states something moves through and what moves it |
|
|
36
|
+
| `erd` | Tables, their columns and keys, and how they reference each other |
|
|
37
|
+
| `sequence` | Who calls whom, in order, for one request or exchange |
|
|
38
|
+
|
|
39
|
+
2. **Gather the facts, with where each came from.**
|
|
40
|
+
- **A repository:** trace the one path the question is about. Read the entry point, follow
|
|
41
|
+
the calls, and for each part and each arrow note the file and lines that show it. Add a
|
|
42
|
+
few characters from those lines after `#` (`src/api.py:12#stripe.PaymentIntent`) so a
|
|
43
|
+
wrong line number is caught. Config and README files show intent, not behaviour: a
|
|
44
|
+
service the README mentions but no code calls is left off, or drawn as an assumption with
|
|
45
|
+
the README as its only evidence and said so in the reply.
|
|
46
|
+
- **A description:** cite the person's words: `"user: anything over ten days goes to HR"`.
|
|
47
|
+
- **Something you believe but can't point to** (who calls the API, what the worker does
|
|
48
|
+
next): `"assumed": true`. It is drawn dashed and listed. Leave it off if it doesn't help
|
|
49
|
+
answer the question.
|
|
50
|
+
|
|
51
|
+
3. **Write the spec** as JSON next to where the page will go. The full shape, every kind and
|
|
52
|
+
the ERD and sequence fields are in [`reference.md`](reference.md). The essentials:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"type": "architecture", "title": "What a checkout request touches",
|
|
56
|
+
"question": "What happens when a shopper checks out?",
|
|
57
|
+
"summary": "One or two sentences: the takeaway a reader should leave with.",
|
|
58
|
+
"groups": [{"id": "vpc", "label": "Private VPC"}],
|
|
59
|
+
"nodes": [{"id": "api", "label": "Checkout API", "kind": "service", "group": "vpc",
|
|
60
|
+
"note": "FastAPI", "evidence": ["src/api.py:8-14#def checkout"]}],
|
|
61
|
+
"edges": [{"from": "api", "to": "db", "label": "insert order", "evidence": ["src/api.py:13"]}],
|
|
62
|
+
"story": [{"text": "The API checks the cache first.", "edges": ["e2"]}]}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Labels are a few words; detail goes in `note` (shown on the box) or `detail` (shown when
|
|
66
|
+
clicked). Add a `story` of three to seven steps when the map has a main path worth walking.
|
|
67
|
+
|
|
68
|
+
4. **Build it.** With a repository, always pass `--repo-root` so the citations are checked:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Claude Code
|
|
72
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/build_diagram.py" diagram.json -o diagram.html --repo-root .
|
|
73
|
+
# Other IDEs (from the skill folder)
|
|
74
|
+
python3 scripts/build_diagram.py diagram.json -o diagram.html --repo-root .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Errors list the item, what is wrong and the fix, and nothing is written until they're gone.
|
|
78
|
+
Fix the spec, never the evidence: a citation the check can't find is wrong, so find the
|
|
79
|
+
right lines or mark the item assumed. Add `--mermaid diagram.mmd` for a version to paste
|
|
80
|
+
into a README, and `--compare old.json` to show what a change adds, removes and alters.
|
|
81
|
+
|
|
82
|
+
5. **Reply with the map first.** The first line is the link to the HTML file and what it
|
|
83
|
+
shows. Then the takeaway in one or two sentences, then one line each for what is drawn as
|
|
84
|
+
an assumption and what was left out, and last how many citations were checked. Keep the
|
|
85
|
+
spec beside the page so the map can be rebuilt after the code changes. No recap of
|
|
86
|
+
commands or layout.
|
|
87
|
+
|
|
88
|
+
6. **When the script can't run** (no `python3`), give the Mermaid version in the reply, with
|
|
89
|
+
assumptions marked, and say that the interactive page needs Python 3.
|
|
90
|
+
|
|
91
|
+
This is a non-interactive skill: don't stop to ask. With too little to draw from, draw what
|
|
92
|
+
you have, mark the rest as assumptions, and say in the reply what would settle them.
|
|
93
|
+
|
|
94
|
+
**Sharing:** the page shows the cited lines when a part is clicked. Files that hold secrets
|
|
95
|
+
(`.env`, keys, credential files) are never shown and secret-looking lines are masked, but
|
|
96
|
+
don't cite them in the first place, and read the page before it leaves the team.
|
|
97
|
+
|
|
98
|
+
## Quality bar
|
|
99
|
+
|
|
100
|
+
- **Every part and connection cites its source or is drawn as an assumption.** No exceptions
|
|
101
|
+
for the "obvious" ones.
|
|
102
|
+
- **Every file citation was checked** against the repository, with the count in the reply.
|
|
103
|
+
- **Nothing on the map comes only from a README, a plan or what such systems usually have.**
|
|
104
|
+
- **The map answers one stated question,** and what was left out is said.
|
|
105
|
+
- **One HTML file that works offline:** no CDN, no external fonts, no network requests.
|
|
106
|
+
- **The reply opens with the link and the takeaway,** not with how it was made.
|
|
107
|
+
|
|
108
|
+
## When to use this skill
|
|
109
|
+
|
|
110
|
+
- ✅ "Map what happens when an order is placed in this repo, as something I can click through"
|
|
111
|
+
- ✅ "Turn this description of our approval process into an interactive flowchart for the team"
|
|
112
|
+
- ✅ "Give me an interactive ERD of schema.sql"
|
|
113
|
+
- ✅ "Show what this branch changes in the architecture" (two specs and `--compare`)
|
|
114
|
+
|
|
115
|
+
## When NOT to use this skill
|
|
116
|
+
|
|
117
|
+
- ❌ A Mermaid, PlantUML or C4 diagram to paste into a doc — use `architecture-diagrams`
|
|
118
|
+
- ❌ An as-is architecture write-up in C4 levels from infrastructure files — use `reverse-architecture`
|
|
119
|
+
- ❌ Fixing or rendering an existing Mermaid diagram — use `mermaid-render`
|
|
120
|
+
- ❌ Charts of numbers, dashboards or metrics — this draws structure, not data
|
|
121
|
+
|
|
122
|
+
## Anti-patterns to avoid
|
|
123
|
+
|
|
124
|
+
- ❌ **The usual architecture.** A load balancer, CDN or auth service drawn because systems
|
|
125
|
+
like this have one.
|
|
126
|
+
- ❌ **README as evidence of behaviour.** "We use Kafka" in a README is a plan until code
|
|
127
|
+
calls it.
|
|
128
|
+
- ❌ **Deleting evidence to pass the check.** A failed citation means the line is wrong or
|
|
129
|
+
the part is an assumption.
|
|
130
|
+
- ❌ **Everything in the repo.** Forty boxes answer no question.
|
|
131
|
+
- ❌ **Hand-placed coordinates** or a hand-written SVG instead of the script.
|
|
132
|
+
- ❌ **A page that loads a library from a CDN.** It breaks offline and leaks the viewer's IP.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"query": "Make an interactive diagram of what happens when someone places an order in this repo", "should_trigger": true},
|
|
3
|
+
{"query": "Visualize our request flow as an HTML page I can click around and send to the team", "should_trigger": true},
|
|
4
|
+
{"query": "Turn this description of our purchase approval process into an interactive flowchart", "should_trigger": true},
|
|
5
|
+
{"query": "Give me an explorable ERD of schema.sql as a single HTML file", "should_trigger": true},
|
|
6
|
+
{"query": "Map how this service talks to the database and the queue, with links back to the code", "should_trigger": true},
|
|
7
|
+
{"query": "Show what this branch changes in the architecture compared with main, as an interactive diagram", "should_trigger": true},
|
|
8
|
+
{"query": "Write a Mermaid C4 container diagram for the design doc from this description", "should_trigger": false},
|
|
9
|
+
{"query": "My mermaid flowchart says parse error on line 3, can you fix it and export a PNG", "should_trigger": false},
|
|
10
|
+
{"query": "Reverse-engineer the as-is architecture from our Terraform and write it up in C4 levels", "should_trigger": false},
|
|
11
|
+
{"query": "Build a dashboard chart of weekly signups from this CSV", "should_trigger": false}
|
|
12
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "interactive-diagram",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "This is the Larder repo. I'm onboarding two new developers next week. Can you make an interactive diagram, one HTML file I can send them, of what happens when a customer places an order?",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"An HTML file is written that works on its own offline: it loads no script, stylesheet or font from a CDN or any http(s) URL",
|
|
9
|
+
"The diagram shows the order path the code shows: the web checkout posting to the API's POST /orders, the API validating and inserting into Postgres, pushing an email job onto the Redis 'emails' list, and the worker popping it and posting to the mailer service",
|
|
10
|
+
"Kafka does not appear as part of the order path; if it is mentioned at all, it is described as planned in the README and not in the code",
|
|
11
|
+
"No payment step or payment provider appears in the order path (the README says payment is taken in the shop on collection)",
|
|
12
|
+
"It does not add parts the repo does not contain, such as a load balancer, CDN, auth service, cache or separate analytics service",
|
|
13
|
+
"The parts or connections in the diagram, or the page's text, name the files they come from (for example api/app.py, worker/worker.py, web/checkout.js, docker-compose.yml)",
|
|
14
|
+
"Anything drawn that the code does not show (for example who hosts the mailer, or what happens after the email is sent) is marked as an assumption, or left off",
|
|
15
|
+
"The reply opens with where the file is and what the diagram shows, in a few sentences, without a long account of how it was built"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": 2,
|
|
20
|
+
"prompt": "Can you turn this into an interactive flowchart I can share with the team as a single file? Here's how expense claims work at our office: you fill in a claim in the finance portal and attach the receipts. Your line manager approves it. If it's over £500 it also needs the finance director. Finance then checks the receipts match and pays it in the next payroll run. Claims older than 90 days are rejected automatically when you submit them.",
|
|
21
|
+
"assertions": [
|
|
22
|
+
"An HTML file is written that works on its own offline, loading nothing from a CDN or any http(s) URL",
|
|
23
|
+
"The flow includes, in order: submit in the finance portal with receipts, the automatic rejection of claims older than 90 days, line manager approval, the over £500 decision leading to finance director approval, finance checking receipts, and payment in the next payroll run",
|
|
24
|
+
"The over £500 rule and the 90-day rule are drawn as decisions or branches, not as plain steps",
|
|
25
|
+
"What happens when the line manager or finance director declines, or when receipts don't match, is not stated in the description, so it is either left off or visibly marked as an assumption, never drawn as a definite step",
|
|
26
|
+
"It adds no steps, roles, systems or thresholds the description does not contain (for example no budget-holder step, no expense card, no second threshold)",
|
|
27
|
+
"The reply is short: where the file is, what it shows, and which paths were not covered by the description"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": 3,
|
|
32
|
+
"prompt": "I need an interactive ERD of schema.sql for the team wiki - something people can click on to see the columns. One self-contained HTML file.",
|
|
33
|
+
"assertions": [
|
|
34
|
+
"An HTML file is written that works on its own offline, loading nothing from a CDN or any http(s) URL",
|
|
35
|
+
"All five tables appear (clinics, practitioners, patients, appointments, appointment_notes) with every column the schema defines and no column it does not",
|
|
36
|
+
"The four foreign keys are drawn as relationships with the right direction: practitioners to clinics, appointments to practitioners, appointments to patients, appointment_notes to appointments",
|
|
37
|
+
"booked_by_user_id is shown as a column but no relationship is drawn for it to a users or staff table as if it existed; if it is mentioned, the reply or page says it has no table in this schema",
|
|
38
|
+
"Primary keys and foreign keys are marked on their columns, and appointment_notes.appointment_id is shown as both",
|
|
39
|
+
"appointment_notes to appointments is shown as one-to-one or at most one note per appointment, not one-to-many",
|
|
40
|
+
"It invents no tables, columns, indexes or constraints that schema.sql does not contain"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": 4,
|
|
45
|
+
"held_out": "2026-10-18",
|
|
46
|
+
"prompt": "This is castbox-media, the backend for our podcast hosting platform. I need to explain to the new on-call engineer what actually happens when a creator's episode upload finishes and the storage webhook hits us, up to the point where we stop being in control. Make me an interactive diagram of that path I can click through and send them as one file.",
|
|
47
|
+
"assertions": [
|
|
48
|
+
"Writes one self-contained HTML file that opens offline: no <script src>, <link href>, @import or font URL pointing at http(s), and the diagram's code and styles are inline",
|
|
49
|
+
"Shows the path in order: the object store's webhook to POST /webhooks/storage (src/server.js), handleStorageWebhook checking the x-storage-signature HMAC and the object.finalized event type (src/handlers/storageWebhook.js), the episodes row set to 'uploaded' in PostgreSQL, a 'transcribe-episode' job added to the Redis/BullMQ 'media' queue, the worker (src/worker.js) running transcribeEpisode (src/jobs/transcribeEpisode.js), submitTranscription POSTing to the transcription API's /v2/jobs (src/clients/transcription.js), the episode set to 'transcribing' with its transcription_job_id, and the processing email sent over SMTP via src/mailer.js",
|
|
50
|
+
"Shows at least one of the branches the code takes: bad signature returns 401, non-finalized events return 204 and are ignored, an unknown or already-taken upload_key is ignored, the email only goes out when notify_on_processing is true, or the episode is marked 'failed' after the 3 attempts are used up",
|
|
51
|
+
"Boxes and arrows cite the source file (and line where given) they came from, and every cited file is one that exists in the repo",
|
|
52
|
+
"Does not draw transcoding, the search indexing or the CDN purge as steps on this path: the README and config/default.json mention them but no code on the upload path calls them (search is only used by the nightly 'nightly-reindex' job, and transcoding and CDN purge appear in no code). If shown at all, they are marked as not called or as claims from the README",
|
|
53
|
+
"Keeps the other paths out of the main flow: the GET /feeds/:showId.xml RSS route and the 03:00 nightly-reindex job are either absent or clearly marked as separate from the upload path",
|
|
54
|
+
"Does not present what the transcription service does after accepting the job as fact: the callback to /webhooks/transcription is dashed, marked as an assumption or left out, and the reply or diagram notes that src/server.js registers no /webhooks/transcription route, so nothing in this repo receives the result or moves the episode to 'published'",
|
|
55
|
+
"The reply opens with where the HTML file is and the main takeaway (for example that the app's part ends once the job is submitted and the email sent, and the transcript callback has no handler), not with a long account of how the diagram was made"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": 5,
|
|
60
|
+
"held_out": "2026-10-18",
|
|
61
|
+
"prompt": "Can you make an interactive sequence diagram of how a repair request goes through at Fernhill Housing Co-op? I want to show it to our board next week as one file they can open. Here's how it works as far as I know:\n\nA tenant phones the contact centre or fills in the web form to report a repair. The contact centre logs it in our housing management system (HMS) and gives the tenant a reference number. Every weekday morning the repairs coordinator goes through the new jobs in HMS, decides the priority (emergency = attend within 24 hours, routine = within 20 working days) and assigns it to one of our two contractors by emailing them the job sheet. The contractor rings the tenant to book a visit, then does the work. After the visit the contractor marks the job complete in their own portal. At the end of each month the contractor sends us an invoice listing the jobs they finished, and finance closes those jobs in HMS when they pay the invoice.\n\nIf the contractor needs to order parts I'm honestly not sure what happens, I think they tell the coordinator? We used to post the tenant a confirmation letter when a job was closed but we stopped doing that in 2025. There's also the annual gas safety check programme but that's a different process, leave it out.",
|
|
62
|
+
"assertions": [
|
|
63
|
+
"Writes one self-contained HTML file that opens offline: no <script src>, <link href>, @import or font URL pointing at http(s), and the diagram's code and styles are inline",
|
|
64
|
+
"It is a sequence diagram with at least these participants: tenant, contact centre, HMS, repairs coordinator, contractor and finance (the contractor's portal may be its own participant or part of the contractor)",
|
|
65
|
+
"The messages run in this order: tenant reports by phone or web form; contact centre logs the job in HMS and gives the tenant a reference number; the coordinator reviews new jobs each weekday morning, sets the priority (emergency 24 hours, routine 20 working days) and emails the job sheet to a contractor; the contractor rings the tenant to book and does the visit; the contractor marks the job complete in its own portal; the contractor sends a monthly invoice to finance; finance closes the jobs in HMS on payment",
|
|
66
|
+
"The parts-ordering step is drawn dashed or labelled as an assumption or unconfirmed (the user said they are not sure), not as a solid confirmed message",
|
|
67
|
+
"The posted confirmation letter does not appear as a current step (absent, or shown only as stopped in 2025), and the gas safety check programme does not appear",
|
|
68
|
+
"Points out at least one hand-off gap the description reveals: nothing tells the tenant the repair is finished or closed; HMS only learns a job is done when finance pays the monthly invoice, so jobs stay open in HMS for weeks; or an emergency reported after the morning review waits until the next weekday morning before the coordinator sees it",
|
|
69
|
+
"Each message carries or cites the words from the user's description it came from, and the diagram adds no participants or systems the user did not describe (for example no SMS service, CRM, scheduling tool or tenant app)",
|
|
70
|
+
"The reply opens with where the HTML file is and the main takeaway (such as the gap found), not with a long account of how the diagram was made"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Larder
|
|
2
|
+
|
|
3
|
+
Grocery ordering for small shops. Customers order on the web, the shop gets an email.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
- `web/` - the storefront
|
|
8
|
+
- `api/` - order API (Flask)
|
|
9
|
+
- `worker/` - sends confirmation emails
|
|
10
|
+
- Order events go to Kafka so analytics can consume them (planned for Q3).
|
|
11
|
+
- Payments are taken in the shop on collection, not online.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import psycopg
|
|
5
|
+
import redis
|
|
6
|
+
from flask import Flask, jsonify, request
|
|
7
|
+
|
|
8
|
+
app = Flask(__name__)
|
|
9
|
+
db = psycopg.connect(os.environ["DATABASE_URL"])
|
|
10
|
+
queue = redis.Redis.from_url(os.environ["REDIS_URL"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate(order):
|
|
14
|
+
if not order.get("items"):
|
|
15
|
+
return "an order needs at least one item"
|
|
16
|
+
if not order.get("email"):
|
|
17
|
+
return "an order needs an email for the confirmation"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.post("/orders")
|
|
21
|
+
def place_order():
|
|
22
|
+
order = request.get_json()
|
|
23
|
+
problem = validate(order)
|
|
24
|
+
if problem:
|
|
25
|
+
# the first rule the order breaks, in words the storefront can show
|
|
26
|
+
return jsonify(error=problem), 400
|
|
27
|
+
with db.cursor() as cur:
|
|
28
|
+
cur.execute(
|
|
29
|
+
"insert into orders (shop_id, email, items) values (%s, %s, %s) returning id",
|
|
30
|
+
(order["shop_id"], order["email"], json.dumps(order["items"])),
|
|
31
|
+
)
|
|
32
|
+
order_id = cur.fetchone()[0]
|
|
33
|
+
db.commit()
|
|
34
|
+
queue.lpush("emails", json.dumps({"order_id": order_id, "to": order["email"]}))
|
|
35
|
+
return jsonify(id=order_id), 201
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.get("/orders/<int:order_id>")
|
|
39
|
+
def get_order(order_id):
|
|
40
|
+
with db.cursor() as cur:
|
|
41
|
+
cur.execute("select id, status from orders where id = %s", (order_id,))
|
|
42
|
+
row = cur.fetchone()
|
|
43
|
+
return jsonify(id=row[0], status=row[1]) if row else ("", 404)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
services:
|
|
2
|
+
web:
|
|
3
|
+
build: ./web
|
|
4
|
+
ports: ["8080:80"]
|
|
5
|
+
api:
|
|
6
|
+
build: ./api
|
|
7
|
+
environment:
|
|
8
|
+
DATABASE_URL: postgresql://larder@db/larder
|
|
9
|
+
REDIS_URL: redis://queue:6379/0
|
|
10
|
+
depends_on: [db, queue]
|
|
11
|
+
worker:
|
|
12
|
+
build: ./worker
|
|
13
|
+
environment:
|
|
14
|
+
REDIS_URL: redis://queue:6379/0
|
|
15
|
+
MAILER_URL: https://api.mailer.example/v1/send
|
|
16
|
+
depends_on: [queue]
|
|
17
|
+
db:
|
|
18
|
+
image: postgres:16
|
|
19
|
+
queue:
|
|
20
|
+
image: redis:7
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function placeOrder(cart, email, shopId) {
|
|
2
|
+
const res = await fetch("/api/orders", {
|
|
3
|
+
method: "POST",
|
|
4
|
+
headers: { "Content-Type": "application/json" },
|
|
5
|
+
body: JSON.stringify({ items: cart.items, email, shop_id: shopId }),
|
|
6
|
+
});
|
|
7
|
+
if (!res.ok) throw new Error((await res.json()).error);
|
|
8
|
+
return (await res.json()).id;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import redis
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
queue = redis.Redis.from_url(os.environ["REDIS_URL"])
|
|
8
|
+
MAILER_URL = os.environ["MAILER_URL"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run():
|
|
12
|
+
while True:
|
|
13
|
+
_, raw = queue.brpop("emails")
|
|
14
|
+
job = json.loads(raw)
|
|
15
|
+
requests.post(MAILER_URL, json={
|
|
16
|
+
"to": job["to"],
|
|
17
|
+
"template": "order-confirmation",
|
|
18
|
+
"data": {"order_id": job["order_id"]},
|
|
19
|
+
}, timeout=10)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
run()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
-- Clinic bookings
|
|
2
|
+
create table clinics (
|
|
3
|
+
id serial primary key,
|
|
4
|
+
name text not null,
|
|
5
|
+
timezone text not null default 'Europe/London'
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
create table practitioners (
|
|
9
|
+
id serial primary key,
|
|
10
|
+
clinic_id integer not null references clinics(id),
|
|
11
|
+
full_name text not null,
|
|
12
|
+
role text not null check (role in ('physio', 'osteo', 'massage'))
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
create table patients (
|
|
16
|
+
id serial primary key,
|
|
17
|
+
email text unique not null,
|
|
18
|
+
phone text
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
create table appointments (
|
|
22
|
+
id serial primary key,
|
|
23
|
+
practitioner_id integer not null references practitioners(id),
|
|
24
|
+
patient_id integer not null references patients(id),
|
|
25
|
+
starts_at timestamptz not null,
|
|
26
|
+
minutes integer not null default 45,
|
|
27
|
+
status text not null default 'booked',
|
|
28
|
+
booked_by_user_id integer -- staff login id from the old system; no table here
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
create table appointment_notes (
|
|
32
|
+
appointment_id integer primary key references appointments(id),
|
|
33
|
+
body text not null,
|
|
34
|
+
written_at timestamptz not null default now()
|
|
35
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# castbox-media
|
|
2
|
+
|
|
3
|
+
Backend for the Hollowbrook Podcast Network's hosting platform.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
When a creator finishes uploading an episode, the object store sends us a
|
|
8
|
+
webhook. We then transcode and transcribe the audio, index the transcript in
|
|
9
|
+
search so listeners can find episodes by what was said, and purge the CDN so
|
|
10
|
+
the new episode shows up in the RSS feed straight away. The creator gets an
|
|
11
|
+
email when processing starts.
|
|
12
|
+
|
|
13
|
+
## Stack
|
|
14
|
+
|
|
15
|
+
- Node 20, Express (API) and BullMQ (background jobs)
|
|
16
|
+
- PostgreSQL (`episodes`, `shows`, `creators`)
|
|
17
|
+
- Redis (job queue)
|
|
18
|
+
- Transcription vendor HTTP API (api.transcribe.example.test)
|
|
19
|
+
- Search service (search.internal.test)
|
|
20
|
+
- CDN in front of the RSS feeds (cdn.example.test)
|
|
21
|
+
- SMTP relay for creator emails
|
|
22
|
+
|
|
23
|
+
## Running
|
|
24
|
+
|
|
25
|
+
npm run api # node src/server.js
|
|
26
|
+
npm run worker # node src/worker.js
|