@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.
- package/LICENSE +5 -0
- package/README.md +75 -0
- package/bin/install.js +45 -0
- package/package.json +29 -0
- package/skills/architecture-walkthrough/SKILL.md +223 -0
- package/skills/architecture-walkthrough/references/sections.md +29 -0
- package/skills/architecture-walkthrough/scripts/check_structure.py +200 -0
- package/skills/autotest-webapp-ui/SKILL.md +58 -0
- package/skills/backend-code-review/SKILL.md +386 -0
- package/skills/backend-code-review/references/report-format.md +333 -0
- package/skills/backend-code-review/scripts/list_routes.py +269 -0
- package/skills/backend-code-review/scripts/sweep.py +550 -0
- package/skills/backend-code-review/scripts/verify_citations.py +201 -0
- package/skills/be-brief/SKILL.md +18 -0
- package/skills/clarke-list-excel/SKILL.md +51 -0
- package/skills/clarke-list-excel/references/output-schema.md +125 -0
- package/skills/clarke-list-excel/scripts/clarke_common.py +251 -0
- package/skills/clarke-list-excel/scripts/clarke_extract.py +487 -0
- package/skills/clarke-list-excel/scripts/load_clarke.py +322 -0
- package/skills/clarke-list-excel/scripts/run_all.py +63 -0
- package/skills/datalab-api/SKILL.md +163 -0
- package/skills/datalab-api/references/parameters-and-payload.md +121 -0
- package/skills/datalab-api/references/table-selection.md +35 -0
- package/skills/datalab-api/scripts/datalab_tables.py +365 -0
- package/skills/find-test-seam/SKILL.md +41 -0
- package/skills/frontend-code-review/SKILL.md +247 -0
- package/skills/frontend-code-review-2/SKILL.md +192 -0
- package/skills/frontend-code-review-2/scripts/fetch_pr_comments.py +65 -0
- package/skills/frontend-code-review-2/scripts/render_report.py +139 -0
- package/skills/murtaza-breif/SKILL.md +143 -0
- package/skills/murtaza-breif/scripts/save_brief.py +128 -0
- package/skills/pdf-to-json/SKILL.md +42 -0
- package/skills/pdf-to-json/references/output-schema.md +168 -0
- package/skills/pdf-to-json/scripts/extract_figures.py +319 -0
- package/skills/pdf-to-json/scripts/load_mongo.py +287 -0
- package/skills/pdf-to-json/scripts/pdf_extract.py +1313 -0
- package/skills/record-api-traffic/SKILL.md +434 -0
- package/skills/record-api-traffic/references/reading-recordings.md +224 -0
- package/skills/record-api-traffic/scripts/check-schema.mjs +184 -0
- package/skills/record-api-traffic/scripts/dump-quotation.mjs +67 -0
- package/skills/record-api-traffic/scripts/dump-source-excel.mjs +75 -0
- package/skills/record-api-traffic/scripts/lib/repo.mjs +109 -0
- package/skills/record-api-traffic/scripts/preflight.py +528 -0
- package/skills/record-api-traffic/scripts/record-api-traffic.py +720 -0
- package/skills/refac-wrt-business-goal/SKILL.md +305 -0
- package/skills/refac-wrt-business-goal/references/critic.md +170 -0
- package/skills/system-resource-triage/SKILL.md +180 -0
- package/skills/system-resource-triage/scripts/reap.sh +116 -0
- package/skills/system-resource-triage/scripts/triage.sh +111 -0
- 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.
|