@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,35 @@
1
+ # Automating table selection with a model call
2
+
3
+ Read this when you need to pick the line-item table across many workbooks
4
+ programmatically. For a one-off, just read the previews and choose — the
5
+ principles in SKILL.md are the whole method, and this file only adds the
6
+ plumbing.
7
+
8
+ Send a prose description of the target plus a short preview of each table, and
9
+ constrain the output to ids so what comes back is something you can look up:
10
+
11
+ ```python
12
+ from pydantic import BaseModel
13
+
14
+ class Selection(BaseModel):
15
+ table_ids: list[str]
16
+ reasoning: str
17
+
18
+ response = client.messages.parse(
19
+ model="claude-opus-4-7",
20
+ max_tokens=4000,
21
+ system=[{"type": "text", "text": SYSTEM, "cache_control": {"type": "ephemeral"}}],
22
+ thinking={"type": "adaptive"},
23
+ output_config={"effort": "high"},
24
+ messages=[{"role": "user", "content": prompt}],
25
+ output_format=Selection,
26
+ )
27
+ ```
28
+
29
+ Build `prompt` from the `--preview` output: each table labelled with its exact
30
+ `id`, five rows, cells truncated to ~40 characters. Cache the system prompt when
31
+ looping over a directory — the target description is identical across files and
32
+ only the previews change.
33
+
34
+ `reasoning` is worth keeping even though you don't act on it. When a selection
35
+ looks wrong, it tells you whether the description or the preview was at fault.
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env python3
2
+ """Extract table blocks from spreadsheets via the Datalab Convert API.
3
+
4
+ datalab_tables.py <file-or-dir> [--json out.json] [--preview out.md]
5
+
6
+ For each input file: submit to Datalab, poll until done, walk the block tree
7
+ for `block_type: "Table"`, and report each table's id, source cell range, and
8
+ size. Raw responses are cached on disk, so re-runs cost no credits.
9
+
10
+ Needs DATALAB_API_KEY in the environment or in a .env file found by walking up
11
+ from the current directory.
12
+ """
13
+
14
+ import argparse
15
+ import hashlib
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+ import time
21
+ from html.parser import HTMLParser
22
+ from pathlib import Path
23
+
24
+ import requests
25
+
26
+ API_URL = "https://www.datalab.to/api/v1/convert"
27
+ MODES = ("fast", "balanced", "accurate")
28
+ POLL_SECONDS = 2
29
+ POLL_ATTEMPTS = 300
30
+
31
+ MIME_TYPES = {
32
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
33
+ ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
34
+ ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
35
+ ".xls": "application/vnd.ms-excel",
36
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
37
+ ".csv": "text/csv",
38
+ }
39
+
40
+ PREVIEW_HEAD = 5
41
+ PREVIEW_TAIL = 3
42
+ CELL_CHARS = 28
43
+
44
+
45
+ # ----------------------------------------------------------------- auth
46
+
47
+ def api_key() -> str:
48
+ key = os.environ.get("DATALAB_API_KEY")
49
+ if key:
50
+ return key
51
+ for directory in [Path.cwd(), *Path.cwd().parents]:
52
+ for name in (".env.local", ".env"):
53
+ env = directory / name
54
+ if not env.exists():
55
+ continue
56
+ for line in env.read_text().splitlines():
57
+ if line.startswith("DATALAB_API_KEY="):
58
+ return line.split("=", 1)[1].strip().strip("'\"")
59
+ raise SystemExit("DATALAB_API_KEY not found in environment or any parent .env file")
60
+
61
+
62
+ # ------------------------------------------------------------ api access
63
+
64
+ def strip_images(node):
65
+ """Drop base64 image payloads.
66
+
67
+ Datalab always returns images for spreadsheets and `disable_image_extraction`
68
+ does not change that, so this is the only way to keep responses small. They
69
+ are stored twice: under the top-level `images` key and inside each Picture
70
+ block. On image-heavy sheets this is ~98% of the bytes.
71
+
72
+ Images are one-per-row product photos on many BOQ-style sheets. If you need
73
+ them, keep the raw response instead of stripping.
74
+ """
75
+ if isinstance(node, list):
76
+ for item in node:
77
+ strip_images(item)
78
+ elif isinstance(node, dict):
79
+ node.pop("images", None)
80
+ strip_images(node.get("children") or [])
81
+ strip_images(node.get("json") or [])
82
+ return node
83
+
84
+
85
+ def cache_path(path: Path, mode: str, cache_dir: Path, keep_images: bool) -> Path:
86
+ """Cache filename that can't serve one file's response for another.
87
+
88
+ The stem alone collides: a/report.xlsx, b/report.xlsx and report.csv would
89
+ share an entry and silently return each other's tables. keep_images belongs
90
+ in the key too, or the flag returns stripped data from an earlier run and
91
+ appears to do nothing.
92
+ """
93
+ digest = hashlib.sha1(str(path.resolve()).encode()).hexdigest()[:8]
94
+ images = ".img" if keep_images else ""
95
+ return cache_dir / f"{path.stem}.{mode}{images}.{digest}.json"
96
+
97
+
98
+ def convert(path: Path, key: str, mode: str, cache_dir: Path, keep_images: bool) -> dict:
99
+ """Submit one file and poll until the conversion completes."""
100
+ cache = cache_path(path, mode, cache_dir, keep_images)
101
+ if cache.exists():
102
+ return json.loads(cache.read_text())
103
+
104
+ mime = MIME_TYPES.get(path.suffix.lower(), "application/octet-stream")
105
+ headers = {"X-API-Key": key}
106
+ with path.open("rb") as fh:
107
+ resp = requests.post(
108
+ API_URL,
109
+ headers=headers,
110
+ files={"file": (path.name, fh, mime)},
111
+ data={"output_format": "json", "mode": mode},
112
+ timeout=120,
113
+ )
114
+ resp.raise_for_status()
115
+ submitted = resp.json()
116
+ if not submitted.get("success"):
117
+ raise SystemExit(f"{path.name}: submit failed: {submitted.get('error')}")
118
+
119
+ check_url = submitted["request_check_url"]
120
+ for _ in range(POLL_ATTEMPTS):
121
+ result = requests.get(check_url, headers=headers, timeout=60).json()
122
+ if result.get("status") == "complete":
123
+ if not keep_images:
124
+ strip_images(result)
125
+ cache_dir.mkdir(parents=True, exist_ok=True)
126
+ cache.write_text(json.dumps(result, indent=2))
127
+ return result
128
+ if result.get("status") == "failed" or result.get("error"):
129
+ raise SystemExit(f"{path.name}: conversion failed: {result.get('error')}")
130
+ time.sleep(POLL_SECONDS)
131
+ raise SystemExit(f"{path.name}: timed out after {POLL_ATTEMPTS * POLL_SECONDS}s")
132
+
133
+
134
+ # ---------------------------------------------------------- block walking
135
+
136
+ def find_tables(node, page=None, out=None):
137
+ """Depth-first walk of the block tree, collecting Table blocks.
138
+
139
+ Blocks nest arbitrarily, so recursion is the reliable way to reach them.
140
+ TableOfContents is included because Datalab sometimes classifies a plain
141
+ lead-in table that way.
142
+ """
143
+ if out is None:
144
+ out = []
145
+ if isinstance(node, list):
146
+ for item in node:
147
+ find_tables(item, page, out)
148
+ return out
149
+ if not isinstance(node, dict):
150
+ return out
151
+
152
+ block_type = node.get("block_type")
153
+ if block_type == "Page":
154
+ page = node.get("id", page)
155
+ if block_type in ("Table", "TableOfContents") and node.get("html"):
156
+ out.append({
157
+ "id": node.get("id"),
158
+ "page": page,
159
+ "html": node["html"],
160
+ "bbox": node.get("bbox"),
161
+ })
162
+ find_tables(node.get("children") or [], page, out)
163
+ return out
164
+
165
+
166
+ # ----------------------------------------------------------- html -> grid
167
+
168
+ class TableParser(HTMLParser):
169
+ """Flatten an HTML table into a list of row-lists of cell strings.
170
+
171
+ Cells are placed by occupancy rather than appended, so `colspan` and
172
+ `rowspan` consume the columns they actually cover. These sheets merge
173
+ header cells constantly, and appending would shift every later cell left —
174
+ a header spanning A:C followed by one over D would report that second
175
+ header as column 2 and misalign it against the data beneath.
176
+
177
+ A merged cell puts its text in the first covered position and leaves the
178
+ rest empty, which is how openpyxl reads the same merge in the source.
179
+ """
180
+
181
+ def __init__(self):
182
+ super().__init__()
183
+ self.cells = {}
184
+ self.filled_rows = set()
185
+ self.row_idx = -1
186
+ self.col_idx = 0
187
+ self.cell = None
188
+ self.span = (1, 1)
189
+
190
+ @staticmethod
191
+ def _span(attrs, name):
192
+ try:
193
+ return max(1, int(dict(attrs).get(name, 1)))
194
+ except (TypeError, ValueError):
195
+ return 1
196
+
197
+ def handle_starttag(self, tag, attrs):
198
+ if tag == "tr":
199
+ self.row_idx += 1
200
+ self.col_idx = 0
201
+ elif tag in ("td", "th"):
202
+ self.cell = []
203
+ self.span = (self._span(attrs, "colspan"), self._span(attrs, "rowspan"))
204
+
205
+ def handle_endtag(self, tag):
206
+ if tag not in ("td", "th") or self.cell is None:
207
+ return
208
+ text = re.sub(r"\s+", " ", "".join(self.cell)).strip()
209
+ self.cell = None
210
+ colspan, rowspan = self.span
211
+ while (self.row_idx, self.col_idx) in self.cells:
212
+ self.col_idx += 1
213
+ for dr in range(rowspan):
214
+ for dc in range(colspan):
215
+ self.cells[(self.row_idx + dr, self.col_idx + dc)] = text if dr == dc == 0 else ""
216
+ self.filled_rows.add(self.row_idx)
217
+ self.col_idx += colspan
218
+
219
+ def handle_data(self, data):
220
+ if self.cell is not None:
221
+ self.cell.append(data)
222
+
223
+ def grid(self) -> list[list[str]]:
224
+ if not self.cells:
225
+ return []
226
+ width = max(c for _, c in self.cells) + 1
227
+ rows = sorted({r for r, _ in self.cells} | self.filled_rows)
228
+ return [[self.cells.get((r, c), "") for c in range(width)] for r in rows]
229
+
230
+
231
+ def to_grid(html: str) -> list[list[str]]:
232
+ parser = TableParser()
233
+ parser.feed(html)
234
+ return parser.grid()
235
+
236
+
237
+ # ------------------------------------------------------------- bbox -> A1
238
+
239
+ def col_letter(n: int) -> str:
240
+ """1 -> A, 27 -> AA."""
241
+ out = ""
242
+ while n > 0:
243
+ n, rem = divmod(n - 1, 26)
244
+ out = chr(65 + rem) + out
245
+ return out
246
+
247
+
248
+ def excel_range(bbox) -> str:
249
+ """Spreadsheet bboxes are [col0, row0, col1, row1], 1-indexed inclusive.
250
+
251
+ This is cell coordinates, not pixels or points. Undocumented, but verified
252
+ against openpyxl on real workbooks.
253
+
254
+ Only Table and Text blocks use that convention. A Page bbox is a sheet
255
+ extent starting [0, 0, ...] and a Picture bbox is continuous, so neither is
256
+ a range. Both are rejected here rather than converted, because col_letter(0)
257
+ is the empty string and would otherwise yield a plausible-looking "0:J19".
258
+ """
259
+ if not bbox or len(bbox) != 4:
260
+ return "n/a"
261
+ c0, r0, c1, r1 = (int(v) for v in bbox)
262
+ if min(c0, r0, c1, r1) < 1:
263
+ return "n/a"
264
+ return f"{col_letter(c0)}{r0}:{col_letter(c1)}{r1}"
265
+
266
+
267
+ # -------------------------------------------------------------- reporting
268
+
269
+ def md_row(cells: list[str], index: str = "") -> str:
270
+ safe = [c.replace("|", "\\|").replace("\n", " ")[:CELL_CHARS] or " " for c in cells]
271
+ return "| " + index + " | " + " | ".join(safe) + " |"
272
+
273
+
274
+ def render(grid: list[list[str]]) -> list[str]:
275
+ """Markdown preview: header, first rows and last rows with a gap marker."""
276
+ if not grid:
277
+ return ["_empty table_"]
278
+ headers = [h or f"col_{i}" for i, h in enumerate(grid[0])]
279
+ body = grid[1:]
280
+
281
+ lines = [md_row(headers, "#"), md_row(["---"] * len(headers), "---")]
282
+ if len(body) <= PREVIEW_HEAD + PREVIEW_TAIL:
283
+ shown, gap_after = [(i, r) for i, r in enumerate(body, 1)], None
284
+ else:
285
+ shown = [(i, r) for i, r in enumerate(body[:PREVIEW_HEAD], 1)]
286
+ shown += [(i, r) for i, r in enumerate(body[-PREVIEW_TAIL:], len(body) - PREVIEW_TAIL + 1)]
287
+ gap_after = PREVIEW_HEAD
288
+
289
+ for pos, (num, row) in enumerate(shown):
290
+ if gap_after is not None and pos == gap_after:
291
+ lines.append(md_row(["…"] * len(headers), "…"))
292
+ lines.append(md_row(row, str(num)))
293
+ return lines
294
+
295
+
296
+ def describe(path: Path, tables: list[dict]) -> list[dict]:
297
+ out = []
298
+ for table in tables:
299
+ grid = to_grid(table["html"])
300
+ out.append({
301
+ "id": table["id"],
302
+ "bbox": table["bbox"],
303
+ "excel_range": excel_range(table["bbox"]),
304
+ "rows": max(len(grid) - 1, 0),
305
+ "columns": len(grid[0]) if grid else 0,
306
+ "headers": grid[0] if grid else [],
307
+ })
308
+ return out
309
+
310
+
311
+ def collect_inputs(source: Path) -> list[Path]:
312
+ if source.is_dir():
313
+ return sorted(p for p in source.iterdir() if p.suffix.lower() in MIME_TYPES)
314
+ return [source]
315
+
316
+
317
+ def main():
318
+ parser = argparse.ArgumentParser(description=__doc__,
319
+ formatter_class=argparse.RawDescriptionHelpFormatter)
320
+ parser.add_argument("input", type=Path, help="a spreadsheet file or a directory of them")
321
+ parser.add_argument("--json", type=Path, help="write the table index as JSON")
322
+ parser.add_argument("--preview", type=Path, help="write a markdown preview of every table")
323
+ parser.add_argument("--mode", choices=MODES, default="fast",
324
+ help="Datalab mode (default: fast; has no effect on spreadsheets)")
325
+ parser.add_argument("--cache-dir", type=Path, default=Path("datalab_raw"),
326
+ help="where to cache raw API responses (default: ./datalab_raw)")
327
+ parser.add_argument("--keep-images", action="store_true",
328
+ help="keep base64 image payloads instead of stripping them")
329
+ args = parser.parse_args()
330
+
331
+ files = collect_inputs(args.input)
332
+ if not files:
333
+ parser.error(f"no spreadsheet files found at {args.input}")
334
+
335
+ key = api_key()
336
+ index, preview_lines = {}, ["# Datalab table extraction\n"]
337
+
338
+ for path in files:
339
+ print(f"{path.name}", file=sys.stderr)
340
+ result = convert(path, key, args.mode, args.cache_dir, args.keep_images)
341
+ tables = find_tables(result.get("json") or {})
342
+ index[path.name] = describe(path, tables)
343
+ print(f" {len(tables)} table block(s)", file=sys.stderr)
344
+
345
+ preview_lines.append(f"\n## {path.name}\n")
346
+ for n, table in enumerate(tables):
347
+ grid = to_grid(table["html"])
348
+ preview_lines.append(f"\n### Table {n} — `{table['id']}`\n")
349
+ preview_lines.append(
350
+ f"{max(len(grid) - 1, 0)} data rows x {len(grid[0]) if grid else 0} columns"
351
+ f" — bbox `{table['bbox']}` — range `{excel_range(table['bbox'])}`\n")
352
+ preview_lines.extend(render(grid))
353
+
354
+ if args.preview:
355
+ args.preview.write_text("\n".join(preview_lines) + "\n")
356
+ print(f"preview -> {args.preview}", file=sys.stderr)
357
+ if args.json:
358
+ args.json.write_text(json.dumps(index, indent=2) + "\n")
359
+ print(f"index -> {args.json}", file=sys.stderr)
360
+ if not args.json and not args.preview:
361
+ print(json.dumps(index, indent=2))
362
+
363
+
364
+ if __name__ == "__main__":
365
+ main()
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: find-test-seam
3
+ description: Find the input and output seams in existing code that a characterization test can be pinned to before a business-logic refactor. Use this skill when a business-goal node/edge graph and an API recording already exist and only the seam is wanted — if the graph does not exist yet, refac-wrt-business-goal builds it and names the seams in one pass. It names and ranks seams; it does not write tests and does not change code.
4
+ ---
5
+
6
+ # Find the test seam
7
+
8
+ Given a happy-path API recording and a business-goal node/edge graph, name the input and output seam
9
+ a characterization test can be pinned to. You name seams. You do not write tests or change code.
10
+
11
+ ## Hard constraint
12
+
13
+ 1. Touch only files in backend — `services/`, `utils/`.
14
+ 2. Find a seam which is currently existing in code. Do not create a new one.
15
+
16
+ ## Method
17
+
18
+ - Filter the recording to state-changing 2xx calls; ignore OPTIONS, 304s and polling GETs.
19
+ - A `202` means the work is fire-and-forget: the response body is not the outcome, and the seam
20
+ does not await it. Name the signal the test polls for completion.
21
+ - Input seam: narrowest existing function with every `E` node on its input side. Count what it
22
+ fetches, reads and imports, not just its parameters.
23
+ - Check it is already reachable — exported, on the default export. If you would have to export it,
24
+ you are creating a seam, not finding one.
25
+ - Output seam: where every outcome becomes durable. Read the `catch` block — the refusal lives there.
26
+ - Reject any seam the refactor would dissolve. Pure helpers die; orchestrator entry and terminal
27
+ write survive.
28
+ - Grep every call site of the output seam; find what distinguishes the in-graph ones.
29
+ - Read that function's body — retry or pruning logic silently voids the assertions you plan.
30
+ - Map each node's output to a field on the output-seam payload. An unmappable node means either a
31
+ misplaced seam or a node not implemented — say which, and what can be checked instead.
32
+ - Rank 2–3 seam pairs on coverage versus testability, checking each can be driven today with the
33
+ fixtures and injection points that exist. An existing script that already invokes the pair is the
34
+ strongest evidence it is drivable. Recommend one.
35
+
36
+ ## Output format
37
+
38
+ First answer the exact question the user is asking. No extra context or extra info. Fewer words is
39
+ better. Use progressive disclosure — keep the headings `## Input seam`, `## Output seam`,
40
+ `## Candidates`, `## Blockers`, and be brief under each: a 30–60 word sentence, or 10–15 word
41
+ bullets for technical points. Cite `file:line` for every claim.