Henry Kobutra
← All notes
Notes

Before you add another dependency

Check whether the project already solves the problem, using a read-only inventory and one concrete acceptance test.

Before adding a tool, write down the part of the job your current setup cannot do. "We need background jobs" is too broad if the backend already has a scheduler. "We need to retry this operation after a provider outage without processing the same request twice" gives you something to inspect and test.

The first step is an inventory. The useful result is a decision about one missing capability, not a spreadsheet of every package anyone has installed.

You need Python 3.9 or later and read access to the project's manifests and engineering docs. You don't need to install its dependencies, start its services or open its environment files.

Start with one project

Keep the project boundary intact in your private audit. An application with an integrated backend and one built around a separate SQL database may need different tools. Combining their dependencies into one list can make it look as though you are missing something that the current project never needed.

Read the repository's instructions and its current architecture notes first. Then choose the manifests to inspect. In a monorepo, the root manifest alone will miss tools declared by the applications and shared packages.

Keep four claims separate:

Claim Evidence to look for
Declared A package appears in a manifest.
Installed The package manager reports it in this checkout's installed tree.
Used A documented workflow, configuration or call site gives it a job. A real execution can verify that path.
Recommended Someone who owns the choice can explain when they'd choose it again and when they wouldn't.

A lockfile records dependency resolution. It doesn't prove that an install exists on your machine or that a feature reaches production. A dev dependency might run on every build, while a runtime dependency might be a forgotten experiment.

Make a read-only inventory

Save this as stack_audit.py, or use the companion example. It reads only the files you name. It doesn't crawl the repository or run package scripts.

"""Inventory explicit package.json files without running project code."""
import argparse
import json
from pathlib import Path

SECTIONS = (
    "dependencies", "devDependencies", "optionalDependencies", "peerDependencies"
)


def inventory(path):
    data = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(data, dict):
        raise ValueError("manifest must be an object")
    for key in (*SECTIONS, "scripts"):
        section = data.get(key, {})
        if not isinstance(section, dict) or not all(
            isinstance(value, str) for value in section.values()
        ):
            raise ValueError("sections must map names to strings")
    return {
        "manifest": str(path),
        "declarations": {key: sorted(data.get(key, {})) for key in SECTIONS},
        "scripts": sorted(data.get("scripts", {})),
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("manifests", nargs="+", type=Path)
    args = parser.parse_args()
    report = []
    for path in args.manifests:
        try:
            report.append(inventory(path))
        except (OSError, ValueError):
            parser.error(
                "could not audit " + ascii(str(path))
                + ": expected a readable UTF-8 JSON manifest with string maps"
            )
    print(json.dumps(report, indent=2, ensure_ascii=True))


if __name__ == "__main__":
    main()

From the companion example directory, run:

python3 -B stack_audit.py fixtures/package.json
python3 -B test_stack_audit.py -v

The synthetic fixture produces zod under dependencies, vitest under devDependencies, and test under script names. The test suite also checks optional and peer declarations, multiple manifests, invalid input and a script that must never execute.

For your project, pass the manifest paths you chose as positional arguments. Use relative paths if you intend to share the result. Review the output anyway: private package names and paths can disclose information. The script omits dependency specifications and command bodies, which can contain private URLs or inline credentials.

This is a declaration inventory, not a vulnerability scanner or an unused-dependency detector. It doesn't expand workspace patterns, resolve versions, inspect transitive dependencies or determine whether a package is installed. It also doesn't normalize aliases. Invalid JSON, unreadable files or incorrectly shaped sections stop the batch with a nonzero exit and no partial JSON report.

Follow one capability into the code

Pick the capability you're considering adding. Find the existing tool that is supposed to cover it, then inspect its configuration and callers. Stay within that project; don't search personal files or production data for proof.

For a test runner, follow the package script into its configuration and identify the test files it includes. For a scheduler, find a registration and the handler it invokes. Read enough to distinguish implemented work from a proposed architecture diagram.

When the docs and manifest disagree, record the disagreement. A package can survive a migration. A service can also be in use through HTTP without a matching SDK dependency. Neither file wins automatically.

Only after that inspection should you run the smallest relevant check, using the project's documented isolation setup and synthetic data. Read a command before running it. A script named check may rewrite files; a script named test may need services or credentials.

Give the proposed tool a test

Use this short decision record:

Capability needed:
Current implementation and evidence:
Specific gap:
Smallest change without a new tool:
Candidate tool and what it would replace:
Synthetic acceptance test:
Setup and ongoing maintenance cost:
Owner and removal condition:
Decision:

For example, suppose an application already has a schema validator and needs to validate a new form. This is a hypothetical decision:

Capability needed: reject malformed email input before saving a form.
Current implementation and evidence: shared validation package exists;
  inspect an existing form and its server handler to confirm the pattern.
Specific gap: the new form has no schema yet.
Smallest change without a new tool: add a schema to the shared package.
Candidate tool and what it would replace: a second validator, replacing nothing.
Synthetic acceptance test: valid input succeeds; malformed input fails
  on the server even when the browser validation is bypassed.
Setup and ongoing maintenance cost: another API and error format if adopted.
Owner and removal condition: form owner; remove the unused candidate
  from this proposal unless the existing validator fails the test.
Decision: try the existing validator first.

That decision changes if the existing library can't express the rule, can't run where validation happens, or produces errors the form cannot use. Test that gap before paying for a migration.

Finish with evidence someone else can check

Keep the relevant manifest paths and repository revision with the decision, plus any uncommitted changes that affected the result. Add the command you ran and its actual outcome. If you haven't exercised the proposed integration, say so.

The audit is finished when another developer can find the existing capability, reproduce your check and see why the addition is necessary, or why you left it out. You don't need to justify the rest of the stack to answer that one question.

A conversation starts somewhere

What are you
working on?

If something here connects with what you're working on, email me.

henry@kobutra.com