T-88JG-lint-inline-script-deps-against-imports
Status: closed/obsoleted · Impact: low · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to open/ready before picking up.
PEP 723 # /// script blocks at the top of uv run --script files
declare their runtime dependencies inline. When a script grows a new
import (directly or transitively, e.g. from lease import …
pulling in pydantic + pyyaml), the inline dependencies = [...]
block has to be bumped by hand. There is currently no lint that
catches the drift; the gap shows up at runtime as an ImportError
on a fresh-cache invocation. The post-mortem of
T-K3RR-add-lease-namespace-conflict-guard hit exactly this
when plugin/skills/setup/setup_planning.py started importing from
lease and the inline deps block didn’t list pydantic/pyyaml.
Automating the check closes the gap.
| Location | Role today |
|---|---|
plugin/scripts/ | Many scripts use # /// script blocks with hand-maintained dependencies = [...] lists. No tooling reconciles those lists against the script’s actual import set. |
plugin/skills/setup/setup_planning.py | Carries an inline dependencies = [...] block; recently bumped by hand to include pydantic + pyyaml after a transitive import via from lease import …. |
plugin/lib/lease/ | Owns the import graph that surfaces this drift on consumers. |
Project quality-checks (just full-check, equivalent runners) | No step today walks # /// script blocks and confirms declared deps cover imports. |
Proposed
Section titled “Proposed”A small linter — plugin/scripts/lint_inline_script_deps.py — that:
- Walks the repo for files whose first ~30 lines contain a
# /// scriptblock. - Parses each block’s
dependencies = [...]list. - Walks the file’s
importandfrom X import Ystatements, resolving each to a top-level distribution name via a small hand-maintained module-to-distribution map (e.g.pydantic→pydantic,yaml→pyyaml,lease→lease). Imports that resolve to first-party packages (lease, anything underplugin/) AND those packages’ transitive runtime dependencies must be declared in the inline block. - Reports any import whose top-level distribution is missing from the inline deps list. Exit non-zero on findings.
- Wires into the project’s existing quality-check runner so the lint runs in CI alongside the other static checks.
A future enhancement (out of scope here) is to teach the linter to read each first-party package’s own declared dependency graph so transitive deps don’t need to be hand-listed in the consuming script’s inline block — but the v1 lint is the dumb explicit check.
Approach
Section titled “Approach”- Survey the existing
# /// scriptblocks in the repo to confirm the parse shape and produce a baseline of expectedmodule → distributionmappings. - Implement
lint_inline_script_deps.pywith a minimal hand-maintained mapping table for the common offenders (pydantic, pyyaml, lease, requests, …). Each unmapped import either passes (stdlib / first-party) or fails with a “add to mapping table” hint. - Add a unit test that plants a deliberately-missing inline dep and asserts the linter catches it.
- Wire the linter into the project’s quality-check entrypoint
(
scripts/run_quality_checks.pyor the equivalent surface/sdlc:task-workinvokes). - Run the linter against the current tree, fix any drift it surfaces, and lock in the green state.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/scripts/lint_inline_script_deps.py | new | The linter itself: parse # /// script blocks, walk imports, diff against declared deps via a module → distribution mapping table. |
plugin/scripts/test_lint_inline_script_deps.py | new | Unit tests: planted-missing-dep fixture must fail; a clean fixture must pass; mapping-table miss must produce the explicit hint. |
scripts/run_quality_checks.py | modify | Path is a best guess; pick whichever runner the project’s just full-check (or equivalent) actually invokes. Add the new lint as a step. |
plugin/scripts/setup_planning.py deps + similar scripts | modify | Fix any drift the linter surfaces on the first run so the green state is the baseline. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Running
plugin/scripts/lint_inline_script_deps.py plugin/against a tree where one script’s inlinedependencies = [...]list is missing a real import exits non-zero with a message naming the script and the missing distribution. - AC-2: Running the same linter against the current tree (after step 5) exits 0.
- AC-3: The linter’s quality-check wiring causes a fresh PR that introduces an undeclared inline dep to fail CI before merge — verified by a follow-up PR that intentionally breaks the contract and is rejected.
Out of scope
Section titled “Out of scope”- Auto-fixing drift (i.e. having the linter rewrite the inline
dependencies = [...]block). Manual fix per-script keeps the blast radius small. - Teaching the linter to read each first-party package’s own dependency graph and infer transitive deps. v1 is the dumb explicit check.
- Generalising to non-PEP-723 dependency declarations (e.g.
pyproject.tomldeps). Different surface, different lint.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-K3RR-add-lease-namespace-conflict-guard on 2026-05-24.
Dedup search (spawn-from-post-mortem)
Section titled “Dedup search (spawn-from-post-mortem)”Bullet: setup_planning.py # /// script block didn’t declare deps for lease. The dispatcher transitively imports pydantic + pyyaml via from lease import …. Bumped the inline dependencies = […] block. First fresh-cache setup invocation is now slightly slower (uv has to resolve pydantic); subsequent runs are unaffected. Keywords searched: setup_planning, transitively, dependencies, fresh-cache, dispatcher, invocation, subsequent, unaffected Excluded: 2026-05-23-add-lease-namespace-conflict-guard Top candidates (score / status / headline):
- 18 / closed/done / 2026-05-19-co-locate-skill-specific-scripts — Co-locate skill-specific scripts under their skill dir; promote when shared
- 16 / closed/done / 2026-05-20-pluralize-recognizes-backlog-as-plural — pluralize() should treat ‘backlog’ as already-plural
- 14 / in-progress / 2026-05-23-add-sdlc-lease-cli-commands — Add
sdlc lease …CLI commands - 10 / closed/done / 2026-05-20-regen-walks-skill-co-located-scripts — regen.mjs walks
plugin/skills/
/*.py for script reference pages - 10 / planning/needs-definition / 2026-05-22-setup-planning-drop-back-compat-helpers — Drop back-compat gitignore helpers in setup_planning.py if no external callers Decision: SPAWNED