@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,143 @@
1
+ ---
2
+ name: murtaza-breif
3
+ description: Explain what a source file does and how its code actually works, as a two-row Markdown table — one row plain English, one row Hinglish (Hindi in Roman script, casual dev tone) — capped at 50 words per language, then save it into FILE_BRIEFS.md at the project root. Use this skill whenever the user says "murtaza breif", "brief this file", "/murtaza-breif", or points at a file or directory and asks for a quick/short/one-line explanation, a summary, a TL;DR, "what does this file do", "explain this in Hinglish", or "samjhao". Use it even when they only name a path and say "brief" with no other instruction. It explains files and records the briefs; it does not review, refactor, or change code.
4
+ ---
5
+
6
+ # Brief a file, in English and Hinglish
7
+
8
+ Someone has landed on an unfamiliar file and wants to know, in the time it takes to read a
9
+ tweet, what it is for and how it pulls that off. Two rows: English, then Hinglish. Fifty words
10
+ each, hard cap. Every brief is also recorded in `FILE_BRIEFS.md` at the project root, so the
11
+ document slowly becomes a map of the codebase rather than a one-off answer.
12
+
13
+ ## Pick the file
14
+
15
+ Use the path the user gave. If they said "this file" with no path, use the file most recently
16
+ opened or discussed in the conversation. If nothing is in play at all, ask which file — guessing
17
+ wastes everyone's time.
18
+
19
+ If they point at a **directory**, brief every source file in it, one table each, in the order the
20
+ files are listed. Don't ask which one they meant — they asked for the directory.
21
+
22
+ Read the **whole** file before writing, not the first screenful. Skimming the top and inferring
23
+ the rest from the filename is exactly how these summaries go wrong. If the file imports project
24
+ modules whose behaviour you'd be guessing at, open them too — you are describing what this file
25
+ actually does, and that often lives one hop away.
26
+
27
+ ## What goes in the 50 words
28
+
29
+ Two things, and roughly this split:
30
+
31
+ - **~20 words — what it's for.** Why the file exists, who calls it, what it guards or produces.
32
+ - **~30 words — how the code works.** The actual mechanism: the key step, the branch that matters,
33
+ what it returns or hands off, the failure path.
34
+
35
+ The "how" gets the bigger share on purpose. Someone can guess "what" from the filename;
36
+ they cannot guess that every auth failure returns the same 401 on purpose. Spend the words on
37
+ what's genuinely non-obvious — a surprising decision, a gotcha, a deliberate trade-off — because
38
+ that is the part that saves the reader from opening the file themselves.
39
+
40
+ Skip the ceremony: imports, exports, boilerplate, and "this file contains a function that..."
41
+ Lead with the verb.
42
+
43
+ ## Tone
44
+
45
+ Write like a colleague explaining it at your desk, not like generated documentation. Concrete
46
+ over abstract — "reads the Bearer token from the header" beats "handles authentication concerns."
47
+
48
+ Keep jargon low. Framework nouns everyone knows (route, token, middleware, database, request) are
49
+ fine and stay in English. But if the file leans on an idiom the reader may not have met — a
50
+ promise chain, a curried wrapper, a stream pipe — spend three words saying what it does in plain
51
+ language rather than naming the pattern and moving on.
52
+
53
+ ## Hinglish is a retelling, not a translation
54
+
55
+ The most common failure here is translating the English row word by word. That produces stiff,
56
+ robotic Hindi that no developer speaks and nobody enjoys reading.
57
+
58
+ Instead, explain the same file again from scratch, the way you'd say it out loud to an Indian dev
59
+ sitting next to you. Hindi carries the connective tissue — *matlab, phir, agar, toh, karta hai,
60
+ milta hai, sambhal leta hai*. English carries the technical nouns — *token, request, middleware,
61
+ database, header*. Nobody translates those and translating them makes the sentence unreadable.
62
+
63
+ Roman script only, never Devanagari. Casual is right; the reader is a peer, not a client. The two
64
+ rows will not be sentence-for-sentence parallel, and that's the point — each should read naturally
65
+ in its own register.
66
+
67
+ ## Count the words
68
+
69
+ Fifty is a cap, not a target — 40 clear words beat 50 padded ones. Count whitespace-separated
70
+ tokens, ignoring standalone punctuation: a clause-separating em-dash is not a word. A path or
71
+ identifier like `req.user` or `auth.middleware.js` counts as one. Count before you output, and if
72
+ you're over, cut a clause rather than shaving articles into a telegram.
73
+
74
+ ## Save the brief
75
+
76
+ After showing each table, record it by running the bundled script from the project root:
77
+
78
+ ```bash
79
+ python3 .claude/skills/murtaza-breif/scripts/save_brief.py \
80
+ --file <path/to/file.ext> \
81
+ --english "<the English row>" \
82
+ --hinglish "<the Hinglish row>"
83
+ ```
84
+
85
+ Run it once per briefed file. It writes to `FILE_BRIEFS.md` at the root of whichever git repo you
86
+ are working in, creating the file on first use. Re-briefing a file **replaces** its existing entry
87
+ rather than appending a second one, so the document stays a current index instead of a changelog —
88
+ which means you can safely re-brief a file after it changes.
89
+
90
+ Use the script rather than editing `FILE_BRIEFS.md` by hand. It enforces the 50-word cap (and
91
+ refuses the write if you're over, which is your cue to cut a clause and rerun), rejects summaries
92
+ containing a `|` that would break the table, and finds the repo root correctly even though
93
+ `.claude/skills` is a symlink into a shared skills repo.
94
+
95
+ If the script reports an error, fix the summary and rerun it — don't fall back to hand-editing the
96
+ file, because that is how duplicate entries and broken tables get in.
97
+
98
+ ## Output format
99
+
100
+ Heading naming the file, the table, then a single line confirming where it was saved. Nothing
101
+ else — no preamble, no analysis, no follow-up commentary. When briefing several files, repeat the
102
+ heading-and-table pair for each, and put one save confirmation at the end covering all of them.
103
+
104
+ ```markdown
105
+ ## Brief: `<path/to/file.ext>`
106
+
107
+ | Language | Summary |
108
+ | --- | --- |
109
+ | English | ... |
110
+ | Hinglish | ... |
111
+
112
+ Saved to `FILE_BRIEFS.md`.
113
+ ```
114
+
115
+ ## Examples
116
+
117
+ **Example 1** — `Backend/src/utils/asyncHandler.js`, a five-line wrapper.
118
+
119
+ ## Brief: `Backend/src/utils/asyncHandler.js`
120
+
121
+ | Language | Summary |
122
+ | --- | --- |
123
+ | English | Wraps an async route handler so its rejected promises don't vanish. Instead of every controller needing its own try/catch, asyncHandler calls the function, catches any error, and passes it to Express's `next`, which hands it to the central error middleware. Five lines, used everywhere. |
124
+ | Hinglish | Async controller ko wrap karta hai taaki error kahin gum na ho jaaye. Har controller mein alag try/catch likhne ki zaroorat nahi — ye function chalata hai, error aaye toh seedha Express ke `next` ko de deta hai, aur central error middleware sambhal leta hai. |
125
+
126
+ Saved to `FILE_BRIEFS.md`.
127
+
128
+ Note what the "how" half buys the reader: the point of the file is the try/catch they now don't
129
+ have to write. That's the insight, so it gets the words.
130
+
131
+ **Example 2** — `Backend/src/middleware/auth.middleware.js`, a JWT gate.
132
+
133
+ ## Brief: `Backend/src/middleware/auth.middleware.js`
134
+
135
+ | Language | Summary |
136
+ | --- | --- |
137
+ | English | Gatekeeper for protected routes. Reads the `Bearer` token from the Authorization header, verifies its signature with `JWT_SECRET`, then loads that user from MongoDB. Any failure — missing token, bad token, deleted user — returns the same 401 so attackers learn nothing. On success it attaches `req.user` and continues. |
138
+ | Hinglish | Protected routes ka darban. Authorization header se `Bearer` token nikaalta hai, `JWT_SECRET` se verify karta hai, user ko MongoDB se laata hai. Token missing ho, galat ho ya user delete ho gaya ho — teeno mein same 401, taaki hacker ko hint na mile. Sahi nikla toh `req.user` set hota hai. |
139
+
140
+ Saved to `FILE_BRIEFS.md`.
141
+
142
+ The identical-401 detail is a deliberate design decision that is invisible from the filename —
143
+ worth more than restating that it's authentication middleware.
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """Upsert one file's brief into FILE_BRIEFS.md at the project root.
3
+
4
+ Re-briefing a file replaces its existing entry rather than appending a
5
+ duplicate, so the document stays a current index instead of a changelog.
6
+
7
+ Usage:
8
+ python3 save_brief.py --file Backend/src/config/db.js \
9
+ --english "..." --hinglish "..."
10
+ """
11
+ import argparse
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ def find_repo_root(start=None):
17
+ """Walk up from the working directory to the enclosing git repo.
18
+
19
+ Deliberately not derived from __file__: .claude/skills is a symlink into a
20
+ shared skills repo, so resolving the script's own path lands in a different
21
+ project entirely. The brief belongs to the repo being briefed, which is the
22
+ one we are running in.
23
+ """
24
+ here = (start or Path.cwd()).resolve()
25
+ for d in [here, *here.parents]:
26
+ if (d / ".git").exists():
27
+ return d
28
+ return here
29
+
30
+ HEADER = """# File briefs
31
+
32
+ Short English + Hinglish summaries of individual files, written by the
33
+ `murtaza-breif` skill. Each entry is capped at 50 words per language.
34
+ Re-briefing a file replaces its entry in place.
35
+ """
36
+
37
+ WORD_CAP = 50
38
+
39
+
40
+ def count_words(s):
41
+ """Words, ignoring standalone punctuation.
42
+
43
+ A bare em-dash between clauses is whitespace-separated but is not a word,
44
+ and counting it makes a 50-word summary read as 51.
45
+ """
46
+ return len([w for w in re.split(r"\s+", s.strip())
47
+ if w and re.search(r"[A-Za-z0-9]", w)])
48
+
49
+
50
+ def render(rel_path, english, hinglish):
51
+ return (
52
+ f"## `{rel_path}`\n\n"
53
+ "| Language | Summary |\n"
54
+ "| --- | --- |\n"
55
+ f"| English | {english} |\n"
56
+ f"| Hinglish | {hinglish} |\n"
57
+ )
58
+
59
+
60
+ def upsert(BRIEFS, rel_path, english, hinglish):
61
+ entry = render(rel_path, english, hinglish)
62
+
63
+ if not BRIEFS.exists():
64
+ BRIEFS.write_text(HEADER + "\n" + entry)
65
+ return "created"
66
+
67
+ text = BRIEFS.read_text()
68
+
69
+ # Match this file's section up to the next "## " heading or end of file.
70
+ pattern = re.compile(
71
+ r"^##\s+`" + re.escape(rel_path) + r"`\s*$.*?(?=^##\s|\Z)",
72
+ re.M | re.S,
73
+ )
74
+ if pattern.search(text):
75
+ text = pattern.sub(entry + "\n", text, count=1)
76
+ BRIEFS.write_text(text)
77
+ return "updated"
78
+
79
+ if not text.endswith("\n"):
80
+ text += "\n"
81
+ BRIEFS.write_text(text + "\n" + entry)
82
+ return "added"
83
+
84
+
85
+ def main():
86
+ ap = argparse.ArgumentParser()
87
+ ap.add_argument("--file", required=True,
88
+ help="Path to the briefed file, absolute or repo-relative")
89
+ ap.add_argument("--english", required=True)
90
+ ap.add_argument("--hinglish", required=True)
91
+ ap.add_argument("--repo-root", default=None,
92
+ help="Override the project root (defaults to the enclosing git repo)")
93
+ args = ap.parse_args()
94
+
95
+ REPO_ROOT = Path(args.repo_root).resolve() if args.repo_root else find_repo_root()
96
+ BRIEFS = REPO_ROOT / "FILE_BRIEFS.md"
97
+
98
+ p = Path(args.file)
99
+ if p.is_absolute():
100
+ try:
101
+ rel = p.resolve().relative_to(REPO_ROOT).as_posix()
102
+ except ValueError:
103
+ sys.exit(f"error: {p} is outside the project at {REPO_ROOT}")
104
+ else:
105
+ rel = p.as_posix()
106
+
107
+ if not (REPO_ROOT / rel).exists():
108
+ sys.exit(f"error: no such file in project: {rel}")
109
+
110
+ english = " ".join(args.english.split())
111
+ hinglish = " ".join(args.hinglish.split())
112
+
113
+ if "|" in english or "|" in hinglish:
114
+ sys.exit("error: summaries cannot contain '|' — it breaks the table")
115
+
116
+ ew, hw = count_words(english), count_words(hinglish)
117
+ over = [f"{name} is {n} words" for name, n in
118
+ (("English", ew), ("Hinglish", hw)) if n > WORD_CAP]
119
+ if over:
120
+ sys.exit(f"error: over the {WORD_CAP}-word cap ({'; '.join(over)}). "
121
+ "Cut a clause and rerun.")
122
+
123
+ action = upsert(BRIEFS, rel, english, hinglish)
124
+ print(f"{action}: {rel} ({ew}/{hw} words) -> {BRIEFS}")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -0,0 +1,42 @@
1
+ ---
2
+
3
+ name: pdf-to-json
4
+ description: Use and invoke this skill whenever a PDF must be converted into structured JSON/Markdown, including catalogues, price lists, quotes, invoices, BOQs, tables, or product images.
5
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
6
+
7
+ # PDF to JSON
8
+
9
+ **Goal:** Convert PDF → JSON + Markdown using Datalab with reliable tables, categories, and optional images.
10
+
11
+ **Workflow**
12
+
13
+ 1. Submit PDF to Datalab and poll until complete.
14
+ 2. Process typed blocks recursively: Page, Text, SectionHeader, ListGroup, Table, Picture, Figure, Equation, Code.
15
+ 3. Preserve pixel `bbox` as `[x0,y0,x1,y1]`; never treat it as spreadsheet coordinates.
16
+ 4. Render Markdown locally from block HTML; do not request a second conversion.
17
+ 5. Repair fragmented/misaligned tables, carry valid headers, remove repeated header rows.
18
+ 6. Infer categories only from document evidence: cell, rowspan, margin label, then carry-forward.
19
+ 7. Use `--page-images` for embedded/in-cell images; map images by physical SKU position, not equal row bands.
20
+ 8. Validate and report missing, inferred, unanchored, ambiguous, and rejected data.
21
+
22
+ **Commands**
23
+ `python scripts/pdf_extract.py "<pdf-or-dir>" [--out-dir DIR] [--mode balanced]`
24
+ `python scripts/load_mongo.py "<abs-path>/<stem>.json" --collection <name>`
25
+
26
+ **Rules**
27
+
28
+ * Datalab `block_type` is authoritative; block IDs are unstable.
29
+ * Keep Datalab-generated image descriptions marked `datalab-generated`.
30
+ * Never assume table fragments share schemas or that equal widths share meaning.
31
+ * Carry headers only from tables containing headers + rows; use `none` when alignment is uncertain.
32
+ * Preserve `category_raw` and `category_fill`; never blindly forward-fill categories.
33
+ * Use rendered-page crops for images to preserve masks/backgrounds.
34
+ * Never interpolate rows whose SKU cannot be located; count them as unanchored.
35
+ * Cache raw Datalab responses; use `--refresh` when source/options change.
36
+ * Use `--force-ocr` for scanned/garbled PDFs; `--use-llm` only for difficult merged-cell tables.
37
+ * Never run MongoDB writes automatically; output the command with an explicit collection placeholder.
38
+ * Use `--dry-run`/`--check` before writes and verify the collection afterward.
39
+
40
+ **Validation:** Report tables/rows, stated vs carried categories, failed pages, dropped headers, alignment decisions, image coverage, unanchored rows, duplicates/rejects, and judgement calls.
41
+
42
+ **Wrong Tool:** XLSX/XLS/CSV → `datalab-api`; plain text-only PDF → `pdftotext`; use Datalab when structure, tables, or images matter.
@@ -0,0 +1,168 @@
1
+ # Shape of the generated `<stem>.json`
2
+
3
+ Read this when you need to consume the JSON programmatically rather than just
4
+ hand it to someone. Everything here is produced by `scripts/pdf_extract.py`.
5
+
6
+ ```jsonc
7
+ {
8
+ "source": {
9
+ "file": "price-list.pdf",
10
+ "path": "/abs/path/price-list.pdf",
11
+ "bytes": 1776420,
12
+ "sha256": "…", // identifies the exact input across re-runs
13
+ "extracted_at": "2026-09-05T06:39:41+00:00"
14
+ },
15
+
16
+ "extraction": {
17
+ "provider": "datalab.to/api/v1/convert",
18
+ "output_format": "json",
19
+ "mode": "fast", // fast | balanced | accurate
20
+ "page_range": null, // whatever --pages was given
21
+ "max_pages": 6,
22
+ "force_ocr": false,
23
+ "use_llm": false,
24
+ "request_id": "…", // quote this when reporting a bad extraction
25
+ "runtime_seconds": 24.7, // ~0.2 if Datalab served it from its own cache
26
+ "cost_cents": 3.0, // 0 on a server-side cache hit
27
+ "page_count": 6,
28
+ "failed_pages": [] // non-empty means data is missing, not just messy
29
+ },
30
+
31
+ "summary": {
32
+ "pages": 6,
33
+ "blocks": 26,
34
+ "tables": 9,
35
+ "figures": 7,
36
+ "text_characters": 998,
37
+ "block_types": { "Page": 6, "Table": 9, "Picture": 7, "Text": 8 },
38
+ "table_rows": 213, // data rows across every table
39
+ "header_sources": { "own": 3, "carried-trimmed": 7 },
40
+ // The next two keys are absent when no table has a grouping column.
41
+ "category_fill": { "own": 7, "rowspan": 124, "margin-label": 53,
42
+ "carried-forward": 5, "carried-back": 24 },
43
+ "categories": { "Screw Drivers": 86, "Levels": 32 }, // rows per group
44
+ // Present only with --page-images. `rows_without_image` are rows the
45
+ // document itself gives no photo; `rows_unanchored` are rows whose SKU
46
+ // could not be found on the page, so they were skipped rather than guessed.
47
+ "page_images": { "extracted": 328, "dpi": 200, "rows_with_image": 785,
48
+ "rows_without_image": 27, "rows_anchored": 801,
49
+ "rows_unanchored": 11, "unplaced_images": 9 }
50
+ },
51
+
52
+ "pages": [
53
+ {
54
+ "page": 1, // 1-based, unlike Datalab's 0-based ids
55
+ "size": { "width": 1064, "height": 1484 },
56
+ "blocks": [
57
+ { "id": "/page/0/SectionHeader/0", "type": "SectionHeader",
58
+ "bbox": [122, 77, 929, 163], "text": "…" },
59
+ { "id": "/page/0/Table/1", "type": "Table",
60
+ "bbox": [74, 246, 995, 310], "rows": 64, "columns": 9,
61
+ "has_header": true },
62
+ { "id": "/page/0/Picture/2", "type": "Picture",
63
+ "bbox": [104, 136, 2023, 1307],
64
+ "images": ["2dfa…_img.jpg"],
65
+ "description": "Stanley logo on yellow", // NOT text from the PDF
66
+ "description_source": "datalab-generated" }
67
+ ]
68
+ }
69
+ ],
70
+
71
+ "tables": [
72
+ {
73
+ "id": "/page/1/Table/0",
74
+ "page": 2,
75
+ "bbox": [74, 246, 995, 310],
76
+ "columns": 9, // after realignment — matches headers
77
+ "columns_raw": 8, // what the block itself had
78
+ "row_count": 64, // data rows only — never includes the header
79
+ "has_header": true, // did this block carry its own <th> row
80
+ "header_source": "own", // own | carried:<id> | carried-trimmed:<id> | none
81
+ // `carried` means the fragment fit unchanged;
82
+ // both are content-checked before being accepted
83
+ // present only when columns had to be moved; `dropped_columns` indexes
84
+ // into the block's own grid and is only ever entirely-empty columns
85
+ "alignment": { "mode": "pad", "left": 1, "right": 0,
86
+ "dropped_columns": [], "confidence": 0.9 },
87
+ "dropped_header_rows": 1, // reprinted header lines removed from the body
88
+ "group_column": { "index": 0, "name": "Category" },
89
+ // --page-images only, positional like `categories`: the photos beside
90
+ // row i. A photo merged over a run of products repeats on each row.
91
+ "row_images": [["p02_STHT33463-8.png"], []],
92
+ "image_column": 8, // which column the PDF drew the photo in
93
+
94
+ "headers": ["Category", "Sku", "…", ""],
95
+ "rows_data": [["Short Tapes", "STHT33203-8", "…"]], // realigned, as printed
96
+ "categories": ["Short Tapes", "Short Tapes"], // filled, one per row
97
+ "category_raw": ["Short Tapes", ""], // what the cell printed
98
+ "category_fill": ["own", "rowspan"],
99
+ "records": [{ "Category": "Short Tapes", "Sku": "STHT33203-8",
100
+ "category": "Short Tapes", "category_raw": "Short Tapes",
101
+ "category_fill": "own" }]
102
+ }
103
+ ],
104
+
105
+ "figures": [
106
+ // Datalab's own Picture/Figure blocks. `file` appears with --page-images,
107
+ // which renders them from the page so the Markdown link always resolves.
108
+ { "name": "2dfa…_img.jpg", "page": 1, "block_id": "/page/0/Picture/2",
109
+ "bbox": [104, 136, 2023, 1307], "source": "datalab",
110
+ "file": "price-list_images/2dfa….png", "file_source": "pdf-render" },
111
+
112
+ // --page-images only: cut out of the rendered page, not from Datalab.
113
+ { "name": "p02_STHT33463-8.png", "page": 2, "source": "pdf-render",
114
+ "file": "price-list_images/p02_STHT33463-8.png",
115
+ "bbox_pdf": [422.1, 175.7, 470.9, 212.8], // PDF points
116
+ "bbox": [733, 305, 818, 370], // same box in Datalab pixels
117
+ "rows": [{ "table": "/page/1/Table/0", "row": 3, "sku": "STHT33463-8" }] }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ## Reading it without getting caught out
123
+
124
+ `rows_data` is the authoritative row list; `records` is a convenience that is
125
+ **absent whenever headers are duplicated**, so branch on its presence rather
126
+ than assuming it. Write `table.get("records") or rows_via(table["headers"],
127
+ table["rows_data"])`, never `table["records"]`.
128
+
129
+ To count line items across a document, use `summary.table_rows`, or sum
130
+ `row_count` over the tables — the header row is already excluded everywhere, and
131
+ reprinted header lines have been removed, so there is no off-by-one to correct.
132
+ Tables whose `header_source` starts with `carried` are continuations of an
133
+ earlier table, so a document with one visual table can still list ten entries
134
+ here. After realignment every table in a chain has the same `columns`, so
135
+ grouping consecutive tables by `columns` recovers the logical table.
136
+
137
+ `rows_data` is realigned to match `headers`, not a verbatim copy of the block's
138
+ own grid — `columns_raw` and `alignment` record what was changed. The transform
139
+ is lossless: padding inserts empty cells for columns the fragment never had, and
140
+ trimming only ever removes columns that were entirely empty.
141
+
142
+ The three category arrays are positional — index `i` of `categories`,
143
+ `category_raw` and `category_fill` describes row `i` of `rows_data`. They are
144
+ absent on tables with no grouping column. Filter on `category_fill` when the
145
+ distinction matters: `own`, `rowspan` and `margin-label` come from the document,
146
+ while `carried-forward` and `carried-back` are inferred from neighbouring rows.
147
+
148
+ `bbox` is `[x0, y0, x1, y1]` in page pixels, top-left origin. Divide by the
149
+ page's `size` to get fractions if you need coordinates that survive a different
150
+ render resolution.
151
+
152
+ Only prose blocks have `text`. Picture and Figure blocks have `description`
153
+ instead, and it is model-generated rather than read off the page — that naming
154
+ split is deliberate, so a pass that collects `text` across blocks cannot
155
+ accidentally sweep generated content into the document body.
156
+
157
+ Figure `name` values match files written under `<stem>_images/` when the script
158
+ ran with `--keep-images`; without that flag the names are still recorded, the
159
+ pixels just aren't on disk. Branch on `file`, not `name`: an entry with `file`
160
+ has bytes you can open, and `source` says whether Datalab found the figure or
161
+ the picture was cut out of the page by `--page-images`.
162
+
163
+ `figures[].rows` and `tables[].row_images` are the same relation read from
164
+ either end, so a catalogue import can walk products and ask for their photos, or
165
+ walk photos and ask which products share them. `rows_without_image` in the
166
+ summary counts rows the *document* gives no picture — it is a fact about the
167
+ PDF, not a failure, and it is the number to quote before someone assumes every
168
+ line item arrived with an image.