skilldrop-cli 0.16.28 → 0.16.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/skilldrop.js +160 -33
- package/catalogue.json +7 -3
- package/model-routing.json +16 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
- package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
- package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
- package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
- package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
- package/packs/data-analytics/loops/measure/LOOP.md +118 -0
- package/packs/data-analytics/loops/measure/evals/eval_queries.json +9 -0
- package/packs/data-analytics/loops/measure/evals/evals.json +28 -0
- package/packs/data-analytics/loops/measure/loop.json +41 -0
- package/packs/dev-team/loops/build/LOOP.md +4 -2
- package/packs/dev-team/loops/build/loop.json +2 -2
- package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
- package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
- package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
- package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
- package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
- package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
- package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
- package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
- package/packs/grc/loops/comply/LOOP.md +133 -0
- package/packs/grc/loops/comply/evals/eval_queries.json +9 -0
- package/packs/grc/loops/comply/evals/evals.json +30 -0
- package/packs/grc/loops/comply/loop.json +54 -0
- package/packs/product-manager/loops/discover/LOOP.md +5 -4
- package/packs/product-manager/loops/discover/loop.json +2 -2
- package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
- package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
- package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
- package/packs/research/loops/research/LOOP.md +123 -0
- package/packs/research/loops/research/evals/eval_queries.json +9 -0
- package/packs/research/loops/research/evals/evals.json +39 -0
- package/packs/research/loops/research/loop.json +35 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Worked example: tallyhall, before adding a refund window
|
|
2
|
+
|
|
3
|
+
A fictional FastAPI ticketing service. The README is two releases stale, one rule has a second copy, and one module is generated.
|
|
4
|
+
|
|
5
|
+
## Input given to the skill
|
|
6
|
+
|
|
7
|
+
> I've just joined and my first ticket is "refunds must be requested within 14 days of the event ending". I've never opened this repo. Get me oriented. The repo is the current directory.
|
|
8
|
+
|
|
9
|
+
The files that mattered, with the line numbers the tour cites:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
README.md
|
|
13
|
+
5 ## Run it
|
|
14
|
+
7 make dev
|
|
15
|
+
12 Settings live in `settings.toml`.
|
|
16
|
+
|
|
17
|
+
Makefile
|
|
18
|
+
1 .PHONY: run test lint
|
|
19
|
+
3 run:
|
|
20
|
+
4 uvicorn tallyhall.main:app --reload
|
|
21
|
+
7 test:
|
|
22
|
+
8 pytest -q -m "not e2e"
|
|
23
|
+
10 lint:
|
|
24
|
+
11 ruff check tallyhall tests
|
|
25
|
+
|
|
26
|
+
.github/workflows/ci.yml
|
|
27
|
+
17 - run: pip install -e ".[dev]"
|
|
28
|
+
19 - run: make lint
|
|
29
|
+
21 - run: make test
|
|
30
|
+
23 env:
|
|
31
|
+
24 TALLYHALL_DB_URL: sqlite:///ci.db
|
|
32
|
+
|
|
33
|
+
pyproject.toml
|
|
34
|
+
14 [tool.pytest.ini_options]
|
|
35
|
+
15 markers = ["e2e: needs the payment sandbox"]
|
|
36
|
+
|
|
37
|
+
tallyhall/main.py
|
|
38
|
+
6 app = FastAPI()
|
|
39
|
+
7 app.include_router(refunds.router)
|
|
40
|
+
|
|
41
|
+
tallyhall/api/refunds.py
|
|
42
|
+
18 @router.post("/orders/{order_id}/refunds", status_code=201)
|
|
43
|
+
19 def create_refund(order_id: int, db=Depends(get_db)):
|
|
44
|
+
20 order = orders.get(db, order_id)
|
|
45
|
+
22 check_refundable(order, now=utcnow())
|
|
46
|
+
24 except RefundRejected as e:
|
|
47
|
+
25 raise HTTPException(422, {"code": e.code, "message": str(e)})
|
|
48
|
+
27 return refunds.create(db, order)
|
|
49
|
+
|
|
50
|
+
tallyhall/rules/refunds.py
|
|
51
|
+
4 class RefundRejected(Exception):
|
|
52
|
+
9 def check_refundable(order, now):
|
|
53
|
+
10 if order.status != "paid":
|
|
54
|
+
11 raise RefundRejected("not_paid", "only paid orders can be refunded")
|
|
55
|
+
12 if order.refunded_at is not None:
|
|
56
|
+
13 raise RefundRejected("already_refunded", "order already refunded")
|
|
57
|
+
|
|
58
|
+
tallyhall/admin/bulk_refund.py
|
|
59
|
+
27 if order.status == "paid" and order.refunded_at is None:
|
|
60
|
+
28 refunds.create(db, order)
|
|
61
|
+
|
|
62
|
+
tallyhall/models_gen.py
|
|
63
|
+
1 # generated by datamodel-codegen:
|
|
64
|
+
2 # filename: schemas/order.json
|
|
65
|
+
|
|
66
|
+
tallyhall/settings.py
|
|
67
|
+
6 DB_URL = os.environ["TALLYHALL_DB_URL"]
|
|
68
|
+
|
|
69
|
+
tests/rules/test_refunds.py (test_rejects_unpaid, test_rejects_already_refunded)
|
|
70
|
+
tests/api/test_refunds_e2e.py (marked e2e)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The fact lister also reported: `make dev` named at `README.md:7` with no `dev` target in the Makefile, and `settings.toml` named at `README.md:12` and not present. No test file imports `tallyhall/admin/bulk_refund.py`. The Order model (`tallyhall/models_gen.py`) has an `event_ends_at` field.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Output
|
|
78
|
+
|
|
79
|
+
**What matters for this change**
|
|
80
|
+
|
|
81
|
+
- The rule goes in `check_refundable` (`tallyhall/rules/refunds.py:9`). Every other refund rule lives there, and the API calls it at `tallyhall/api/refunds.py:22`.
|
|
82
|
+
- There is a second copy of the refund rules in the support bulk-refund tool (`tallyhall/admin/bulk_refund.py:27`). It doesn't call `check_refundable`, so it won't pick up the window unless someone changes it. Whether it should is the first question for a maintainer.
|
|
83
|
+
- Ignore the README's `make dev` and `settings.toml`. Neither exists. Run with `make run` (`Makefile:3`); settings come from env vars (`tallyhall/settings.py:6`).
|
|
84
|
+
- `tallyhall/models_gen.py` is generated from `schemas/order.json`. Don't edit it. The field the window needs, `event_ends_at`, is already there.
|
|
85
|
+
- `make test` skips the e2e tests (`Makefile:8`, marker at `pyproject.toml:15`). The rule tests in `tests/rules/` do run.
|
|
86
|
+
|
|
87
|
+
# Tour: tallyhall, for "refunds within 14 days of the event ending"
|
|
88
|
+
|
|
89
|
+
_Read on 2026-10-20. Scope: `POST /orders/{order_id}/refunds` from the app bootstrap to `refunds.create`. Skipped on purpose: ticket purchase, the payment-provider client, deployment config._
|
|
90
|
+
|
|
91
|
+
[assumption] "Within 14 days of the event ending" means `now - order.event_ends_at <= 14 days`, with the boundary day allowed.
|
|
92
|
+
|
|
93
|
+
## Run it
|
|
94
|
+
|
|
95
|
+
| To | Command | Source | Status |
|
|
96
|
+
|---|---|---|---|
|
|
97
|
+
| Install | `pip install -e ".[dev]"` | `.github/workflows/ci.yml:17` | read, not run |
|
|
98
|
+
| Test (whole suite) | `make test` → `pytest -q -m "not e2e"` | `.github/workflows/ci.yml:21`, `Makefile:8` | ran: passed (14 passed, 3 deselected) |
|
|
99
|
+
| Test (this area) | `pytest -q -m "not e2e" tests/rules/test_refunds.py` | derived from `Makefile:8` | ran: passed (2 passed) |
|
|
100
|
+
| Lint | `make lint` → `ruff check tallyhall tests` | `.github/workflows/ci.yml:19`, `Makefile:11` | ran: passed |
|
|
101
|
+
| Run locally | `make run` | `Makefile:3` | read, not run (needs `TALLYHALL_DB_URL`) |
|
|
102
|
+
|
|
103
|
+
Needs: `TALLYHALL_DB_URL`, read at `tallyhall/settings.py:6` with no default; CI sets `sqlite:///ci.db` at `.github/workflows/ci.yml:24`.
|
|
104
|
+
|
|
105
|
+
## The path this change touches
|
|
106
|
+
|
|
107
|
+
1. `tallyhall/main.py:7`: the refunds router is mounted on the app.
|
|
108
|
+
2. `tallyhall/api/refunds.py:19` `create_refund`: loads the order (line 20).
|
|
109
|
+
3. `tallyhall/api/refunds.py:22`: calls `check_refundable(order, now=utcnow())`.
|
|
110
|
+
4. `tallyhall/rules/refunds.py:9` `check_refundable`: raises `RefundRejected` for an unpaid order (line 10) or one already refunded (line 12).
|
|
111
|
+
5. `tallyhall/api/refunds.py:25`: `RefundRejected` becomes a 422 with `{code, message}`.
|
|
112
|
+
6. `tallyhall/api/refunds.py:27`: `refunds.create` writes the refund. This is the far edge.
|
|
113
|
+
|
|
114
|
+
## Where the change goes
|
|
115
|
+
|
|
116
|
+
- `tallyhall/rules/refunds.py` `check_refundable`: add the window check after line 13, raising `RefundRejected("refund_window_closed", …)`. This follows the two rules already there.
|
|
117
|
+
- Must change with it: nothing generated. `event_ends_at` already exists in `tallyhall/models_gen.py`. Whether `tallyhall/admin/bulk_refund.py:27` changes too is open (question 1).
|
|
118
|
+
|
|
119
|
+
## Conventions the code follows
|
|
120
|
+
|
|
121
|
+
- **Errors:** a rule raises `RefundRejected(code, message)` with a snake_case code (`tallyhall/rules/refunds.py:11`); the route maps it to 422 (`tallyhall/api/refunds.py:25`).
|
|
122
|
+
- **Time:** the route passes `now` in, so rules are tested without a clock (`tallyhall/api/refunds.py:22`).
|
|
123
|
+
- **Tests:** one `test_rejects_<reason>` per rule, under `tests/rules/` (`tests/rules/test_refunds.py`).
|
|
124
|
+
- **Config:** env vars read in `tallyhall/settings.py`; a missing var fails at import (`tallyhall/settings.py:6`).
|
|
125
|
+
|
|
126
|
+
## Traps
|
|
127
|
+
|
|
128
|
+
- **Generated:** `tallyhall/models_gen.py` from `schemas/order.json` (`tallyhall/models_gen.py:1-2`). No regenerate command is defined in the Makefile, pyproject or CI; see question 2. CI drift check: none found.
|
|
129
|
+
- **Second copy:** `tallyhall/admin/bulk_refund.py:27` repeats the paid and not-refunded checks inline.
|
|
130
|
+
- **Untested on the path:** none on the API path. Off the path, `tallyhall/admin/bulk_refund.py` has no test.
|
|
131
|
+
- **Tests the default command skips:** `e2e` (`Makefile:8`, `pyproject.toml:15`): `tests/api/test_refunds_e2e.py`, which needs the payment sandbox.
|
|
132
|
+
- **Env vars and flags on the path:** `TALLYHALL_DB_URL` only. No feature flags found.
|
|
133
|
+
- **Vendored:** none.
|
|
134
|
+
|
|
135
|
+
## Where the README is wrong
|
|
136
|
+
|
|
137
|
+
- README says `make dev` (`README.md:7`); the Makefile has `run`, `test` and `lint` only (`Makefile:1`). Use `make run`.
|
|
138
|
+
- README says settings live in `settings.toml` (`README.md:12`); there is no such file, and settings are env vars (`tallyhall/settings.py:6`).
|
|
139
|
+
|
|
140
|
+
## Tests for this area
|
|
141
|
+
|
|
142
|
+
- Covered by: `tests/rules/test_refunds.py` (unpaid and already-refunded rejections).
|
|
143
|
+
- Run just these: `pytest -q -m "not e2e" tests/rules/test_refunds.py` (derived from `Makefile:8`).
|
|
144
|
+
- Not covered: the 422 mapping is exercised only by the e2e test, which `make test` skips.
|
|
145
|
+
|
|
146
|
+
## First step
|
|
147
|
+
|
|
148
|
+
Add `test_rejects_refund_after_window` to `tests/rules/test_refunds.py`, with an order whose `event_ends_at` is 15 days before `now`. Run `pytest -q -m "not e2e" tests/rules/test_refunds.py`. It fails until `check_refundable` checks the window. Add a 14-day case that passes, to pin the boundary.
|
|
149
|
+
|
|
150
|
+
## Open questions for a maintainer
|
|
151
|
+
|
|
152
|
+
1. Should the bulk-refund tool (`tallyhall/admin/bulk_refund.py:27`) apply the window, or is it the support override that skips it?
|
|
153
|
+
2. How is `tallyhall/models_gen.py` regenerated? Nothing in the repo defines the command.
|
|
154
|
+
3. Is the window measured from `event_ends_at` in UTC? `utcnow()` is used at `tallyhall/api/refunds.py:22`, but the schema doesn't say what timezone the event times are in.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
Why this passes the bar: every command has a source and a status, both README claims are contradicted with citations, the second copy is named and left as a question rather than silently changed or ignored, the generated file is called out before anyone edits it, and the first step names a file, a command and the failure that proves it.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codebase-tour",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Get a newcomer (a new engineer, or the agent itself) oriented in an unfamiliar repository before they make one specific change, with a short tour scoped to that change. It gives the install, run and test commands taken from CI, package scripts and the Makefile; the path the change touches, traced through the code with file:line citations; where the change goes; the conventions the code actually follows; the traps (generated files, a second copy of the logic, untested code, env vars, flags); and a first step that can be checked. Use when someone says \"I'm new to this repo\", \"get me oriented\", \"where would I make this change\", \"walk me through the code for X\", \"onboard me before I start this ticket\", or \"how do I run and test this\".",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": []
|
|
9
|
+
},
|
|
10
|
+
"env": {
|
|
11
|
+
"required": [],
|
|
12
|
+
"optional": []
|
|
13
|
+
},
|
|
14
|
+
"related": [
|
|
15
|
+
"agents-md-generator",
|
|
16
|
+
"architecture-diagrams",
|
|
17
|
+
"feature-implement-loop",
|
|
18
|
+
"interactive-diagram",
|
|
19
|
+
"pre-merge-review",
|
|
20
|
+
"reverse-architecture"
|
|
21
|
+
],
|
|
22
|
+
"handoff": [
|
|
23
|
+
{
|
|
24
|
+
"to": "feature-implement-loop",
|
|
25
|
+
"when": "the tour names where the change goes and a first step",
|
|
26
|
+
"purpose": "Implement the change against acceptance criteria, starting from the tour's first step and running the area's test command it found.",
|
|
27
|
+
"fallback": "If feature-implement-loop is absent, take the tour's first step: write the failing test it names, run the area's test command before and after the change, and update every place listed under 'Must change with it'."
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"tags": [
|
|
31
|
+
"onboarding",
|
|
32
|
+
"codebase",
|
|
33
|
+
"repo-orientation",
|
|
34
|
+
"provenance",
|
|
35
|
+
"developer-experience"
|
|
36
|
+
],
|
|
37
|
+
"permissions": {
|
|
38
|
+
"network": [],
|
|
39
|
+
"commands": [],
|
|
40
|
+
"files": "none",
|
|
41
|
+
"notes": "scripts/repo_facts.py reads files under the repository root it is given and prints to stdout. It writes nothing and runs no commands."
|
|
42
|
+
},
|
|
43
|
+
"model": {
|
|
44
|
+
"tier": "standard",
|
|
45
|
+
"rationale": "Reading a repo and citing what it finds is generation over evidence; every claim is checked against a file, so a mid tier is enough."
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""List the facts a codebase tour cites, each with the file and line it came from.
|
|
3
|
+
|
|
4
|
+
Reads a repository and prints, to stdout:
|
|
5
|
+
- build and tool manifests found (package.json, pyproject.toml, Makefile, ...)
|
|
6
|
+
- commands the repo itself defines: package.json scripts, Makefile and justfile
|
|
7
|
+
targets, CI `run:` / `script:` steps, tox and nox sessions, pyproject entry points
|
|
8
|
+
- commands the README or CONTRIBUTING names that no manifest or CI file defines, and
|
|
9
|
+
paths they name that do not exist
|
|
10
|
+
- entry points (main functions, `__main__` guards, Dockerfile CMD/ENTRYPOINT, bin)
|
|
11
|
+
- test files grouped by directory, and test-runner config files
|
|
12
|
+
- files that say they are generated, vendored directories, and lockfiles
|
|
13
|
+
- environment variables the code reads, with the line that reads each
|
|
14
|
+
- the directories holding the most files
|
|
15
|
+
|
|
16
|
+
It is read-only: it opens files under the root it is given and writes nothing.
|
|
17
|
+
It cannot tell which command works; it tells you where each one is written down.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 repo_facts.py <repo-root>
|
|
21
|
+
python3 repo_facts.py <repo-root> --json
|
|
22
|
+
python3 repo_facts.py <repo-root> --max 25 # cap items per section (default 40)
|
|
23
|
+
|
|
24
|
+
Exit codes: 0 facts printed, 2 bad input.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
from collections import Counter, defaultdict
|
|
33
|
+
|
|
34
|
+
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", ".tox",
|
|
35
|
+
".nox", "target", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".next",
|
|
36
|
+
"coverage", ".gradle", ".idea", ".vscode", ".terraform"}
|
|
37
|
+
MANIFESTS = {"package.json", "pyproject.toml", "setup.cfg", "setup.py", "tox.ini", "noxfile.py",
|
|
38
|
+
"Makefile", "makefile", "GNUmakefile", "justfile", "Taskfile.yml", "go.mod",
|
|
39
|
+
"Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "Gemfile",
|
|
40
|
+
"composer.json", "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
41
|
+
"compose.yml", "compose.yaml", "requirements.txt", "requirements-dev.txt",
|
|
42
|
+
".tool-versions", ".nvmrc", ".python-version", "CONTRIBUTING.md", "Procfile"}
|
|
43
|
+
LOCKFILES = {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "uv.lock",
|
|
44
|
+
"Pipfile.lock", "Cargo.lock", "go.sum", "Gemfile.lock", "composer.lock"}
|
|
45
|
+
VENDOR_DIRS = {"vendor", "third_party", "third-party", "vendored", "external"}
|
|
46
|
+
SOURCE_EXT = {".py", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".go", ".rb", ".java",
|
|
47
|
+
".kt", ".rs", ".php", ".cs", ".sh", ".scala", ".swift"}
|
|
48
|
+
TEST_FILE = re.compile(r"(^test_.*\.py$|_test\.py$|_test\.go$|\.(test|spec)\.[cm]?[jt]sx?$|"
|
|
49
|
+
r"Tests?\.(java|kt|cs)$|_spec\.rb$|_test\.rb$)")
|
|
50
|
+
TEST_CONFIG = re.compile(r"^(pytest\.ini|conftest\.py|jest\.config\.\w+|vitest\.config\.\w+|"
|
|
51
|
+
r"karma\.conf\.\w+|\.mocharc\.\w+|phpunit\.xml(\.dist)?)$")
|
|
52
|
+
GENERATED = re.compile(r"(?i)(do not edit|@generated|code generated|auto-?generated|"
|
|
53
|
+
r"this file is generated|generated by|don't change)")
|
|
54
|
+
# Environment reads. `[.]` keeps each pattern literal without a backslash-dot sequence.
|
|
55
|
+
ENV_READS = [
|
|
56
|
+
re.compile(r"os[.]environ(?:[.]get)?\s*[\[(]\s*[\"']([A-Za-z_][A-Za-z0-9_]*)"),
|
|
57
|
+
re.compile(r"os[.]getenv\s*\(\s*[\"']([A-Za-z_][A-Za-z0-9_]*)"),
|
|
58
|
+
re.compile(r"process[.]env[.]([A-Za-z_][A-Za-z0-9_]*)"),
|
|
59
|
+
re.compile(r"process[.]env\[\s*[\"'`]([A-Za-z_][A-Za-z0-9_]*)"),
|
|
60
|
+
re.compile(r"os[.](?:Getenv|LookupEnv)\(\s*\"([A-Za-z_][A-Za-z0-9_]*)\""),
|
|
61
|
+
re.compile(r"ENV(?:[.]fetch\(|\[)\s*[\"']([A-Za-z_][A-Za-z0-9_]*)"),
|
|
62
|
+
re.compile(r"System[.]getenv\(\s*\"([A-Za-z_][A-Za-z0-9_]*)\""),
|
|
63
|
+
re.compile(r"env::var\(\s*\"([A-Za-z_][A-Za-z0-9_]*)\""),
|
|
64
|
+
]
|
|
65
|
+
RUNNERS = {"npm", "yarn", "pnpm", "make", "just", "nox", "tox"}
|
|
66
|
+
MAX_BYTES = 512 * 1024
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def walk(root):
|
|
70
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
71
|
+
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
|
|
72
|
+
for name in sorted(filenames):
|
|
73
|
+
full = os.path.join(dirpath, name)
|
|
74
|
+
yield os.path.relpath(full, root).replace(os.sep, "/"), full
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def read_lines(full):
|
|
78
|
+
try:
|
|
79
|
+
if os.path.getsize(full) > MAX_BYTES:
|
|
80
|
+
return []
|
|
81
|
+
with open(full, encoding="utf-8", errors="replace") as fh:
|
|
82
|
+
return fh.read().splitlines()
|
|
83
|
+
except OSError:
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def line_of(lines, pattern, start=0):
|
|
88
|
+
for i in range(start, len(lines)):
|
|
89
|
+
if re.search(pattern, lines[i]):
|
|
90
|
+
return i + 1
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def package_scripts(rel, lines, out, targets):
|
|
95
|
+
try:
|
|
96
|
+
doc = json.loads("\n".join(lines))
|
|
97
|
+
except ValueError:
|
|
98
|
+
return
|
|
99
|
+
for name, cmd in (doc.get("scripts") or {}).items():
|
|
100
|
+
n = line_of(lines, r'^\s*"%s"\s*:' % re.escape(name))
|
|
101
|
+
out.append({"command": "npm run %s" % name, "runs": cmd, "where": "%s:%d" % (rel, n)})
|
|
102
|
+
targets["npm"].add(name)
|
|
103
|
+
for key in ("main", "bin"):
|
|
104
|
+
if key in doc:
|
|
105
|
+
yield key, doc[key], "%s:%d" % (rel, line_of(lines, r'^\s*"%s"\s*:' % key))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def make_targets(rel, lines, out, targets, runner):
|
|
109
|
+
for i, line in enumerate(lines):
|
|
110
|
+
m = re.match(r"^([A-Za-z0-9][\w.-]*)\s*(?:[^:=\n]*)?:(?!=)", line)
|
|
111
|
+
if not m or m.group(1).startswith(".") or "=" in line.split(":")[0]:
|
|
112
|
+
continue
|
|
113
|
+
recipe = ""
|
|
114
|
+
for nxt in lines[i + 1:i + 4]:
|
|
115
|
+
if nxt.startswith(("\t", " ")) and nxt.strip():
|
|
116
|
+
recipe = nxt.strip()
|
|
117
|
+
break
|
|
118
|
+
out.append({"command": "%s %s" % (runner, m.group(1)), "runs": recipe,
|
|
119
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
120
|
+
targets[runner].add(m.group(1))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def ci_steps(rel, lines, out):
|
|
124
|
+
i = 0
|
|
125
|
+
while i < len(lines):
|
|
126
|
+
m = re.match(r"^(\s*)(?:-\s*)?run:\s*(.*)$", lines[i])
|
|
127
|
+
if m:
|
|
128
|
+
indent, value = len(m.group(1)), m.group(2).strip()
|
|
129
|
+
if value in ("|", ">", "|-", ">-", ""):
|
|
130
|
+
j = i + 1
|
|
131
|
+
while j < len(lines) and (not lines[j].strip() or
|
|
132
|
+
len(lines[j]) - len(lines[j].lstrip()) > indent):
|
|
133
|
+
if lines[j].strip() and not lines[j].strip().startswith("#"):
|
|
134
|
+
out.append({"command": lines[j].strip(), "where": "%s:%d" % (rel, j + 1)})
|
|
135
|
+
j += 1
|
|
136
|
+
i = j
|
|
137
|
+
continue
|
|
138
|
+
out.append({"command": value, "where": "%s:%d" % (rel, i + 1)})
|
|
139
|
+
i += 1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def gitlab_steps(rel, lines, out):
|
|
143
|
+
inside = None
|
|
144
|
+
for i, line in enumerate(lines):
|
|
145
|
+
m = re.match(r"^(\s*)(before_script|script|after_script):\s*$", line)
|
|
146
|
+
if m:
|
|
147
|
+
inside = len(m.group(1))
|
|
148
|
+
continue
|
|
149
|
+
if inside is not None:
|
|
150
|
+
item = re.match(r"^(\s*)-\s+(.*)$", line)
|
|
151
|
+
if item and len(item.group(1)) >= inside:
|
|
152
|
+
out.append({"command": item.group(2).strip().strip("'\""), "where": "%s:%d" % (rel, i + 1)})
|
|
153
|
+
elif line.strip():
|
|
154
|
+
inside = None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def tox_nox(rel, name, lines, out, targets):
|
|
158
|
+
if name == "tox.ini":
|
|
159
|
+
env = None
|
|
160
|
+
for i, line in enumerate(lines):
|
|
161
|
+
sec = re.match(r"^\[(testenv(?::(\S+))?)\]", line)
|
|
162
|
+
if sec:
|
|
163
|
+
env = sec.group(2) or "(default)"
|
|
164
|
+
if sec.group(2):
|
|
165
|
+
targets["tox"].add(sec.group(2))
|
|
166
|
+
elif env and re.match(r"^\s*commands\s*=", line):
|
|
167
|
+
cmd = line.split("=", 1)[1].strip()
|
|
168
|
+
out.append({"command": "tox -e %s" % env, "runs": cmd, "where": "%s:%d" % (rel, i + 1)})
|
|
169
|
+
else:
|
|
170
|
+
for i, line in enumerate(lines):
|
|
171
|
+
if re.match(r"^\s*@nox\.session", line):
|
|
172
|
+
fn = next((re.match(r"^\s*def\s+(\w+)", lines[j]) for j in range(i + 1, min(i + 4, len(lines)))
|
|
173
|
+
if re.match(r"^\s*def\s+\w+", lines[j])), None)
|
|
174
|
+
if fn:
|
|
175
|
+
targets["nox"].add(fn.group(1))
|
|
176
|
+
body = []
|
|
177
|
+
for j in range(i + 1, len(lines)):
|
|
178
|
+
if j > i + 1 and re.match(r"^(@|def |class )", lines[j]):
|
|
179
|
+
break
|
|
180
|
+
body.append(lines[j])
|
|
181
|
+
runs = [b.strip() for b in body if "session.run(" in b]
|
|
182
|
+
out.append({"command": "nox -s %s" % fn.group(1), "runs": "; ".join(runs),
|
|
183
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def pyproject(rel, lines, out, entries):
|
|
187
|
+
table = ""
|
|
188
|
+
for i, line in enumerate(lines):
|
|
189
|
+
sec = re.match(r"^\[([^\]]+)\]", line)
|
|
190
|
+
if sec:
|
|
191
|
+
table = sec.group(1).strip()
|
|
192
|
+
continue
|
|
193
|
+
kv = re.match(r"^\s*([\w.-]+)\s*=\s*(.+)$", line)
|
|
194
|
+
if not kv:
|
|
195
|
+
continue
|
|
196
|
+
if table in ("project.scripts", "tool.poetry.scripts"):
|
|
197
|
+
entries.append({"what": "console script `%s`" % kv.group(1), "target": kv.group(2).strip('"\''),
|
|
198
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
199
|
+
elif table.startswith("tool.hatch.envs.") and table.endswith(".scripts"):
|
|
200
|
+
out.append({"command": "hatch run %s" % kv.group(1), "runs": kv.group(2),
|
|
201
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
202
|
+
elif table == "tool.pytest.ini_options" and kv.group(1) in ("addopts", "testpaths", "markers"):
|
|
203
|
+
out.append({"command": "(pytest config) %s" % kv.group(1), "runs": kv.group(2),
|
|
204
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def doc_commands(root, rel, lines, targets, missing):
|
|
208
|
+
fence = False
|
|
209
|
+
for i, line in enumerate(lines):
|
|
210
|
+
if line.strip().startswith("```"):
|
|
211
|
+
fence = not fence
|
|
212
|
+
continue
|
|
213
|
+
indented = line.startswith((" ", "\t")) and not line.lstrip().startswith(("-", "*"))
|
|
214
|
+
if fence or indented:
|
|
215
|
+
candidates = [line.strip().lstrip("$ ").strip()]
|
|
216
|
+
else:
|
|
217
|
+
candidates = re.findall(r"`([^`]+)`", line)
|
|
218
|
+
for cand in candidates:
|
|
219
|
+
words = cand.split()
|
|
220
|
+
if len(words) > 1 and words[0] in ("python", "python3") and words[1].endswith(".py"):
|
|
221
|
+
if not os.path.exists(os.path.join(root, words[1])):
|
|
222
|
+
missing.append({"command": cand, "where": "%s:%d" % (rel, i + 1),
|
|
223
|
+
"why": "`%s` does not exist" % words[1]})
|
|
224
|
+
continue
|
|
225
|
+
if len(words) < 2 and not (words and words[0] in ("make", "just")):
|
|
226
|
+
continue
|
|
227
|
+
if not words or words[0] not in RUNNERS:
|
|
228
|
+
continue
|
|
229
|
+
runner, target = words[0], None
|
|
230
|
+
if runner in ("npm", "pnpm") and len(words) > 1:
|
|
231
|
+
target = words[2] if words[1] == "run" and len(words) > 2 else (
|
|
232
|
+
words[1] if words[1] in ("test", "start") else None)
|
|
233
|
+
runner = "npm"
|
|
234
|
+
elif runner == "yarn" and len(words) > 1 and words[1] not in ("install", "add"):
|
|
235
|
+
target = words[2] if words[1] == "run" and len(words) > 2 else words[1]
|
|
236
|
+
runner = "npm"
|
|
237
|
+
elif runner in ("make", "just"):
|
|
238
|
+
target = words[1] if len(words) > 1 else None
|
|
239
|
+
elif runner == "nox" and "-s" in words:
|
|
240
|
+
target = words[words.index("-s") + 1] if words.index("-s") + 1 < len(words) else None
|
|
241
|
+
elif runner == "tox" and "-e" in words:
|
|
242
|
+
target = words[words.index("-e") + 1] if words.index("-e") + 1 < len(words) else None
|
|
243
|
+
if runner in ("make", "just") and target is None:
|
|
244
|
+
if not targets[runner]:
|
|
245
|
+
missing.append({"command": cand, "where": "%s:%d" % (rel, i + 1),
|
|
246
|
+
"why": "no %s file defines any target" % runner})
|
|
247
|
+
continue
|
|
248
|
+
if target and target not in targets[runner]:
|
|
249
|
+
source = {"npm": "package.json scripts", "make": "a Makefile", "just": "a justfile",
|
|
250
|
+
"nox": "noxfile.py", "tox": "tox.ini"}[runner]
|
|
251
|
+
missing.append({"command": cand, "where": "%s:%d" % (rel, i + 1),
|
|
252
|
+
"why": "`%s` is not defined in %s" % (target, source)})
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def doc_paths(root, rel, lines, missing):
|
|
256
|
+
for i, line in enumerate(lines):
|
|
257
|
+
for cand in re.findall(r"`([^`\s]+)`", line):
|
|
258
|
+
looks_like_path = "/" in cand or re.search(r"\.(json|ya?ml|toml|ini|cfg|py|[jt]sx?|go|rb|sql|md)$", cand)
|
|
259
|
+
if not looks_like_path or cand.startswith(("http", "-", "@", "$")) or "*" in cand or "<" in cand:
|
|
260
|
+
continue
|
|
261
|
+
if not os.path.exists(os.path.join(root, cand.strip("/"))):
|
|
262
|
+
missing.append({"path": cand, "where": "%s:%d" % (rel, i + 1)})
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def collect(root):
|
|
266
|
+
facts = {"manifests": [], "commands": [], "doc_commands_not_found": [], "doc_paths_not_found": [],
|
|
267
|
+
"entry_points": [],
|
|
268
|
+
"tests": {}, "test_config": [], "generated": [], "vendored": [], "lockfiles": [],
|
|
269
|
+
"env_vars": [], "largest_dirs": []}
|
|
270
|
+
targets = defaultdict(set)
|
|
271
|
+
docs, dir_count, tests = [], Counter(), defaultdict(int)
|
|
272
|
+
for rel, full in walk(root):
|
|
273
|
+
name = os.path.basename(rel)
|
|
274
|
+
parts = rel.split("/")
|
|
275
|
+
dir_count["/".join(parts[:2]) if len(parts) > 2 else (parts[0] if len(parts) > 1 else ".")] += 1
|
|
276
|
+
vend = next((p for p in parts[:-1] if p in VENDOR_DIRS), None)
|
|
277
|
+
if vend:
|
|
278
|
+
vdir = "/".join(parts[:parts.index(vend) + 1])
|
|
279
|
+
if vdir not in facts["vendored"]:
|
|
280
|
+
facts["vendored"].append(vdir)
|
|
281
|
+
continue
|
|
282
|
+
if name in LOCKFILES:
|
|
283
|
+
facts["lockfiles"].append(rel)
|
|
284
|
+
continue
|
|
285
|
+
ext = os.path.splitext(name)[1].lower()
|
|
286
|
+
is_ci = rel.startswith(".github/workflows/") and ext in (".yml", ".yaml")
|
|
287
|
+
is_gitlab = name == ".gitlab-ci.yml"
|
|
288
|
+
if name in MANIFESTS or is_ci or is_gitlab or name.lower().startswith("readme"):
|
|
289
|
+
facts["manifests"].append(rel)
|
|
290
|
+
if TEST_FILE.search(name):
|
|
291
|
+
tests["/".join(parts[:-1]) or "."] += 1
|
|
292
|
+
if TEST_CONFIG.match(name):
|
|
293
|
+
facts["test_config"].append(rel)
|
|
294
|
+
wanted = (name in MANIFESTS or is_ci or is_gitlab or ext in SOURCE_EXT or name == ".gitattributes"
|
|
295
|
+
or name.lower().startswith("readme") or ext in (".md", ".yml", ".yaml", ".json", ".sql",
|
|
296
|
+
".toml", ".cfg", ".ini", ".proto"))
|
|
297
|
+
if not wanted:
|
|
298
|
+
continue
|
|
299
|
+
lines = read_lines(full)
|
|
300
|
+
if not lines:
|
|
301
|
+
continue
|
|
302
|
+
if name == "package.json":
|
|
303
|
+
for key, val, where in package_scripts(rel, lines, facts["commands"], targets):
|
|
304
|
+
facts["entry_points"].append({"what": "package.json %s" % key, "target": val, "where": where})
|
|
305
|
+
elif name in ("Makefile", "makefile", "GNUmakefile"):
|
|
306
|
+
make_targets(rel, lines, facts["commands"], targets, "make")
|
|
307
|
+
elif name == "justfile":
|
|
308
|
+
make_targets(rel, lines, facts["commands"], targets, "just")
|
|
309
|
+
elif is_ci:
|
|
310
|
+
ci_steps(rel, lines, facts["commands"])
|
|
311
|
+
elif is_gitlab:
|
|
312
|
+
gitlab_steps(rel, lines, facts["commands"])
|
|
313
|
+
elif name in ("tox.ini", "noxfile.py"):
|
|
314
|
+
tox_nox(rel, name, lines, facts["commands"], targets)
|
|
315
|
+
elif name == "pyproject.toml":
|
|
316
|
+
pyproject(rel, lines, facts["commands"], facts["entry_points"])
|
|
317
|
+
elif name == "Dockerfile":
|
|
318
|
+
for i, line in enumerate(lines):
|
|
319
|
+
if re.match(r"^\s*(CMD|ENTRYPOINT)\b", line):
|
|
320
|
+
facts["entry_points"].append({"what": "Dockerfile", "target": line.strip(),
|
|
321
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
322
|
+
elif name == ".gitattributes":
|
|
323
|
+
for i, line in enumerate(lines):
|
|
324
|
+
if "linguist-generated" in line or "linguist-vendored" in line:
|
|
325
|
+
facts["generated"].append({"path": line.split()[0], "marker": line.strip(),
|
|
326
|
+
"where": "%s:%d" % (rel, i + 1)})
|
|
327
|
+
if name.lower().startswith(("readme", "contributing")) and ext in (".md", ".rst", ""):
|
|
328
|
+
docs.append((rel, lines))
|
|
329
|
+
for i, line in enumerate(lines[:10]):
|
|
330
|
+
if ext in SOURCE_EXT | {".json", ".yml", ".yaml", ".sql", ".toml"} and GENERATED.search(line):
|
|
331
|
+
facts["generated"].append({"path": rel, "marker": line.strip()[:100], "where": "%s:%d" % (rel, i + 1)})
|
|
332
|
+
break
|
|
333
|
+
if ext in SOURCE_EXT:
|
|
334
|
+
for i, line in enumerate(lines):
|
|
335
|
+
if (re.match(r"^\s*func main\(\)", line) or re.match(r"^if __name__ == [\"']__main__[\"']", line)):
|
|
336
|
+
facts["entry_points"].append({"what": "main", "target": rel, "where": "%s:%d" % (rel, i + 1)})
|
|
337
|
+
for pat in ENV_READS:
|
|
338
|
+
for var in pat.findall(line):
|
|
339
|
+
facts["env_vars"].append({"name": var, "where": "%s:%d" % (rel, i + 1)})
|
|
340
|
+
for rel, lines in docs:
|
|
341
|
+
doc_commands(root, rel, lines, targets, facts["doc_commands_not_found"])
|
|
342
|
+
doc_paths(root, rel, lines, facts["doc_paths_not_found"])
|
|
343
|
+
facts["tests"] = dict(sorted(tests.items()))
|
|
344
|
+
facts["largest_dirs"] = [{"dir": d, "files": n} for d, n in dir_count.most_common(10)]
|
|
345
|
+
return facts
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def render(facts, cap):
|
|
349
|
+
out = []
|
|
350
|
+
|
|
351
|
+
def section(title, items, fmt, empty):
|
|
352
|
+
out.append("## %s" % title)
|
|
353
|
+
if not items:
|
|
354
|
+
out.append("- %s" % empty)
|
|
355
|
+
for item in items[:cap]:
|
|
356
|
+
out.append("- " + fmt(item))
|
|
357
|
+
if len(items) > cap:
|
|
358
|
+
out.append("- ... %d more (raise --max to see them)" % (len(items) - cap))
|
|
359
|
+
out.append("")
|
|
360
|
+
|
|
361
|
+
out.append("# Repository facts\n")
|
|
362
|
+
out.append("Where each fact is written down. Nothing here was run.\n")
|
|
363
|
+
section("Manifests, CI and docs", facts["manifests"], lambda m: "`%s`" % m, "none found")
|
|
364
|
+
section("Commands the repo defines", facts["commands"],
|
|
365
|
+
lambda c: "`%s`%s — %s" % (c["command"], (" → `%s`" % c["runs"]) if c.get("runs") else "", c["where"]),
|
|
366
|
+
"none found: no package scripts, Makefile, CI steps, tox or nox sessions")
|
|
367
|
+
section("Commands the docs name that nothing defines", facts["doc_commands_not_found"],
|
|
368
|
+
lambda c: "`%s` — %s — %s" % (c["command"], c["where"], c["why"]), "none")
|
|
369
|
+
section("Paths the docs name that do not exist", facts["doc_paths_not_found"],
|
|
370
|
+
lambda p: "`%s` — %s" % (p["path"], p["where"]), "none")
|
|
371
|
+
section("Entry points", facts["entry_points"],
|
|
372
|
+
lambda e: "%s: `%s` — %s" % (e["what"], e["target"], e["where"]), "none found")
|
|
373
|
+
tests = [{"dir": d, "n": n} for d, n in facts["tests"].items()]
|
|
374
|
+
section("Test files by directory", tests, lambda t: "`%s/` — %d file(s)" % (t["dir"], t["n"]),
|
|
375
|
+
"no test files found")
|
|
376
|
+
section("Test runner config", facts["test_config"], lambda t: "`%s`" % t, "none found")
|
|
377
|
+
section("Generated files", facts["generated"],
|
|
378
|
+
lambda g: "`%s` — %s — \"%s\"" % (g["path"], g["where"], g["marker"]), "no generated-file markers found")
|
|
379
|
+
section("Vendored directories", facts["vendored"], lambda v: "`%s/`" % v, "none")
|
|
380
|
+
section("Lockfiles", facts["lockfiles"], lambda f: "`%s`" % f, "none")
|
|
381
|
+
section("Environment variables the code reads", facts["env_vars"],
|
|
382
|
+
lambda e: "`%s` — %s" % (e["name"], e["where"]), "none found")
|
|
383
|
+
section("Largest directories (file count)", facts["largest_dirs"],
|
|
384
|
+
lambda d: "`%s` — %d" % (d["dir"], d["files"]), "empty")
|
|
385
|
+
return "\n".join(out)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def main(argv=None):
|
|
389
|
+
ap = argparse.ArgumentParser(description="List a repo's commands, tests, generated files and env reads with file:line.")
|
|
390
|
+
ap.add_argument("root", help="repository root to read")
|
|
391
|
+
ap.add_argument("--json", action="store_true", help="print JSON instead of Markdown")
|
|
392
|
+
ap.add_argument("--max", type=int, default=40, help="items per section in Markdown (default 40)")
|
|
393
|
+
args = ap.parse_args(argv)
|
|
394
|
+
if not os.path.isdir(args.root):
|
|
395
|
+
print("error: %s is not a directory" % args.root, file=sys.stderr)
|
|
396
|
+
return 2
|
|
397
|
+
facts = collect(args.root)
|
|
398
|
+
print(json.dumps(facts, indent=2) if args.json else render(facts, max(1, args.max)))
|
|
399
|
+
return 0
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
if __name__ == "__main__":
|
|
403
|
+
sys.exit(main())
|