@sitar_fiercer4c/skills 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +75 -0
  3. package/bin/install.js +45 -0
  4. package/package.json +29 -0
  5. package/skills/architecture-walkthrough/SKILL.md +223 -0
  6. package/skills/architecture-walkthrough/references/sections.md +29 -0
  7. package/skills/architecture-walkthrough/scripts/check_structure.py +200 -0
  8. package/skills/autotest-webapp-ui/SKILL.md +58 -0
  9. package/skills/backend-code-review/SKILL.md +386 -0
  10. package/skills/backend-code-review/references/report-format.md +333 -0
  11. package/skills/backend-code-review/scripts/list_routes.py +269 -0
  12. package/skills/backend-code-review/scripts/sweep.py +550 -0
  13. package/skills/backend-code-review/scripts/verify_citations.py +201 -0
  14. package/skills/be-brief/SKILL.md +18 -0
  15. package/skills/clarke-list-excel/SKILL.md +51 -0
  16. package/skills/clarke-list-excel/references/output-schema.md +125 -0
  17. package/skills/clarke-list-excel/scripts/clarke_common.py +251 -0
  18. package/skills/clarke-list-excel/scripts/clarke_extract.py +487 -0
  19. package/skills/clarke-list-excel/scripts/load_clarke.py +322 -0
  20. package/skills/clarke-list-excel/scripts/run_all.py +63 -0
  21. package/skills/datalab-api/SKILL.md +163 -0
  22. package/skills/datalab-api/references/parameters-and-payload.md +121 -0
  23. package/skills/datalab-api/references/table-selection.md +35 -0
  24. package/skills/datalab-api/scripts/datalab_tables.py +365 -0
  25. package/skills/find-test-seam/SKILL.md +41 -0
  26. package/skills/frontend-code-review/SKILL.md +247 -0
  27. package/skills/frontend-code-review-2/SKILL.md +192 -0
  28. package/skills/frontend-code-review-2/scripts/fetch_pr_comments.py +65 -0
  29. package/skills/frontend-code-review-2/scripts/render_report.py +139 -0
  30. package/skills/murtaza-breif/SKILL.md +143 -0
  31. package/skills/murtaza-breif/scripts/save_brief.py +128 -0
  32. package/skills/pdf-to-json/SKILL.md +42 -0
  33. package/skills/pdf-to-json/references/output-schema.md +168 -0
  34. package/skills/pdf-to-json/scripts/extract_figures.py +319 -0
  35. package/skills/pdf-to-json/scripts/load_mongo.py +287 -0
  36. package/skills/pdf-to-json/scripts/pdf_extract.py +1313 -0
  37. package/skills/record-api-traffic/SKILL.md +434 -0
  38. package/skills/record-api-traffic/references/reading-recordings.md +224 -0
  39. package/skills/record-api-traffic/scripts/check-schema.mjs +184 -0
  40. package/skills/record-api-traffic/scripts/dump-quotation.mjs +67 -0
  41. package/skills/record-api-traffic/scripts/dump-source-excel.mjs +75 -0
  42. package/skills/record-api-traffic/scripts/lib/repo.mjs +109 -0
  43. package/skills/record-api-traffic/scripts/preflight.py +528 -0
  44. package/skills/record-api-traffic/scripts/record-api-traffic.py +720 -0
  45. package/skills/refac-wrt-business-goal/SKILL.md +305 -0
  46. package/skills/refac-wrt-business-goal/references/critic.md +170 -0
  47. package/skills/system-resource-triage/SKILL.md +180 -0
  48. package/skills/system-resource-triage/scripts/reap.sh +116 -0
  49. package/skills/system-resource-triage/scripts/triage.sh +111 -0
  50. package/skills/using-git-worktrees/SKILL.md +167 -0
