Skip to content

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.

LocationRole 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.pyCarries 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.

A small linter — plugin/scripts/lint_inline_script_deps.py — that:

  • Walks the repo for files whose first ~30 lines contain a # /// script block.
  • Parses each block’s dependencies = [...] list.
  • Walks the file’s import and from X import Y statements, resolving each to a top-level distribution name via a small hand-maintained module-to-distribution map (e.g. pydanticpydantic, yamlpyyaml, leaselease). Imports that resolve to first-party packages (lease, anything under plugin/) 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.

  1. Survey the existing # /// script blocks in the repo to confirm the parse shape and produce a baseline of expected module → distribution mappings.
  2. Implement lint_inline_script_deps.py with 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.
  3. Add a unit test that plants a deliberately-missing inline dep and asserts the linter catches it.
  4. Wire the linter into the project’s quality-check entrypoint (scripts/run_quality_checks.py or the equivalent surface /sdlc:task-work invokes).
  5. Run the linter against the current tree, fix any drift it surfaces, and lock in the green state.
LocationKindChange
plugin/scripts/lint_inline_script_deps.pynewThe linter itself: parse # /// script blocks, walk imports, diff against declared deps via a module → distribution mapping table.
plugin/scripts/test_lint_inline_script_deps.pynewUnit tests: planted-missing-dep fixture must fail; a clean fixture must pass; mapping-table miss must produce the explicit hint.
scripts/run_quality_checks.pymodifyPath 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 scriptsmodifyFix any drift the linter surfaces on the first run so the green state is the baseline.
  • AC-1: Running plugin/scripts/lint_inline_script_deps.py plugin/ against a tree where one script’s inline dependencies = [...] 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.
  • 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.toml deps). Different surface, different lint.
  • none

Spawned by /sdlc:task-work post-mortem of T-K3RR-add-lease-namespace-conflict-guard on 2026-05-24.

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

← Back to Tasks