spritegen-cli 0.2.0__tar.gz → 0.4.0__tar.gz
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.
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.gitignore +9 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/PKG-INFO +1 -1
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0003-append-only-jsonl-ledger.md +2 -1
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0006-centralise-configuration-and-never-cache-it.md +2 -1
- spritegen_cli-0.4.0/docs/adr/0015-record-the-call-before-the-files-it-writes.md +53 -0
- spritegen_cli-0.4.0/docs/adr/0016-a-workspace-is-marked-by-the-file-that-configures-it.md +63 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/codewiki/spending-money.md +5 -5
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/codewiki/the-stage-registry.md +10 -5
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/codewiki/the-workspace.md +9 -9
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/glossary.md +3 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/notes.md +4 -0
- spritegen_cli-0.4.0/plans/code-health.md +119 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/pyproject.toml +1 -1
- spritegen_cli-0.4.0/specs/workspace-config/design.md +98 -0
- spritegen_cli-0.4.0/specs/workspace-config/requirements.md +50 -0
- spritegen_cli-0.4.0/specs/workspace-config/tasks.md +25 -0
- spritegen_cli-0.4.0/src/spritegen/__init__.py +27 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/atlas.py +29 -11
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/cli.py +31 -6
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/clip.py +84 -32
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/drive.py +14 -5
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/fal.py +45 -8
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/imaging.py +24 -3
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/ledger.py +43 -4
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/matting.py +16 -2
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/migrate.py +1 -1
- spritegen_cli-0.4.0/src/spritegen/report.py +284 -0
- spritegen_cli-0.4.0/src/spritegen/settings.py +420 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/sheet.py +7 -4
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/skill/__init__.py +128 -19
- spritegen_cli-0.4.0/src/spritegen/stages/__init__.py +80 -0
- spritegen_cli-0.4.0/src/spritegen/stages/_common.py +105 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/anchor.py +7 -19
- spritegen_cli-0.2.0/src/spritegen/stages/__init__.py → spritegen_cli-0.4.0/src/spritegen/stages/catalog.py +47 -155
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/matte.py +17 -4
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/motion.py +30 -19
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/pose.py +10 -27
- spritegen_cli-0.4.0/src/spritegen/stages/registry.py +59 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/video.py +16 -61
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/upscale.py +43 -16
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/workspace.py +119 -277
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/conftest.py +41 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_anchor.py +4 -12
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_board_stage.py +3 -10
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_cli.py +54 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_clip.py +54 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_fal.py +120 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_imaging.py +24 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_ledger.py +59 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_matte.py +29 -8
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_matting.py +36 -3
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_migrate.py +5 -3
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_motion.py +26 -11
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_pose.py +6 -8
- spritegen_cli-0.4.0/tests/test_settings.py +462 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_sheet.py +24 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_show.py +3 -3
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_skill.py +157 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_stages.py +30 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_upscale.py +116 -3
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_versions.py +0 -7
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_video.py +3 -11
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_workspace.py +164 -24
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/uv.lock +217 -195
- spritegen_cli-0.2.0/src/spritegen/__init__.py +0 -3
- spritegen_cli-0.2.0/src/spritegen/settings.py +0 -127
- spritegen_cli-0.2.0/tests/test_settings.py +0 -165
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/agents/code-review.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/agents/security-review.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-adr.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-codewiki.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-glossary.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-init.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-plan-run.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-prd.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-stack.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/commands/scc-wiki.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/artifacts.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/autonomy.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/caveman.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/code-search.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/delivery.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/knowledge-base.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/methodology.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/notes.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/prior-art.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/project.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/routing.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/specs.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/tasks.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/rules/verification.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/scc-manifest.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/adr/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/codewiki/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/glossary/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/init/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/plan-run/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/prd/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/stack/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.claude/skills/wiki/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.env.template +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.gitattributes +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/.python-version +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/CLAUDE.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/README.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0001-asset-directory-and-no-path-arguments.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0002-transfer-movement-instead-of-generating-frames.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0004-allow-list-every-downloaded-host.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0005-a-directory-per-artifact-kind.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0007-heavy-dependencies-are-optional-extras.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0008-local-backends-are-the-default.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0009-walk-the-redirect-chain-here.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0010-ci-on-three-operating-systems.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0011-keep-pixelfixer-out-of-the-distribution.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0012-publish-with-one-secret.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0013-require-python-3-13.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/adr/0014-every-run-is-a-version-and-the-state-names-the-chosen-one.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/stack.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/changelog.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/index.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/configuration.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/grid-and-palette-recovery.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/local-instead-of-paid.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/motion-transfer.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/paid-calls-and-the-ledger.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-asset-directory.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-generated-skill.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/docs/wiki/pages/the-pipeline.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/plans/motion-optimisation.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/specs/artifact-versions/design.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/specs/artifact-versions/requirements.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/specs/artifact-versions/tasks.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/endpoints.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/prompts.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/rrdb.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/skill/files/SKILL.md +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/src/spritegen/stages/board.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/helpers.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/anchor_crop.pixelart.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_chroma.cut.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_matted.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_row.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/synthetic_video_board.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.gif +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/expected/walk_south_row.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/input/anchor_crop.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_board.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_chroma.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/input/synthetic_matted.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/input/walk_south_board.png +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/parity/manifest.json +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_atlas.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_drive.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_parity.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/test_prompts.py +0 -0
- {spritegen_cli-0.2.0 → spritegen_cli-0.4.0}/tests/tests_fal_doubles.py +0 -0
|
@@ -217,3 +217,12 @@ __marimo__/
|
|
|
217
217
|
# Streamlit
|
|
218
218
|
.streamlit/secrets.toml
|
|
219
219
|
assets/
|
|
220
|
+
|
|
221
|
+
# CodeGraph index — never committed
|
|
222
|
+
.codegraph/
|
|
223
|
+
|
|
224
|
+
# ai-jail sandbox configuration, local to a checkout
|
|
225
|
+
.ai-jail
|
|
226
|
+
|
|
227
|
+
# the spritegen workspace's own settings, and its key
|
|
228
|
+
.spritegen.json
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# The ledger records a call before the files it writes
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`adr:0003-append-only-jsonl-ledger` puts one line per paid call in `ledger.jsonl`,
|
|
10
|
+
carrying the endpoint, the payload, the URLs returned and the sha256 of every file
|
|
11
|
+
written. A line can only carry those digests once the files exist, so the write had to
|
|
12
|
+
wait for them.
|
|
13
|
+
|
|
14
|
+
What sits between the endpoint answering and the files landing is a download, a decode
|
|
15
|
+
and a pack. Any of it can fail — a dropped transfer, a clip `av` will not open, a full
|
|
16
|
+
disk — and the money is already spent when it does. The ledger is the only record that
|
|
17
|
+
a call happened, and in exactly that case it recorded nothing at all: the failure it
|
|
18
|
+
exists to survive was the one it did not.
|
|
19
|
+
|
|
20
|
+
Appending is what makes the file safe to write from a run that dies, and it is also
|
|
21
|
+
what stops the line being completed later. A record cannot be both written early and
|
|
22
|
+
filled in afterwards.
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
A paid call is **two lines joined by a `call` id**, not one.
|
|
27
|
+
|
|
28
|
+
The first is written the moment the endpoint answers, before anything is downloaded:
|
|
29
|
+
`kind: "call"`, with the endpoint, the redacted payload and the URLs. The second is
|
|
30
|
+
written when the files land: `kind: "files"`, with the sha256 of each. A stage whose
|
|
31
|
+
whole output is what the call returned, with nothing to download, still writes one
|
|
32
|
+
line.
|
|
33
|
+
|
|
34
|
+
`summarise` counts a call where `kind` is not `"files"`, so the two lines fold to one
|
|
35
|
+
call and one set of files.
|
|
36
|
+
|
|
37
|
+
## Consequences
|
|
38
|
+
|
|
39
|
+
A run that dies mid-download leaves a `call` line with no `files` line. That is the
|
|
40
|
+
point: the asset's ledger says the money went, and the absent second line says nothing
|
|
41
|
+
landed. `cost` reports the call.
|
|
42
|
+
|
|
43
|
+
A line written before this decision has no `kind`, and is read as a whole call on its
|
|
44
|
+
own — old ledgers keep totalling to what they always did, and the format stays
|
|
45
|
+
append-only, so nothing on disk is rewritten.
|
|
46
|
+
|
|
47
|
+
The cost is that a reader can no longer assume one line is one call, and that the two
|
|
48
|
+
lines of a call are adjacent only in practice — a concurrent run against one asset
|
|
49
|
+
would interleave them, and the `call` id is what makes that legible rather than the
|
|
50
|
+
line order.
|
|
51
|
+
|
|
52
|
+
This supersedes `adr:0003-append-only-jsonl-ledger` on the shape of a line only. Its
|
|
53
|
+
other decisions — append-only, upload URLs redacted, one file per asset — stand.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# A workspace is marked by the file that configures it
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`adr:0006-centralise-configuration-and-never-cache-it` reads settings from the
|
|
10
|
+
environment, the nearest `.env`, and the declared default, in that order. Where the
|
|
11
|
+
assets are was a separate question, answered by walking up from the working directory
|
|
12
|
+
looking for a directory named `spritegen/assets`.
|
|
13
|
+
|
|
14
|
+
That walk had nothing to recognise. A directory of the right name is not a claim to be
|
|
15
|
+
one, so the search could not tell a workspace from a coincidence — and its ceiling
|
|
16
|
+
admitted the home directory as a candidate. Run in `~/spritegen1/test1`, `init`
|
|
17
|
+
reported a workspace at `~/spritegen/assets` and adopted it: one workspace became every
|
|
18
|
+
project's, and a second project on the same machine was not possible without giving it
|
|
19
|
+
a repository first.
|
|
20
|
+
|
|
21
|
+
The two questions turned out to be one. A project that has its own assets usually wants
|
|
22
|
+
its own cell size, its own matte backend, and — where more than one account is in play —
|
|
23
|
+
its own key.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
**A directory holding `.spritegen.json` is a workspace root.** Its assets are
|
|
28
|
+
`<root>/spritegen/assets`, and the nearest root at or above the working directory is
|
|
29
|
+
the one a command uses.
|
|
30
|
+
|
|
31
|
+
The same file carries the settings, as a JSON object whose names are the `Settings`
|
|
32
|
+
fields without the environment's prefix. It sits **between the environment and the
|
|
33
|
+
`.env`**: an exported variable still wins, which is what makes a one-off override work,
|
|
34
|
+
and a machine with one key in `~/.env` keeps working. A name in it that is not a setting
|
|
35
|
+
is refused rather than ignored.
|
|
36
|
+
|
|
37
|
+
**The home directory is a candidate only when it is where the command was typed.** A
|
|
38
|
+
marker there serves a command run in it and is never climbed into from below.
|
|
39
|
+
|
|
40
|
+
`init` creates the workspace where it was run, always. It writes the marker with no
|
|
41
|
+
credential in it, adds it to a `.gitignore` that exists, and warns when the marker is
|
|
42
|
+
already tracked by git.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
Several workspaces on one machine work, and they work without a repository: the marker
|
|
47
|
+
is the boundary that `.git` used to have to stand in for.
|
|
48
|
+
|
|
49
|
+
A credential can live in a file inside a project, which is why `init` writes it into
|
|
50
|
+
`.gitignore` and warns when git already has it. `allowed_hosts` is settable there too
|
|
51
|
+
(`adr:0004-allow-list-every-downloaded-host`) — the same reach a `.env` already had,
|
|
52
|
+
bounded by the same ceiling.
|
|
53
|
+
|
|
54
|
+
Two files now answer the same question, and that is the cost. `.env` is kept because
|
|
55
|
+
removing it would break every machine that has a key today; whether it eventually goes
|
|
56
|
+
is deliberately not decided here.
|
|
57
|
+
|
|
58
|
+
A workspace opened before this exists has no marker. It keeps working through the
|
|
59
|
+
unmarked fallback, which now holds the same line about the home directory — but it will
|
|
60
|
+
not be found from a sibling project, and `init` in its root is what gives it one.
|
|
61
|
+
|
|
62
|
+
This supersedes `adr:0006-centralise-configuration-and-never-cache-it` **on the list of
|
|
63
|
+
sources only**. One class, nothing cached, and the ceiling all stand.
|
|
@@ -6,7 +6,7 @@ goes wrong rather than because of a way it works.
|
|
|
6
6
|
|
|
7
7
|
## A URL an endpoint returned is still a URL somebody could have chosen
|
|
8
8
|
|
|
9
|
-
[src/spritegen/fal.py:24-
|
|
9
|
+
[src/spritegen/fal.py:24-88]()
|
|
10
10
|
|
|
11
11
|
The allowed hosts are a fixed tuple plus whatever `SPRITEGEN_ALLOWED_HOSTS` adds. The
|
|
12
12
|
scheme must be https, and an authority carrying userinfo is refused outright — `https://
|
|
@@ -16,7 +16,7 @@ check.
|
|
|
16
16
|
|
|
17
17
|
## The redirect chain is walked here, not by the client
|
|
18
18
|
|
|
19
|
-
[src/spritegen/fal.py:
|
|
19
|
+
[src/spritegen/fal.py:153-218]()
|
|
20
20
|
|
|
21
21
|
This is the reason `httpx` is a dependency at all. A client following redirects itself
|
|
22
22
|
would apply the host check to the URL that was handed in and to nothing after it, so one
|
|
@@ -25,7 +25,7 @@ hop out of the allowed set is enough to fetch from anywhere. Every hop is checke
|
|
|
25
25
|
|
|
26
26
|
## One door, and two refusals before anything is spent
|
|
27
27
|
|
|
28
|
-
[src/spritegen/fal.py:
|
|
28
|
+
[src/spritegen/fal.py:220-241]()
|
|
29
29
|
|
|
30
30
|
`require_key` and the `dry_run` branch live in `call` rather than in each stage, so no
|
|
31
31
|
stage can forget one. `_subscribe` is the only function that touches `fal_client`, and it
|
|
@@ -34,7 +34,7 @@ without an HTTP client and what lets a test stand in for the endpoint without on
|
|
|
34
34
|
|
|
35
35
|
## The record redacts what would otherwise be a key
|
|
36
36
|
|
|
37
|
-
[src/spritegen/ledger.py:
|
|
37
|
+
[src/spritegen/ledger.py:30-65]()
|
|
38
38
|
|
|
39
39
|
`fal_client.upload_file` returns standing access to the uploaded file for anyone holding
|
|
40
40
|
the link. Writing those into the ledger verbatim would turn a per-asset record into a way
|
|
@@ -44,7 +44,7 @@ named explicitly.
|
|
|
44
44
|
|
|
45
45
|
## One line, appended, never rewritten
|
|
46
46
|
|
|
47
|
-
[src/spritegen/ledger.py:
|
|
47
|
+
[src/spritegen/ledger.py:81-141]()
|
|
48
48
|
|
|
49
49
|
JSONL and `append` rather than a document re-serialised whole, because a run that dies
|
|
50
50
|
halfway has still spent the money. A format that rewrites the file to add a line can lose
|
|
@@ -5,9 +5,14 @@ builds its sub-commands from it, `workspace` checks order against it, and `skill
|
|
|
5
5
|
generates the published tables out of it. Reading the file tells you what it holds;
|
|
6
6
|
this page is why it holds it there rather than in the stages.
|
|
7
7
|
|
|
8
|
+
It is three files. `registry.py` is the two dataclasses a stage is described with,
|
|
9
|
+
`catalog.py` is the table itself — four fifths of the whole by line count and none of
|
|
10
|
+
its behaviour — and `__init__.py` is the lookup, which is what a reader of the registry
|
|
11
|
+
came for.
|
|
12
|
+
|
|
8
13
|
## An option is declared by the registry, not by the stage that takes it
|
|
9
14
|
|
|
10
|
-
[src/spritegen/stages/
|
|
15
|
+
[src/spritegen/stages/registry.py:12-26]()
|
|
11
16
|
|
|
12
17
|
The obvious place for `--size` is inside `anchor`. It is here instead because `--help`
|
|
13
18
|
has to be complete, and building help from the stages would mean importing five modules
|
|
@@ -16,7 +21,7 @@ value off the parsed namespace and knows nothing about how it was parsed.
|
|
|
16
21
|
|
|
17
22
|
## `requires` is any-one-of, because the pipeline forks
|
|
18
23
|
|
|
19
|
-
[src/spritegen/stages/
|
|
24
|
+
[src/spritegen/stages/registry.py:29-54]()
|
|
20
25
|
|
|
21
26
|
`motion` and `video` produce the same artifact by different routes, and `matte` takes
|
|
22
27
|
whichever ran. A field meaning *all of these* would have no way to express that, and the
|
|
@@ -24,7 +29,7 @@ fork is the shape of the tool rather than an exception in it.
|
|
|
24
29
|
|
|
25
30
|
## Kind and variant, once the stage stopped being the artifact
|
|
26
31
|
|
|
27
|
-
[src/spritegen/stages/
|
|
32
|
+
[src/spritegen/stages/registry.py:29-59]()
|
|
28
33
|
|
|
29
34
|
`produces` was the stage's name for as long as a stage made one thing. `anchor` makes an
|
|
30
35
|
anchor, box art or an icon; `board` closes one set of frames into as many art directions
|
|
@@ -38,7 +43,7 @@ Both name an option this stage already declares, and neither is turned into a pa
|
|
|
38
43
|
|
|
39
44
|
## Order is derived, and there is no list of it
|
|
40
45
|
|
|
41
|
-
[src/spritegen/stages/__init__.py:
|
|
46
|
+
[src/spritegen/stages/__init__.py:43-63]()
|
|
42
47
|
|
|
43
48
|
`order` walks the table rather than reading a sequence somebody maintained beside it. A
|
|
44
49
|
second record of the pipeline's shape would be the one that goes stale the first time a
|
|
@@ -46,7 +51,7 @@ stage is added.
|
|
|
46
51
|
|
|
47
52
|
## The module is resolved only when the command has been chosen
|
|
48
53
|
|
|
49
|
-
[src/spritegen/stages/__init__.py:
|
|
54
|
+
[src/spritegen/stages/__init__.py:66-80]()
|
|
50
55
|
|
|
51
56
|
This is the reason the registry can describe the whole pipeline without importing Pillow,
|
|
52
57
|
numpy or `fal_client`. It also decides the blast radius of a broken stage: an import
|
|
@@ -6,7 +6,7 @@ refusing things.
|
|
|
6
6
|
|
|
7
7
|
## Every upward search stops, and the stop is the security control
|
|
8
8
|
|
|
9
|
-
[src/spritegen/workspace.py:
|
|
9
|
+
[src/spritegen/workspace.py:70-101]()
|
|
10
10
|
|
|
11
11
|
A command is typed from wherever is convenient, so searching upward for the project is
|
|
12
12
|
right. Searching upward without a ceiling reaches the filesystem root, and everything
|
|
@@ -16,7 +16,7 @@ directory nobody chose to configure this tool.
|
|
|
16
16
|
|
|
17
17
|
## Where the assets are, and why the order is the design
|
|
18
18
|
|
|
19
|
-
[src/spritegen/workspace.py:
|
|
19
|
+
[src/spritegen/workspace.py:103-138]()
|
|
20
20
|
|
|
21
21
|
The explicit variable wins first, which is what lets a test and a second collection of
|
|
22
22
|
characters exist at all. The nearest existing `spritegen/assets/` comes next because that
|
|
@@ -25,9 +25,9 @@ else's, so the images belong to that project rather than to the tool.
|
|
|
25
25
|
|
|
26
26
|
## A name off a command line becomes a directory
|
|
27
27
|
|
|
28
|
-
[src/spritegen/workspace.py:
|
|
28
|
+
[src/spritegen/workspace.py:49-68]()
|
|
29
29
|
|
|
30
|
-
[src/spritegen/workspace.py:
|
|
30
|
+
[src/spritegen/workspace.py:140-168]()
|
|
31
31
|
|
|
32
32
|
The forbidden set and the reserved names are Windows', and they are checked on every
|
|
33
33
|
platform. An asset name arrives from an argument or from a state file written earlier,
|
|
@@ -36,7 +36,7 @@ on Windows — which is why CI runs the suite on three operating systems rather
|
|
|
36
36
|
|
|
37
37
|
## Kind, variant and version become a path in exactly one place
|
|
38
38
|
|
|
39
|
-
[src/spritegen/workspace.py:
|
|
39
|
+
[src/spritegen/workspace.py:476-505]()
|
|
40
40
|
|
|
41
41
|
Every stage writes through this. That is what let layout 1 become layout 2 without each
|
|
42
42
|
stage learning about the move, layout 2 become layout 3 the same way, and it is what
|
|
@@ -44,9 +44,9 @@ stage learning about the move, layout 2 become layout 3 the same way, and it is
|
|
|
44
44
|
|
|
45
45
|
## Which version a run writes, and which one it reads
|
|
46
46
|
|
|
47
|
-
[src/spritegen/workspace.py:
|
|
47
|
+
[src/spritegen/workspace.py:527-541]()
|
|
48
48
|
|
|
49
|
-
[src/spritegen/workspace.py:
|
|
49
|
+
[src/spritegen/workspace.py:667-684]()
|
|
50
50
|
|
|
51
51
|
Two records go into picking a number and neither is trusted over the other: what the
|
|
52
52
|
state file recorded, and what is on disk. They disagree when a run died between writing
|
|
@@ -64,7 +64,7 @@ has none, so a re-run cannot re-aim the paid stages that follow it — that is
|
|
|
64
64
|
|
|
65
65
|
## A recorded path is not trusted
|
|
66
66
|
|
|
67
|
-
[src/spritegen/workspace.py:
|
|
67
|
+
[src/spritegen/workspace.py:547-570]()
|
|
68
68
|
|
|
69
69
|
The state file holds relative paths, and it is a file on disk that something else may
|
|
70
70
|
have written. `inside` is the check that a recorded path still resolves within the asset
|
|
@@ -73,7 +73,7 @@ another asset's output because the state said so.
|
|
|
73
73
|
|
|
74
74
|
## What "ready" means when `requires` forks
|
|
75
75
|
|
|
76
|
-
[src/spritegen/workspace.py:
|
|
76
|
+
[src/spritegen/workspace.py:414-454]()
|
|
77
77
|
|
|
78
78
|
`satisfied` is where the registry's any-one-of semantics turn into a decision, and
|
|
79
79
|
`next_stages` is what `status` prints. Both read the registry; neither keeps a copy of
|
|
@@ -9,6 +9,9 @@ directory `row/` to `sheet/as-is/` — retired a spelling whose word is still in
|
|
|
9
9
|
use elsewhere, and listing it would report every correct use as a finding. Add an
|
|
10
10
|
`Avoid:` the first time a genuinely dead, distinctive name turns up.
|
|
11
11
|
|
|
12
|
+
- **workspace** — a directory holding `.spritegen.json`, and the assets under `spritegen/assets` inside it. One machine carries several, and a command uses the nearest one at or above where it was typed.
|
|
13
|
+
- **workspace root** — the directory the marker is in, which is what `status` names and what `init` creates in. Not the assets directory, which sits inside it.
|
|
14
|
+
- **marker** — `.spritegen.json`: the file that says a directory is a workspace, and holds that workspace's settings. Avoid: config file, settings file
|
|
12
15
|
- **asset** — everything belonging to one character, under `assets/<name>/`. The unit `new`, `status`, `show` and `cost` all talk about, and the reason no stage takes an input or output path.
|
|
13
16
|
- **state file** — `state.json` inside an asset, the only record of progress. It holds the stages that have completed, never what should happen next.
|
|
14
17
|
- **stage** — one step of the pipeline, declared in `stages.STAGES` and implemented by the module of the same name. Most spend money; `board` does not.
|
|
@@ -41,3 +41,7 @@ over this file answers with the example above as well as with the notes. -->
|
|
|
41
41
|
- n-0001 2026-08-28 #gotcha @src/spritegen/workspace.py — Context.version is resolved on the first ask for out and kept, so a stage that opens its output twice in one run writes both halves to the same version
|
|
42
42
|
- n-0002 2026-08-28 #gotcha @tests/conftest.py — settings.DEFAULT_CACHE is built from Path.home() at import, so conftest moving home afterwards does not move the cache; the suite points SPRITEGEN_CACHE at a temporary directory instead
|
|
43
43
|
- n-0003 2026-08-28 #gotcha @pyproject.toml — the assets root spritegen/ in the repo root shadows the package name, so coverage source=[spritegen] resolves to the empty assets directory and collects nothing — source_pkgs is the unambiguous form
|
|
44
|
+
- n-0004 2026-08-28 #gotcha @src/spritegen/sheet.py — the GIF preview writes one global palette plus one shared local palette whatever the frames were quantised with — Pillow unifies on save, so quantising per frame and quantising the strip produce the same file
|
|
45
|
+
- n-0005 2026-08-28 #gotcha @src/spritegen/upscale.py — the weight cache sidecar must stay a content hash — size and mtime are forgeable with the same write access a swap needs, so a stat-based fast path silently disables the check
|
|
46
|
+
- n-0006 2026-08-28 #gotcha @docs/codewiki/the-workspace.md — a codewiki citation that still resolves after a refactor is not still correct — the range shifts onto a neighbouring function and validate cannot tell, so check what each range opens on
|
|
47
|
+
- n-0007 2026-08-29 #gotcha @src/spritegen/__init__.py — the CLI version came from a hand-written __version__ in spritegen/__init__.py, a second copy of what pyproject.toml declares — it read 0.1.0 through three releases; it is importlib.metadata now and a test pins the two together
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
autonomy: auto
|
|
3
|
+
ci: wait
|
|
4
|
+
status: approved
|
|
5
|
+
pr: per-group
|
|
6
|
+
merge: auto
|
|
7
|
+
checksum: a381e0be7b8276b7d56f6be879477d27acc6bab7600bae8957b6bee7d8d0b577
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Code health
|
|
11
|
+
|
|
12
|
+
Clear what a full read of `src/` and `tests/` turned up: ten defects, seven places
|
|
13
|
+
that repeat work already done, four modules that carry more than one job, and a test
|
|
14
|
+
suite that pays for video encoding no assertion reads.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
None of this was found by a failure — the suite is green and has been. It is what a
|
|
19
|
+
deliberate read found sitting under green tests, which is why it is worth landing as
|
|
20
|
+
one piece of work rather than waiting for each item to announce itself. Three of the
|
|
21
|
+
defects lose something a user cannot get back: a paid call whose ledger line is never
|
|
22
|
+
written, a re-fetch that deletes the good file it was going to replace, and a weight
|
|
23
|
+
file that stops being checked the moment its digest sidecar goes missing. The rest is
|
|
24
|
+
waste that compounds — a 64 MB model re-read per image, a colour count that allocates
|
|
25
|
+
one tuple per pixel of an upscaled frame, twenty-five tests re-encoding the same clip
|
|
26
|
+
at the slowest x264 preset. Done when the defects are fixed with tests that would have
|
|
27
|
+
caught them, the repeated work happens once, the four oversized modules are split
|
|
28
|
+
along the seams the survey mapped, and the suite is faster with no coverage lost.
|
|
29
|
+
|
|
30
|
+
## Paths
|
|
31
|
+
|
|
32
|
+
- `src/spritegen/` — `workspace.py`, `fal.py`, `upscale.py`, `imaging.py`, `clip.py`,
|
|
33
|
+
`drive.py`, `matting.py`, `sheet.py`, `cli.py`, `atlas.py`, `ledger.py`
|
|
34
|
+
- `src/spritegen/stages/` — `__init__.py`, `motion.py`, `pose.py`, `video.py`,
|
|
35
|
+
`anchor.py`, `matte.py`, `board.py`
|
|
36
|
+
- `src/spritegen/skill/__init__.py`
|
|
37
|
+
- `tests/` — `conftest.py`, `helpers.py`, `test_motion.py`, `test_video.py`
|
|
38
|
+
|
|
39
|
+
## References
|
|
40
|
+
|
|
41
|
+
- `adr:0003-append-only-jsonl-ledger` — one line per paid call, carrying the sha256 of
|
|
42
|
+
every file written. Task 1.1 cannot hold to both halves at once and supersedes it.
|
|
43
|
+
- `adr:0006-centralise-configuration-and-never-cache-it` — nothing is cached and
|
|
44
|
+
`load()` builds a new object every call. Task 3.5 resolves once per command and
|
|
45
|
+
passes it down, which is the shape that ADR permits; a module-level cache is not.
|
|
46
|
+
- `adr:0004-allow-list-every-downloaded-host` — the host check task 1.9 walks results
|
|
47
|
+
for.
|
|
48
|
+
- `adr:0007-heavy-dependencies-are-optional-extras` — why `available()` exists at all,
|
|
49
|
+
which task 4.7 decides the fate of.
|
|
50
|
+
|
|
51
|
+
## Out of scope
|
|
52
|
+
|
|
53
|
+
- The unbounded recursion in `fal.seed_in` and `fal.urls_in`: a hostile response nested
|
|
54
|
+
a thousand levels deep raises `RecursionError` after billing. Hosts are allow-listed
|
|
55
|
+
and the impact is one denied run; it is filed on PR #17 and belongs with a review of
|
|
56
|
+
what else trusts endpoint JSON shape.
|
|
57
|
+
- Introducing `ruff format`. No formatter is configured here and adding one rewrites
|
|
58
|
+
the tree in a commit nobody can read.
|
|
59
|
+
- Raising the coverage floor. The real number is 91.36% against a floor of 86, but it
|
|
60
|
+
has been there for one day.
|
|
61
|
+
|
|
62
|
+
## Tasks
|
|
63
|
+
|
|
64
|
+
- [x] 1.1 (TDD) Record the paid call in the ledger before the files it writes, and supersede `adr:0003` with the two-line shape
|
|
65
|
+
- [x] 1.2 (TDD) Download to a partial file and rename on success, so a failed re-fetch cannot delete the good file
|
|
66
|
+
_Depends 1.1_
|
|
67
|
+
- [x] 1.3 (TDD) Write the weight digest when the sidecar is missing, and stop the `finally` arm deleting a completed download
|
|
68
|
+
- [x] 1.4 (Unit) Default `--device` to `None` so the setting is reachable and a dry run names the device the real run will use
|
|
69
|
+
- [x] 1.5 (Unit) Make `matte --mask` on the local backend either write the mask or say it cannot
|
|
70
|
+
- [ ] 1.6 (Unit) Quantise the GIF preview once over the strip instead of a palette per
|
|
71
|
+
frame
|
|
72
|
+
_Status removed_
|
|
73
|
+
_Reason measured against the parity fixture: Pillow already writes one global palette plus one shared local palette whatever the frames were quantised with, so the per-frame ADAPTIVE call produces the same two palettes the strip does — the change grew the file by 800 bytes, altered no palette, and broke a documented parity baseline_
|
|
74
|
+
- [x] 1.7 (Unit) Map transport, codec and OS errors to exit codes, and narrow the catch that turns an internal bug into a user message
|
|
75
|
+
- [x] 1.8 (Unit) Name the file when `state.json` does not parse
|
|
76
|
+
- [x] 1.9 (Unit) Keep walking under a `url` key whose value is not a string
|
|
77
|
+
- [x] 1.10 (Unit) Generate the skill's free-command table from the parser instead of maintaining a second copy
|
|
78
|
+
- [x] 2.1 (Unit) Build the upscale network once per command and pass it to each file
|
|
79
|
+
- [x] 2.2 (Unit) Count colours before the upscale and without a tuple per pixel
|
|
80
|
+
- [x] 2.3 (TDD) Choose the wanted frame indices before decoding, and keep only those
|
|
81
|
+
- [ ] 2.4 (Unit) Re-hash a cached weight only when its size or mtime changed
|
|
82
|
+
_Status removed_
|
|
83
|
+
_Reason the win it chased was already delivered by 2.1 — with the network built once per command the weight is hashed once per command either way — and the mechanism defeated the integrity check beside it: size and mtime are forgeable with the same write access the attack needs, so a matching stamp let a swapped weight through unhashed and silent_
|
|
84
|
+
- [x] 2.5 (Unit) Resolve settings once per command and pass them down, adding no cache
|
|
85
|
+
_Depends 1.4_
|
|
86
|
+
- [x] 2.6 (Unit) Measure alpha in memory, read the sheet once per build, and skip the crops a run without a GIF throws away
|
|
87
|
+
- [x] 2.7 (Unit) Compare found against recorded with sets, not lists
|
|
88
|
+
- [x] 3.1 (Unit) Move the stage catalogue out of `stages/__init__.py` into its own module
|
|
89
|
+
- [ ] 3.2 (Unit) Declare the chroma, prompt, reference and frame-extraction options once
|
|
90
|
+
and splice them into each stage
|
|
91
|
+
_Depends 3.1_
|
|
92
|
+
_Status removed_
|
|
93
|
+
_Reason only the chroma and despill cluster was duplicated verbatim, and that is done. The prompt, reference and frame-extraction options share flags, kinds and defaults but not their help text: motion explains that the driving cycle caps --frames and video explains that an image-to-video clip opens on a still input image, which is the part of an option worth having. Replaced by 3.8_
|
|
94
|
+
- [x] 3.3 (Unit) Give `Context` the anchor lookup the three stages copy, and drop the cross-stage import
|
|
95
|
+
- [x] 3.4 (Unit) Extract the reference-file check, the missing-URL guard and the chroma finish that each exist three or four times
|
|
96
|
+
_Depends 3.3_
|
|
97
|
+
- [x] 3.5 (Unit) Extract the layout refusal and the artifact-key composition to one place each
|
|
98
|
+
- [x] 3.6 (Unit) Have the two grid packers share their arithmetic
|
|
99
|
+
- [x] 3.7 (Unit) Split `workspace.py` along the seams the survey mapped, presentation first
|
|
100
|
+
_Depends 3.3, 3.5_
|
|
101
|
+
- [x] 4.1 (Unit) Delete what nothing calls, and decide whether the availability helpers get a caller or go
|
|
102
|
+
- [x] 5.1 (Unit) Stop the motion tests encoding at 720 and the slowest preset
|
|
103
|
+
- [ ] 5.2 (Unit) Share the cache across the session so the same driving clip encodes
|
|
104
|
+
once
|
|
105
|
+
_Depends 5.1_
|
|
106
|
+
_Status removed_
|
|
107
|
+
_Reason measured after 5.1: an encode is 20ms at the cell size the tests now use, so sharing the cache across the session saves about 1s of a 20.8s suite, bought with mutable state shared across 799 tests against the one fixture whose purpose is isolating them_
|
|
108
|
+
- [x] 5.3 (Unit) Move the fixtures copied across eleven files into `conftest.py`
|
|
109
|
+
_Depends 5.1_
|
|
110
|
+
- [x] 3.8 (Unit) Declare the chroma and despill options once and splice them into the
|
|
111
|
+
three stages that cut
|
|
112
|
+
_Reason 3.2 asked for four option groups and only one of them was duplication; this is what was actually built_
|
|
113
|
+
|
|
114
|
+
## Done when
|
|
115
|
+
|
|
116
|
+
- `uv run pytest -q --cov` passes with coverage no lower than 91.36%.
|
|
117
|
+
- `uv run ruff check src tests` and `scc validate` both exit 0.
|
|
118
|
+
- Every task above is ticked or struck out with a reason.
|
|
119
|
+
- The suite's wall-clock time is lower than the 34 s it takes now, measured the same way.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Workspace configuration — design
|
|
2
|
+
|
|
3
|
+
## What changes, and where
|
|
4
|
+
|
|
5
|
+
Today a workspace is found by looking for a directory that happens to be named
|
|
6
|
+
`spritegen/assets`, walking up from the working directory. That has no marker, so the
|
|
7
|
+
walk cannot tell a workspace from a coincidence — and it reaches the home directory,
|
|
8
|
+
where one workspace becomes every project's. Run from `~/spritegen1/test1`, `init`
|
|
9
|
+
reported a workspace at `~/spritegen/assets`, which belongs to something else.
|
|
10
|
+
|
|
11
|
+
The change is a marker. `.spritegen.json` at the root of a workspace says *this is
|
|
12
|
+
one*, the way `package.json` does for npm and `pyproject.toml` for this repository. The
|
|
13
|
+
walk stops at the nearest one, so a second project one directory over is a second
|
|
14
|
+
workspace, and nothing has to be configured for that to be true.
|
|
15
|
+
|
|
16
|
+
The same file carries the settings, which is the other half: a credential and a cell
|
|
17
|
+
size belong to the project they are for, not to the shell that happened to run the
|
|
18
|
+
command.
|
|
19
|
+
|
|
20
|
+
- `settings.py` — a source between the environment and `.env`, and the search that
|
|
21
|
+
finds it.
|
|
22
|
+
- `workspace.py` — `assets_root` resolves through the marker rather than by looking for
|
|
23
|
+
a directory of the right name.
|
|
24
|
+
- `skill/__init__.py` — `cmd_init` creates in the working directory and writes the file.
|
|
25
|
+
|
|
26
|
+
## The order settings are read in
|
|
27
|
+
|
|
28
|
+
Highest wins:
|
|
29
|
+
|
|
30
|
+
1. **The environment.** `SPRITEGEN_CELL=200 spritegen board …` still overrides for one
|
|
31
|
+
run, which is `adr:0006`'s reason and does not change.
|
|
32
|
+
2. **`.spritegen.json`** at the workspace root — the new one, and where a project's own
|
|
33
|
+
answer belongs.
|
|
34
|
+
3. **The nearest `.env`** — kept, because a machine with one `FAL_KEY` for everything is
|
|
35
|
+
a real setup and breaking it buys nothing.
|
|
36
|
+
4. **The declared default.**
|
|
37
|
+
|
|
38
|
+
A name in the file that is not a setting is refused rather than ignored: a typo that
|
|
39
|
+
does nothing is worse than one that says so, and this is the file a `fal_key` is
|
|
40
|
+
misspelled in.
|
|
41
|
+
|
|
42
|
+
## The file
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"fal_key": "…",
|
|
47
|
+
"cell": 166,
|
|
48
|
+
"matte_backend": "local"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every field is a `Settings` field under its own name, without the `SPRITEGEN_` prefix
|
|
53
|
+
the environment uses — the prefix exists to keep a shared namespace apart, and a file
|
|
54
|
+
of this tool's own has no such namespace to share.
|
|
55
|
+
|
|
56
|
+
## The credential, and git
|
|
57
|
+
|
|
58
|
+
`init` writes the file with no credential in it, and adds it to `.gitignore` where one
|
|
59
|
+
exists. It cannot add it where there is no `.gitignore`: writing one is a decision
|
|
60
|
+
about a repository this tool does not own.
|
|
61
|
+
|
|
62
|
+
That leaves the case the guard is for — a `.spritegen.json` somebody committed before
|
|
63
|
+
putting a key in it. `git ls-files --error-unmatch` answers whether it is tracked in one
|
|
64
|
+
call and needs no repository when there is none, so a warning is cheap. It warns and
|
|
65
|
+
proceeds: refusing would make the tool unusable in exactly the situation somebody is
|
|
66
|
+
trying to fix.
|
|
67
|
+
|
|
68
|
+
## What a file may point at
|
|
69
|
+
|
|
70
|
+
A path out of this file is **relative to the workspace and resolved under it** — R4.4,
|
|
71
|
+
R4.5. The environment keeps the unrestricted form, and the difference is who decided:
|
|
72
|
+
exporting a variable takes a shell, while this file arrives by being cloned, extracted,
|
|
73
|
+
or synced onto a mounted drive, and is then read for no reason but the command having
|
|
74
|
+
been run in that directory.
|
|
75
|
+
|
|
76
|
+
The reachable case is worse than a redirected output directory. `Path.is_dir()` on
|
|
77
|
+
`\host\share` opens an SMB connection and authenticates, so `status` — free, and
|
|
78
|
+
touching no network — would hand a credential to whoever wrote the file. So rooted
|
|
79
|
+
counts, not only absolute: on Windows `/etc/x` carries no drive and is not
|
|
80
|
+
`is_absolute()`, and it leaves the workspace all the same.
|
|
81
|
+
|
|
82
|
+
**`allowed_hosts` becomes settable from a file inside the project**
|
|
83
|
+
(`adr:0004-allow-list-every-downloaded-host`). That is the same reach a `.env` already
|
|
84
|
+
had, and the ceiling in R1.5 is what bounds both — worth stating rather than
|
|
85
|
+
discovering.
|
|
86
|
+
|
|
87
|
+
## What this supersedes
|
|
88
|
+
|
|
89
|
+
`adr:0006-centralise-configuration-and-never-cache-it` names the environment, the
|
|
90
|
+
nearest `.env` and the default as the three sources, in that order. It gains a fourth
|
|
91
|
+
between the first two, and its other decisions — one class, nothing cached, the ceiling
|
|
92
|
+
— stand. A new record supersedes it on the source list alone.
|
|
93
|
+
|
|
94
|
+
## Not decided here
|
|
95
|
+
|
|
96
|
+
Whether `.env` is eventually dropped. Two files for one job is one too many, but
|
|
97
|
+
removing one while adding the other would break every machine that has a key today,
|
|
98
|
+
and nothing forces the choice now.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
autonomy: auto
|
|
3
|
+
ci: wait
|
|
4
|
+
branch: feat/workspace-config
|
|
5
|
+
delivery: in-review
|
|
6
|
+
pr: 24
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Workspace configuration — requirements
|
|
10
|
+
|
|
11
|
+
A workspace is marked by a file it holds, so one machine can carry several and a
|
|
12
|
+
command run inside one never reaches into another.
|
|
13
|
+
|
|
14
|
+
## R1 — where the workspace is
|
|
15
|
+
|
|
16
|
+
- **R1.1** The system shall treat a directory holding `.spritegen.json` as a workspace root.
|
|
17
|
+
- **R1.2** When a command needs a workspace, the system shall use the nearest workspace root at or above the working directory.
|
|
18
|
+
- **R1.3** If no workspace root is at or above the working directory, then the system shall refuse and name `spritegen init`.
|
|
19
|
+
- **R1.4** While a workspace root is in use, the system shall keep the assets under `<root>/spritegen/assets`.
|
|
20
|
+
- **R1.5** The system shall stop the upward search at the first repository boundary or at the home directory.
|
|
21
|
+
|
|
22
|
+
## R2 — opening one
|
|
23
|
+
|
|
24
|
+
- **R2.1** When `init` runs, the system shall create the workspace in the working directory.
|
|
25
|
+
- **R2.2** When `init` creates a workspace, the system shall write `.spritegen.json` in that directory.
|
|
26
|
+
- **R2.3** Where a `.gitignore` is at the workspace root, the system shall add `.spritegen.json` to it.
|
|
27
|
+
- **R2.4** If a workspace root is already at or above the working directory, then the system shall name it and create the new one anyway.
|
|
28
|
+
- **R2.5** If `.spritegen.json` is already in the working directory, then the system shall leave its contents alone.
|
|
29
|
+
|
|
30
|
+
## R3 — what configuration is read
|
|
31
|
+
|
|
32
|
+
- **R3.1** The system shall read its settings from `.spritegen.json` at the workspace root.
|
|
33
|
+
- **R3.2** Where a setting is in the environment, the environment shall win over the file.
|
|
34
|
+
- **R3.3** Where a setting is in neither, the system shall use the `.env` the search finds, and then the default.
|
|
35
|
+
- **R3.4** If `.spritegen.json` does not parse, then the system shall refuse naming the file and the reason.
|
|
36
|
+
- **R3.5** If `.spritegen.json` holds a name that is not a setting, then the system shall refuse naming it.
|
|
37
|
+
- **R3.6** The system shall build its settings fresh on every read and cache nothing.
|
|
38
|
+
|
|
39
|
+
## R4 — the key in it
|
|
40
|
+
|
|
41
|
+
- **R4.1** Where `.spritegen.json` holds `fal_key`, the system shall use it as the credential.
|
|
42
|
+
- **R4.2** When `init` writes `.spritegen.json`, the system shall write no credential into it.
|
|
43
|
+
- **R4.3** If `.spritegen.json` is tracked by git, then the system shall warn that a credential in it is committed.
|
|
44
|
+
- **R4.4** (ADDED) Where `.spritegen.json` sets a path, the system shall resolve it under the workspace root.
|
|
45
|
+
- **R4.5** (ADDED) If `.spritegen.json` sets a path that is absolute, rooted or outside the workspace, then the system shall refuse it.
|
|
46
|
+
|
|
47
|
+
## R5 — saying which one
|
|
48
|
+
|
|
49
|
+
- **R5.1** When `status` runs, the system shall name the workspace root it is using.
|
|
50
|
+
- **R5.2** When `init` finishes, the system shall name the directory it created.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Workspace configuration — tasks
|
|
2
|
+
|
|
3
|
+
## Tasks
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 (TDD) Find the workspace root by the marker file, bounded by the ceiling — R1.1, R1.2, R1.5
|
|
6
|
+
- [x] 1.2 (Unit) Resolve the assets under the root the marker names, and refuse when there is none — R1.3, R1.4
|
|
7
|
+
_Depends 1.1_
|
|
8
|
+
- [x] 2.1 (TDD) Read `.spritegen.json` between the environment and `.env`, refusing a name that is not a setting — R3.1, R3.2, R3.3, R3.4, R3.5, R3.6
|
|
9
|
+
_Depends 1.1_
|
|
10
|
+
- [x] 2.2 (Unit) Take the credential from the file — R4.1
|
|
11
|
+
_Depends 2.1_
|
|
12
|
+
- [x] 3.1 (Unit) Create the workspace where `init` was run, and write the marker with no credential in it — R2.1, R2.2, R2.4, R2.5, R4.2, R5.2
|
|
13
|
+
_Depends 1.2_
|
|
14
|
+
- [x] 3.2 (Unit) Add the marker to a `.gitignore` that is there — R2.3
|
|
15
|
+
_Depends 3.1_
|
|
16
|
+
- [x] 3.3 (Unit) Warn when the marker is tracked by git — R4.3
|
|
17
|
+
_Depends 3.1_
|
|
18
|
+
- [x] 4.1 (Unit) Name the workspace root in `status` — R5.1
|
|
19
|
+
_Depends 1.2_
|
|
20
|
+
- [x] 4.2 (Unit) Record the decision as an ADR superseding `adr:0006` on its source list, and the term in the glossary — R3.1
|
|
21
|
+
_Depends 2.1_
|
|
22
|
+
- [x] 3.4 (Unit) Stop the skill search climbing into the home directory's .claude — R1.5
|
|
23
|
+
_Reason the same defect in the other search: the docstring says it is bounded and it is not, so init in a fresh project reported the global skill as already there and set that project up with none_
|
|
24
|
+
- [x] 2.3 (Unit) Bound a path the marker sets to the workspace it is in — R4.4, R4.5
|
|
25
|
+
_Reason security review: a hostile marker could point assets at a UNC path, and stat on a share authenticates, so status alone leaked a credential_
|