@@ -0,0 +1,333 @@
1
+ # Report format
2
+
3
+ The shape below is the output contract. It came out of three rounds of a reader reacting to
4
+ drafts, and each part of it earns its place โ€” the notes explain what each is for so you can
5
+ tell when a deviation is reasonable.
6
+
7
+ ## Contents
8
+
9
+ - [Skeleton](#skeleton)
10
+ - [Severity model](#severity-model)
11
+ - [The MVCS skeleton](#the-mvcs-skeleton)
12
+ - [Finding tables](#finding-tables)
13
+ - [What the Location column asserts](#what-the-location-column-asserts)
14
+ - [Rows whose claim names a set](#rows-whose-claim-names-a-set)
15
+ - [Two things every repair does](#two-things-every-repair-does)
16
+ - [Worked example rows](#worked-example-rows)
17
+ - [Inventory tables are transcribed, not typed](#inventory-tables-are-transcribed-not-typed)
18
+ - [Closing sections](#closing-sections)
19
+
20
+ ---
21
+
22
+ ## Skeleton
23
+
24
+ ````markdown
25
+ # Route Review โ€” <scope, e.g. "full HTTP surface" or "POST surface">
26
+
27
+ **Target:** <absolute path to the backend root>
28
+ **Rules applied:** <path to code-rules.md> โ€” <one-line paraphrase of the layering it mandates>
29
+ **Scope:** all N routes across the M route files, plus the K routes defined inline in `<entrypoint>`.
30
+
31
+ | Severity | Count | Meaning |
32
+ |---|---|---|
33
+ | ๐Ÿ”ด | N | Rule violation, security defect, or correctness bug |
34
+ | ๐ŸŸก | N | Rule drift, duplication, fragile coupling |
35
+ | โš ๏ธ | N | Observation / nit / inconsistency |
36
+ | โœ… | N | Verified correct โ€” worth keeping as-is |
37
+
38
+ ---
39
+
40
+ ## Route inventory
41
+
42
+ | # | Method | Path (as mounted) | File:line | Auth | Validator |
43
+ |---|---|---|---|---|---|
44
+ | 1 | POST | `/api/auth/register` | `auth.routes.js:13` | โ€” | โœ… |
45
+ ...
46
+
47
+ **<X> of <N> routes have a validator.** <One sentence naming where they cluster, if they do.>
48
+
49
+ ---
50
+
51
+ ## Config
52
+ *Methodology not yet established โ€” this section was not reviewed.*
53
+
54
+ ## Controllers
55
+ *Methodology not yet established โ€” this section was not reviewed.*
56
+
57
+ ## Middleware
58
+ *Methodology not yet established โ€” this section was not reviewed.*
59
+
60
+ ## Models
61
+ *Methodology not yet established โ€” this section was not reviewed.*
62
+
63
+ ## Routes
64
+
65
+ ### `src/routes/<file>.js`
66
+
67
+ > <Any file-level fact that changes how every route in it reads โ€” most often the mount
68
+ > prefix, when it differs from what the file's own comments claim.>
69
+
70
+ #### `<METHOD> <path>` โ€” line N
71
+ `<middleware chain> โ†’ <controller>.<handler>`
72
+
73
+ | Sev | Rule | Location | Finding |
74
+ |---|---|---|---|
75
+ | ๐Ÿ”ด | <code-rules section> | `file.js:LINE` | <finding> |
76
+
77
+ ### Routes defined in `src/<entrypoint>`
78
+ ...
79
+
80
+ ## Services
81
+ *Methodology not yet established โ€” this section was not reviewed.*
82
+
83
+ ## Utils
84
+ *Methodology not yet established โ€” this section was not reviewed.*
85
+
86
+ ## Validators
87
+ *Methodology not yet established โ€” this section was not reviewed.*
88
+
89
+ ---
90
+
91
+ ## Cross-cutting
92
+
93
+ | Sev | Rule | Location | Finding |
94
+ |---|---|---|---|
95
+
96
+ ---
97
+
98
+ ## Highest-value fixes, in order
99
+
100
+ 1. `file.js:LINE` โ€” <what and why it ranks here>
101
+
102
+ ---
103
+
104
+ ## Notes on inputs
105
+
106
+ <Anything true about a prior review document, or about the sweep/inventory scripts' own output,
107
+ that a reader of this review needs. Omit the section if there is nothing. Never a finding row.>
108
+
109
+ ---
110
+
111
+ ## Files read for this review
112
+
113
+ <list>
114
+
115
+ Not read (referenced only): <list>
116
+ ````
117
+
118
+ ---
119
+
120
+ ## Severity model
121
+
122
+ | Marker | Means |
123
+ |---|---|
124
+ | ๐Ÿ”ด | Rule violation, security defect, or correctness bug |
125
+ | ๐ŸŸก | Rule drift, duplication, fragile coupling |
126
+ | โš ๏ธ | Observation, nit, inconsistency |
127
+ | โœ… | Verified correct โ€” worth keeping as-is |
128
+
129
+ Put the counts at the top. A reader deciding whether to read 400 lines of tables wants to know
130
+ the shape of what is coming, and the counts are also the honest headline โ€” a review with three
131
+ ๐Ÿ”ด and forty โš ๏ธ is a different message than the reverse, and burying that lets a reader
132
+ mistake volume for severity.
133
+
134
+ Count as you write rather than estimating up front; the tally has to match the tables or the
135
+ whole document loses credibility on the first thing the reader can check.
136
+
137
+ **`โœ…` rows are not padding.** A review listing only faults gives the reader no way to
138
+ distinguish "examined and sound" from "not examined". That ambiguity matters most exactly where
139
+ the stakes are highest โ€” an auth middleware with no findings against it could mean it is
140
+ correct or that nobody read it. Say which. It also makes the review usable as a record of what
141
+ was verified, and it stops a correct-but-unusual construction from being "fixed" later by
142
+ someone who assumes it was overlooked.
143
+
144
+ Aim to record the non-obvious correct decisions, not every line that works: a security control
145
+ that is properly closed, a guard placed before expensive work, an idempotency mechanism, a
146
+ deliberate departure from a convention that turns out to be right.
147
+
148
+ A `โœ…` row names the failure path it followed and what it found there โ€” the catch block, the
149
+ concurrent second caller, the missing-config case. The happy path working is the default, not
150
+ evidence. Because the marker promises "examined and sound", it is the one row type where being
151
+ wrong is worse than being absent: it stops the next reader from looking. Keep its scope to what
152
+ you actually probed and do not let it spill onto adjacent code โ€” a careful streaming mechanism
153
+ can sit inside a `catch` that flattens every error to `404`, and a `โœ…` on the former reads as
154
+ absolution for the latter.
155
+
156
+ ---
157
+
158
+ ## The MVCS skeleton
159
+
160
+ The report's H2 headings come from the section list in the target repo's `code-rules.md`, in the
161
+ order that file lists them โ€” not from a fixed list in this skill. A repo whose rules file names
162
+ `jobs` or `adapters` gets those headings.
163
+
164
+ Every section that was not reviewed still gets its heading, carrying:
165
+
166
+ ```markdown
167
+ *Methodology not yet established โ€” this section was not reviewed.*
168
+ ```
169
+
170
+ This is the point of the skeleton. Without those lines a reader cannot tell an unreviewed
171
+ section from a clean one, and silence reads as a pass. With them, the report doubles as a map of
172
+ how much of the architecture has been covered so far.
173
+
174
+ ---
175
+
176
+ ## Finding tables
177
+
178
+ One H3 per route file, one H4 per route, one table per route.
179
+
180
+ Under each H4, restate the middleware chain as `middleware โ†’ controller.handler`. It takes one
181
+ line and it means a reader can see the route's full shape without opening the file.
182
+
183
+ Columns:
184
+
185
+ | Column | Contents |
186
+ |---|---|
187
+ | **Sev** | One marker. |
188
+ | **Rule** | The `code-rules.md` section the finding is judged against โ€” `routes`, `controllers`, `services`, `validators`, `services (DONTS)`. Use `โ€”` when the finding is a security or correctness issue rather than a layering one. |
189
+ | **Location** | `file.js:LINE` or `file.js:LINEโ€“LINE`. Two locations when the finding is about a relationship (duplication, a helper that exists but is bypassed). `file.js` (absent) when the finding is that something is missing. |
190
+ | **Finding** | Lead with a **bold claim in a few words**, then the evidence, then the consequence, then the repair. |
191
+
192
+ Order rows within a table by severity, ๐Ÿ”ด first.
193
+
194
+ The Rule column is what makes this a review against *this repo's stated architecture* rather
195
+ than generic advice. If a finding cannot be tied to a rule and is not a security or correctness
196
+ issue, it is probably a preference โ€” cut it.
197
+
198
+ ### What the Location column asserts
199
+
200
+ **Every line in a ๐Ÿ”ด/๐ŸŸก Location is part of the defect.** The column is not "lines relevant to
201
+ this row" โ€” a reader treats each entry as something you are asking them to change, and a line
202
+ folded in for context arrives as an accusation against code that is fine. Contributing context
203
+ goes in the finding body, where it can be labelled as context.
204
+
205
+ One review put `app.js:20` โ€” `app.set('trust proxy', 1)` โ€” into the Location of a ๐ŸŸก about
206
+ rate-limiter keying. The trust-proxy line was background; the defect was elsewhere. The row read
207
+ as "change your trust proxy setting", which was not the finding and was not correct.
208
+
209
+ **Every path in a Location is under the reviewed source tree.** Two things reliably tempt their
210
+ way into a finding row and do not belong there:
211
+
212
+ - observations about a *previous review document* ("`ROUTE_REVIEW.md` cites the wrong line")
213
+ - observations about the *tooling* ("`sweep.py` reports this export as used, but that is a false
214
+ positive")
215
+
216
+ Both can be worth saying. Neither is a finding about the backend, and a reader scanning for code
217
+ to fix has to stop and work out that this row is not about code. Put them in a `## Notes on
218
+ inputs` section below the tables.
219
+
220
+ **A row claiming an absence cites the whole construct.** "No `return`", "never calls the
221
+ validator", "lacks a guard" โ€” you can only see an absence across a complete unit. A range that
222
+ stops before the closing brace does not support the claim, because the thing you say is missing
223
+ may be on the next line, and that is precisely how one review reported a function as missing its
224
+ `return results;` when the return was one line past the cited range. Cite from the signature to
225
+ the closing brace, and quote the closing lines or the command that establishes the absence.
226
+ `scripts/verify_citations.py` flags these; run it before the report is done.
227
+
228
+ ### Rows whose claim names a set
229
+
230
+ Some findings are about a set rather than a spot: the keys a masking list is missing, the sites a
231
+ duplicated block appears at, the variables a config module reads unguarded, the endpoints without
232
+ a limiter. These are the rows that go wrong most often, and they go wrong the same way every
233
+ time โ€” the set is right minus one. A reader acts on the row, fixes what it names, and the missing
234
+ element survives with the row marked done. That is worse than not raising it, because now nobody
235
+ will look again.
236
+
237
+ So a row whose claim names a set carries three things:
238
+
239
+ 1. **The count, stated as "N of N"** โ€” "three of the three request keys that reach the logger",
240
+ not "keys including `code` and `state`". Writing the count forces you to have one.
241
+ 2. **Where the set came from**, named in the row: the sweep section, the grep, the inventory
242
+ column. "From `sweep.py --section request-inputs`" tells the reader the enumeration was
243
+ mechanical, and tells you that it was.
244
+ 3. **One repair clause per element.** If the claim names three keys, the repair names three keys.
245
+
246
+ A repair with fewer clauses than the claim has elements is an **incomplete finding**, not a
247
+ finding with a shortened repair โ€” the missing clause is the whole cost of the row. This is
248
+ checkable by counting, which is why it is stated this way: "repair the whole finding" is good
249
+ advice that people follow when they remember the third element and not when they don't.
250
+
251
+ The corollary is that these enumerations come from the scripts, not from recall. Recall is what
252
+ produces "Nโˆ’1 of N".
253
+
254
+ ### Two things every repair does
255
+
256
+ **Name what the repair removes, not only what it adds.** A finding says one `JWT_SECRET` signs
257
+ both session tokens and image-proxy tokens; the repair proposes a shared `getJwtSecret()` helper
258
+ imported at all four sites. That is tidier code and the exact same defect โ€” one secret, two token
259
+ types โ€” now with a helper making it convenient. A repair that leaves the complained-of property
260
+ in place is not a repair. Say what stops being true: "image tokens move to `IMAGE_TOKEN_SECRET`,
261
+ so a leaked session key no longer mints image URLs."
262
+
263
+ **A repair that relocates code quotes the rule that gives the destination its job.** "Move this to
264
+ the service layer" is a guess unless the rules file says the service owns it, and the guess is
265
+ usually the layer you thought of first. Open the rules file, find the section for the destination,
266
+ and quote the sentence. One review moved file-type validation โ€” a `415` thrown on an unexpected
267
+ extension โ€” into a service, when the rules file had a `validators` section describing exactly that
268
+ responsibility; the section was never opened. Quoting it costs one line and makes the repair
269
+ arguable on the repo's own terms instead of on taste.
270
+
271
+ Beyond those two: every ๐Ÿ”ด and ๐ŸŸก row ends in a repair โ€” the function to extract, the module to
272
+ create, the export to narrow, the signature to change. The reader is deciding what to do on
273
+ Monday, and a row that stops at the consequence makes them re-derive a conclusion you already
274
+ reached. If the right repair is genuinely contested, name the options and their costs and say
275
+ which one the rules file favours; that still tells them where to start.
276
+
277
+ ---
278
+
279
+ ## Worked example rows
280
+
281
+ The specificity below is the target. Each names a location, states what is true there, says what
282
+ it costs, and says what to do โ€” a reader can act on any of them without asking a follow-up
283
+ question.
284
+
285
+ | Sev | Rule | Location | Finding |
286
+ |---|---|---|---|
287
+ | ๐Ÿ”ด | services | `auth.service.js:76` | **One global secret resets any account.** `secretCode !== process.env.FORGOT_PASSWORD_SECRET_CODE` has no per-user binding, no expiry, no single-use consumption. Anyone who learns it resets every user's password given only an email. Issue a per-user random token instead, stored hashed with an expiry and cleared on use. |
288
+ | ๐ŸŸก | services (DONTS) | `quotation.service.js:112, 131, 204, 288` | **Six helpers are exported but only ever called in-file.** `toNonNegativeNumber`, `round2`, `recalcItem`, `sanitizeDescriptionStyle`, `QUOTATION_ITEM_FIELDS`, `QUOTATION_HEADER_FIELDS` have no importer anywhere in `src`, `tests` or `scripts`, yet each has four or five callers inside this module. They are over-exported, not dead: drop them from the `export` block at `:640` and keep every definition. |
289
+ | ๐Ÿ”ด | controllers | `email.controller.js:89` + `:107` | **Two Graph round-trips for one request.** `getAttachmentMetadata` calls `microsoft.getMessage` via `withGraph` (`email.service.js:612`), then `getAttachmentBuffer` runs `withGraph` again (597) โ€” second connection load, second potential token refresh, second call for the same message. Should be one service function returning both. |
290
+ | ๐ŸŸก | services / config | `s3Image.service.js:318, 471` vs `auth.middleware.js:20` | **`JWT_SECRET` is reused for two different token types.** Confusion is blocked today only by payload-shape luck โ€” the auth middleware requires `payload.userId` (image tokens lack it) and the image verifier requires `payload.quotationId` (session tokens lack it). Neither check is documented as a security boundary, so a future payload change silently breaks it. `oauthState.js:3` already uses a dedicated secret โ€” follow that. |
291
+ | โœ… | services | `s3Image.service.js:470โ€“495` | **Path traversal is properly closed.** The reconstructed key (488) is compared for equality against the signed `payload.key`, and that key was minted through a `^quotations\/([^/]+)\/images\/([^/]+)$` regex (298). A `..` in `:filename` cannot match a signed key. |
292
+
293
+ Note what the last row does: it reports a security control that a reviewer would naturally probe,
294
+ and records the specific attack it followed and why that attack fails. That is more useful than
295
+ silence, because the next reader does not have to re-derive it โ€” and it is a `โœ…` about a failure
296
+ path, not about the code reading well.
297
+
298
+ Note what the fourth row does โ€” it names an adjacent place in the same codebase that already
299
+ solves the problem correctly. A fix the reader can copy from their own repo lands better than an
300
+ abstract recommendation.
301
+
302
+ Note what the second row does *not* do: it does not say "delete them". Six exports with no
303
+ importer look dead from outside the file and are not, and a review that says "safe to delete"
304
+ about identifiers with in-file callers hands the reader a change that breaks the module. Naming
305
+ the export block as the thing to edit is what makes the row safe to act on.
306
+
307
+ ---
308
+
309
+ ## Inventory tables are transcribed, not typed
310
+
311
+ The route inventory, and any per-file handler inventory, take their line numbers from
312
+ `scripts/list_routes.py` output โ€” paste the column, do not retype it from the file you have open.
313
+
314
+ This sounds pedantic and is not. Inventory rows are where citation errors cluster, because a
315
+ finding's citation gets checked while writing the argument around it and an inventory row gets
316
+ checked by nobody. One review's handler inventory for a single controller was off by three to six
317
+ lines on every row, in a document whose finding citations were almost all exact. A reader who
318
+ opens the first inventory row, lands three lines away, and finds nothing there has no way to know
319
+ the rest of the report is better than this.
320
+
321
+ ---
322
+
323
+ ## Closing sections
324
+
325
+ **Highest-value fixes, in order.** The tables are organised for lookup, which means they are not
326
+ organised for action โ€” a ๐Ÿ”ด in the last table is no less urgent than one in the first. This
327
+ section is the answer to "what do I do Monday". Rank by exploitability and blast radius rather
328
+ than by severity marker alone: an unauthenticated remote issue outranks an internal layering
329
+ violation even when both are ๐Ÿ”ด.
330
+
331
+ **Files read for this review.** State the blast radius. A reader who knows a service was never
332
+ opened can weigh the review's silence about it correctly. List what was referenced but not read
333
+ separately.
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env python3
2
+ """Enumerate Express routes with their real mounted paths.
3
+
4
+ A route's real path is the router's mount prefix plus the path written in the
5
+ route file. Doc comments naming the path drift from the mount point and cannot
6
+ be trusted, so this resolves the prefix from the app entrypoint instead.
7
+
8
+ Usage:
9
+ python3 list_routes.py <src-dir> [--json]
10
+
11
+ Output is a scaffold, not a finding. Regex parsing misses dynamic mounts,
12
+ re-exported routers, and conditionally registered routes, so confirm every row
13
+ against the file before it goes into a review.
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ METHODS = ("get", "post", "put", "patch", "delete", "options", "head", "all")
23
+
24
+ ENTRYPOINT_NAMES = ("app.js", "server.js", "index.js", "app.ts", "server.ts", "index.ts")
25
+
26
+
27
+ def find_call(text, start):
28
+ """Return (inner_text, end_index) for the parenthesised call starting at `start`.
29
+
30
+ `start` is the index of the opening paren. Tracks string literals so a paren
31
+ or quote inside a route path doesn't unbalance the scan.
32
+ """
33
+ depth = 0
34
+ i = start
35
+ quote = None
36
+ while i < len(text):
37
+ ch = text[i]
38
+ if quote:
39
+ if ch == "\\":
40
+ i += 2
41
+ continue
42
+ if ch == quote:
43
+ quote = None
44
+ elif ch in "'\"`":
45
+ quote = ch
46
+ elif ch == "(":
47
+ depth += 1
48
+ elif ch == ")":
49
+ depth -= 1
50
+ if depth == 0:
51
+ return text[start + 1 : i], i
52
+ i += 1
53
+ return None, len(text)
54
+
55
+
56
+ def split_args(inner):
57
+ """Split a call's argument list on top-level commas."""
58
+ args, depth, quote, current = [], 0, None, []
59
+ for i, ch in enumerate(inner):
60
+ if quote:
61
+ current.append(ch)
62
+ if ch == "\\":
63
+ continue
64
+ if ch == quote:
65
+ quote = None
66
+ continue
67
+ if ch in "'\"`":
68
+ quote = ch
69
+ current.append(ch)
70
+ continue
71
+ if ch in "([{":
72
+ depth += 1
73
+ elif ch in ")]}":
74
+ depth -= 1
75
+ if ch == "," and depth == 0:
76
+ args.append("".join(current).strip())
77
+ current = []
78
+ continue
79
+ current.append(ch)
80
+ if current:
81
+ args.append("".join(current).strip())
82
+ return [a for a in args if a]
83
+
84
+
85
+ def line_of(text, index):
86
+ return text.count("\n", 0, index) + 1
87
+
88
+
89
+ def strip_quotes(arg):
90
+ arg = arg.strip()
91
+ if len(arg) >= 2 and arg[0] in "'\"`" and arg[-1] == arg[0]:
92
+ return arg[1:-1]
93
+ return None
94
+
95
+
96
+ def clean_ident(arg):
97
+ """Reduce an argument expression to something readable in a table cell."""
98
+ arg = " ".join(arg.split())
99
+ if len(arg) > 40:
100
+ arg = arg[:37] + "..."
101
+ return arg
102
+
103
+
104
+ def parse_imports(text):
105
+ """Map imported identifier -> resolved module path fragment."""
106
+ out = {}
107
+ for m in re.finditer(r"import\s+(\w+)\s+from\s+['\"]([^'\"]+)['\"]", text):
108
+ out[m.group(1)] = m.group(2)
109
+ return out
110
+
111
+
112
+ def parse_mounts(entry_text, imports):
113
+ """Map router identifier -> mount prefix, and collect non-router app.use middleware.
114
+
115
+ Returns (mounts, prefix_middleware) where mounts is {identifier: prefix} and
116
+ prefix_middleware is a list of (prefix, identifier, line) for middleware applied
117
+ by path prefix rather than inside a route file -- rate limiters, most often.
118
+ Those are worth surfacing because they are invisible from the route file.
119
+ """
120
+ mounts, prefix_middleware = {}, []
121
+ for m in re.finditer(r"\bapp\.use\s*\(", entry_text):
122
+ inner, _ = find_call(entry_text, m.end() - 1)
123
+ if inner is None:
124
+ continue
125
+ args = split_args(inner)
126
+ if len(args) < 2:
127
+ continue
128
+ prefix = strip_quotes(args[0])
129
+ if prefix is None:
130
+ continue
131
+ for ident in args[1:]:
132
+ ident = ident.strip()
133
+ if not re.fullmatch(r"\w+", ident):
134
+ continue
135
+ source = imports.get(ident, "")
136
+ if "route" in source.lower() or "route" in ident.lower():
137
+ mounts[ident] = prefix
138
+ else:
139
+ prefix_middleware.append(
140
+ (prefix, ident, line_of(entry_text, m.start()))
141
+ )
142
+ return mounts, prefix_middleware
143
+
144
+
145
+ def parse_routes(text, filename, prefix):
146
+ """Extract route definitions from a router file."""
147
+ rows = []
148
+ pattern = re.compile(r"\b(?:router|app)\.(" + "|".join(METHODS) + r")\s*\(")
149
+ for m in pattern.finditer(text):
150
+ method = m.group(1).upper()
151
+ inner, _ = find_call(text, m.end() - 1)
152
+ if inner is None:
153
+ continue
154
+ args = split_args(inner)
155
+ if not args:
156
+ continue
157
+ route_path = strip_quotes(args[0])
158
+ if route_path is None:
159
+ continue
160
+ rest = args[1:]
161
+ handler = clean_ident(rest[-1]) if rest else ""
162
+ middleware = [clean_ident(a) for a in rest[:-1]] if len(rest) > 1 else []
163
+ full = (prefix.rstrip("/") + route_path) if prefix else route_path
164
+ if not full.startswith("/"):
165
+ full = "/" + full
166
+ rows.append(
167
+ {
168
+ "method": method,
169
+ "path": full,
170
+ "route_path": route_path,
171
+ "file": filename,
172
+ "line": line_of(text, m.start()),
173
+ "middleware": middleware,
174
+ "handler": handler,
175
+ "has_auth": any("auth" in x.lower() for x in middleware),
176
+ "has_validator": any(
177
+ "valid" in x.lower() for x in middleware
178
+ ),
179
+ }
180
+ )
181
+ return rows
182
+
183
+
184
+ def main():
185
+ ap = argparse.ArgumentParser(description=__doc__)
186
+ ap.add_argument("src", help="backend source directory (the one containing app.js and routes/)")
187
+ ap.add_argument("--json", action="store_true", help="emit JSON instead of markdown")
188
+ args = ap.parse_args()
189
+
190
+ src = Path(args.src)
191
+ if not src.is_dir():
192
+ sys.exit(f"not a directory: {src}")
193
+
194
+ entry = next((src / n for n in ENTRYPOINT_NAMES if (src / n).exists()), None)
195
+ if entry is None:
196
+ sys.exit(f"no app entrypoint found in {src} (looked for {', '.join(ENTRYPOINT_NAMES)})")
197
+
198
+ entry_text = entry.read_text(encoding="utf-8", errors="replace")
199
+ imports = parse_imports(entry_text)
200
+ mounts, prefix_middleware = parse_mounts(entry_text, imports)
201
+
202
+ rows = []
203
+
204
+ # Routes defined inline in the entrypoint. These are worth flagging on sight:
205
+ # an endpoint outside routes/ has no controller and usually no middleware.
206
+ for r in parse_routes(entry_text, entry.name, ""):
207
+ r["inline_in_entrypoint"] = True
208
+ rows.append(r)
209
+
210
+ routes_dir = src / "routes"
211
+ if routes_dir.is_dir():
212
+ by_source = {}
213
+ for ident, prefix in mounts.items():
214
+ source = imports.get(ident, "")
215
+ by_source[Path(source).name] = prefix
216
+
217
+ for f in sorted(routes_dir.iterdir()):
218
+ if f.suffix not in (".js", ".ts", ".mjs"):
219
+ continue
220
+ prefix = by_source.get(f.name)
221
+ if prefix is None:
222
+ prefix = ""
223
+ text = f.read_text(encoding="utf-8", errors="replace")
224
+ for r in parse_routes(text, f.name, prefix):
225
+ r["inline_in_entrypoint"] = False
226
+ r["mounted"] = f.name in by_source
227
+ rows.append(r)
228
+
229
+ if args.json:
230
+ print(json.dumps({"routes": rows, "prefix_middleware": prefix_middleware}, indent=2))
231
+ return
232
+
233
+ print(f"# Route inventory โ€” {src}\n")
234
+ print(f"Entrypoint: `{entry.name}` ยท {len(rows)} routes\n")
235
+ print("| # | Method | Path (as mounted) | File:line | Middleware | Handler | Auth | Validator |")
236
+ print("|---|---|---|---|---|---|---|---|")
237
+ for i, r in enumerate(rows, 1):
238
+ mw = ", ".join(f"`{x}`" for x in r["middleware"]) or "โ€”"
239
+ note = " **(inline)**" if r["inline_in_entrypoint"] else ""
240
+ print(
241
+ f"| {i} | {r['method']} | `{r['path']}`{note} | `{r['file']}:{r['line']}` "
242
+ f"| {mw} | `{r['handler']}` | {'yes' if r['has_auth'] else 'โ€”'} "
243
+ f"| {'yes' if r['has_validator'] else 'โ€”'} |"
244
+ )
245
+
246
+ with_validator = sum(1 for r in rows if r["has_validator"])
247
+ print(f"\n**{with_validator} of {len(rows)} routes have a validator.**")
248
+
249
+ unmounted = sorted({r["file"] for r in rows if not r["inline_in_entrypoint"] and not r.get("mounted")})
250
+ if unmounted:
251
+ print(
252
+ f"\n> Could not resolve a mount prefix for: {', '.join(f'`{u}`' for u in unmounted)}. "
253
+ "Paths above are unprefixed โ€” resolve by hand before using."
254
+ )
255
+
256
+ if prefix_middleware:
257
+ print("\n## Middleware applied by path prefix\n")
258
+ print(
259
+ "Applied in the entrypoint rather than in a route file, so it is invisible "
260
+ "when reading the route. `app.use` matches all methods and all subpaths.\n"
261
+ )
262
+ print("| Prefix | Middleware | Entrypoint line |")
263
+ print("|---|---|---|")
264
+ for prefix, ident, line in prefix_middleware:
265
+ print(f"| `{prefix}` | `{ident}` | {line} |")
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()