slicelab 0.0.1__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.
Files changed (50) hide show
  1. slicelab-0.0.1/.editorconfig +28 -0
  2. slicelab-0.0.1/.github/dependabot.yml +24 -0
  3. slicelab-0.0.1/.github/workflows/ci.yml +81 -0
  4. slicelab-0.0.1/.github/workflows/engine.yml +161 -0
  5. slicelab-0.0.1/.github/workflows/release.yml +149 -0
  6. slicelab-0.0.1/.gitignore +42 -0
  7. slicelab-0.0.1/.pre-commit-config.yaml +22 -0
  8. slicelab-0.0.1/AGENTS.md +178 -0
  9. slicelab-0.0.1/CHANGELOG.md +153 -0
  10. slicelab-0.0.1/CONTRIBUTING.md +105 -0
  11. slicelab-0.0.1/LICENSE +216 -0
  12. slicelab-0.0.1/PKG-INFO +184 -0
  13. slicelab-0.0.1/README.md +161 -0
  14. slicelab-0.0.1/SECURITY.md +149 -0
  15. slicelab-0.0.1/docs/DECISIONS.md +602 -0
  16. slicelab-0.0.1/docs/RESEARCH.md +95 -0
  17. slicelab-0.0.1/justfile +65 -0
  18. slicelab-0.0.1/notes/README.md +40 -0
  19. slicelab-0.0.1/notes/critique.md +224 -0
  20. slicelab-0.0.1/notes/evidence.md +256 -0
  21. slicelab-0.0.1/notes/refuted.md +359 -0
  22. slicelab-0.0.1/pyproject.toml +84 -0
  23. slicelab-0.0.1/slicelab/__init__.py +11 -0
  24. slicelab-0.0.1/slicelab/__main__.py +7 -0
  25. slicelab-0.0.1/slicelab/adapters/__init__.py +29 -0
  26. slicelab-0.0.1/slicelab/adapters/base.py +68 -0
  27. slicelab-0.0.1/slicelab/adapters/orcaslicer.py +22 -0
  28. slicelab-0.0.1/slicelab/adapters/prusaslicer.py +22 -0
  29. slicelab-0.0.1/slicelab/cli.py +243 -0
  30. slicelab-0.0.1/slicelab/engine/__init__.py +8 -0
  31. slicelab-0.0.1/slicelab/engine/discover.py +197 -0
  32. slicelab-0.0.1/slicelab/engine/flatpak.py +95 -0
  33. slicelab-0.0.1/slicelab/engine/identity.py +142 -0
  34. slicelab-0.0.1/slicelab/engine/launch.py +241 -0
  35. slicelab-0.0.1/slicelab/presets.py +101 -0
  36. slicelab-0.0.1/slicelab/py.typed +0 -0
  37. slicelab-0.0.1/slicelab/report.py +32 -0
  38. slicelab-0.0.1/slicelab/status.py +206 -0
  39. slicelab-0.0.1/tests/__init__.py +0 -0
  40. slicelab-0.0.1/tests/conftest.py +81 -0
  41. slicelab-0.0.1/tests/test_boundaries.py +447 -0
  42. slicelab-0.0.1/tests/test_discovery.py +225 -0
  43. slicelab-0.0.1/tests/test_exit_codes.py +122 -0
  44. slicelab-0.0.1/tests/test_no_engine_data.py +127 -0
  45. slicelab-0.0.1/tests/test_presets.py +206 -0
  46. slicelab-0.0.1/tests/test_public_surface.py +49 -0
  47. slicelab-0.0.1/tests/test_report.py +44 -0
  48. slicelab-0.0.1/tests/test_status_total.py +45 -0
  49. slicelab-0.0.1/tests/test_usage_exit_64.py +73 -0
  50. slicelab-0.0.1/uv.lock +380 -0
@@ -0,0 +1,28 @@
1
+ # EditorConfig — consistent formatting across editors
2
+ # https://editorconfig.org
3
+
4
+ root = true
5
+
6
+ [*]
7
+ charset = utf-8
8
+ end_of_line = lf
9
+ insert_final_newline = true
10
+ trim_trailing_whitespace = true
11
+ indent_style = space
12
+ indent_size = 2
13
+
14
+ [*.{py,rs}]
15
+ indent_size = 4
16
+
17
+ [*.go]
18
+ indent_style = tab
19
+
20
+ [*.md]
21
+ trim_trailing_whitespace = false
22
+
23
+ [Makefile]
24
+ indent_style = tab
25
+
26
+ [justfile]
27
+ indent_style = space
28
+ indent_size = 4
@@ -0,0 +1,24 @@
1
+ version: 2
2
+ updates:
3
+ # The pins live in pyproject.toml's [dependency-groups] and uv.lock. slicelab
4
+ # has no runtime dependencies by design (docs/DECISIONS.md, and org AGENTS.md
5
+ # section 10 makes adding one an escalation), so everything here is dev tooling.
6
+ - package-ecosystem: uv
7
+ directory: /
8
+ schedule:
9
+ interval: weekly
10
+ commit-message:
11
+ prefix: build
12
+ groups:
13
+ python:
14
+ patterns: ["*"]
15
+
16
+ - package-ecosystem: github-actions
17
+ directory: /
18
+ schedule:
19
+ interval: weekly
20
+ commit-message:
21
+ prefix: ci
22
+ groups:
23
+ actions:
24
+ patterns: ["*"]
@@ -0,0 +1,81 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ # No branch filter: a stacked pull request is based on its parent branch, not
7
+ # on main, and a base change fires `edited`, which is not a default activity
8
+ # type -- so filtering on main means a retargeted PR gets no checks at all.
9
+ pull_request:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: ci-${{ github.ref }}
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ check:
20
+ name: Check
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 10
23
+ steps:
24
+ - uses: actions/checkout@v7
25
+ - uses: extractions/setup-just@v4
26
+ - uses: astral-sh/setup-uv@v7
27
+ with:
28
+ enable-cache: true
29
+ - run: just setup
30
+ - run: just check
31
+
32
+ test:
33
+ name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }})
34
+ runs-on: ${{ matrix.os }}
35
+ timeout-minutes: 10
36
+ strategy:
37
+ fail-fast: false
38
+ matrix:
39
+ python-version: ["3.11", "3.12", "3.13"]
40
+ os: [ubuntu-latest]
41
+ include:
42
+ - python-version: "3.11"
43
+ os: windows-latest
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+ - uses: extractions/setup-just@v4
47
+ - uses: astral-sh/setup-uv@v7
48
+ with:
49
+ enable-cache: true
50
+ python-version: ${{ matrix.python-version }}
51
+ - run: just setup
52
+ - run: just test
53
+
54
+ # The engine matrix is engine.yml, not a job here: it installs a real slicer on
55
+ # four host shapes and is scheduled rather than run per pull request. It sets
56
+ # SLICELAB_REQUIRE_ENGINE=1, so a failed engine install is loud rather than a
57
+ # silent skip. The jobs below run on hosts with no engine, where the
58
+ # engine-dependent tests skip and `-ra` prints how many were withheld.
59
+
60
+ ok:
61
+ name: ok
62
+ if: always()
63
+ needs: [check, test]
64
+ runs-on: ubuntu-latest
65
+ timeout-minutes: 5
66
+ steps:
67
+ # Requires SUCCESS rather than testing for failure. `contains(needs.*.result,
68
+ # 'failure')` does not match 'skipped', so an `if: false` on `check` or `test`
69
+ # produced a fully green pull request with nothing checked -- measured in a
70
+ # sibling repo, where that is exactly what happened.
71
+ #
72
+ # The tolerant form is right for a workflow with path filtering, where jobs
73
+ # skip legitimately on a docs-only change. This one has none, so a skipped job
74
+ # here is always a defect.
75
+ - name: Fail unless every upstream job succeeded
76
+ if: >-
77
+ needs.check.result != 'success' ||
78
+ needs.test.result != 'success'
79
+ run: exit 1
80
+ - name: Pass
81
+ run: echo 'All checks passed'
@@ -0,0 +1,161 @@
1
+ name: Engine
2
+
3
+ # The real engines, across the ways people actually install them.
4
+ #
5
+ # ci.yml runs the suite everywhere and SKIPS the engine tests, because no
6
+ # standard runner has a slicer. That leaves discovery -- the thing `which`
7
+ # exists to do -- covered by fake launchers alone. Fakes are exactly what would
8
+ # hide the finding this verb was written for: PrusaSlicer's Flathub entrypoint
9
+ # backgrounds its child and therefore returns 0 for every invocation, so its
10
+ # exit status carries no information (notes/evidence.md V10).
11
+ #
12
+ # Every job sets SLICELAB_REQUIRE_ENGINE=1, so a failed install is a LOUD
13
+ # failure rather than a green run full of skips. A job that could not get an
14
+ # engine must not look like a job that tested one.
15
+ #
16
+ # THIS MATRIX IS ALSO A CONTROL. Every empirical finding behind this project
17
+ # was taken on one Linux host against two Flatpaks. A Flatpak is a materially
18
+ # different execution environment -- sandboxed filesystem, its own /tmp,
19
+ # translated paths -- so behaviour observed only there is not established for
20
+ # native builds. The macOS and Windows jobs run the same code against native
21
+ # 2.9.6, which is the only control available: PrusaSlicer publishes no Linux
22
+ # asset on its GitHub releases (checked across 2.9.6 and the four before it --
23
+ # only setup.exe, .dmg and a Windows .zip), and Debian/Ubuntu ship 2.9.2, a
24
+ # different version.
25
+ #
26
+ # Not on every push: these install large GUI applications and are slower and
27
+ # more fragile than the gate. On demand, and weekly so upstream packaging
28
+ # changes surface without anyone remembering to look.
29
+
30
+ on:
31
+ workflow_dispatch:
32
+ schedule:
33
+ - cron: "0 6 * * 1" # Mondays, 06:00 UTC
34
+
35
+ permissions:
36
+ contents: read
37
+
38
+ concurrency:
39
+ group: engine-${{ github.ref }}
40
+ cancel-in-progress: true
41
+
42
+ env:
43
+ SLICELAB_REQUIRE_ENGINE: "1"
44
+
45
+ jobs:
46
+ flatpak:
47
+ name: Linux / Flatpak (Flathub, both engines)
48
+ runs-on: ubuntu-latest
49
+ timeout-minutes: 60
50
+ steps:
51
+ - uses: actions/checkout@v7
52
+ - uses: extractions/setup-just@v4
53
+ - uses: astral-sh/setup-uv@v7
54
+ with:
55
+ enable-cache: true
56
+ - name: Install both engines from Flathub
57
+ run: |
58
+ set -euo pipefail
59
+ sudo apt-get update
60
+ sudo apt-get install -y flatpak
61
+ sudo flatpak remote-add --if-not-exists flathub \
62
+ https://dl.flathub.org/repo/flathub.flatpakrepo
63
+ sudo flatpak install -y --noninteractive flathub com.prusa3d.PrusaSlicer
64
+ sudo flatpak install -y --noninteractive flathub com.orcaslicer.OrcaSlicer
65
+ - name: Show what was installed
66
+ run: flatpak list --app
67
+ - run: just setup
68
+ - run: just test
69
+ - name: Report what discovery chose, and why
70
+ # The interesting output of this whole workflow. On the development
71
+ # host PrusaSlicer's default entrypoint is REJECTED for returning 0 and
72
+ # the --command= bypass wins, while OrcaSlicer keeps its entrypoint
73
+ # (which carries the packager's LC_NUMERIC=C fix). If a Flathub update
74
+ # ever changes either, this is where it surfaces.
75
+ run: uv run slicelab which
76
+
77
+ linux-apt:
78
+ name: Linux / distro package on PATH
79
+ runs-on: ubuntu-latest
80
+ timeout-minutes: 45
81
+ steps:
82
+ - uses: actions/checkout@v7
83
+ - uses: extractions/setup-just@v4
84
+ - uses: astral-sh/setup-uv@v7
85
+ with:
86
+ enable-cache: true
87
+ - name: Install PrusaSlicer from the distro archive
88
+ run: |
89
+ set -euo pipefail
90
+ sudo apt-get update
91
+ sudo apt-get install -y prusa-slicer
92
+ command -v prusa-slicer
93
+ - run: just setup
94
+ - run: just test
95
+ - name: Report what discovery chose, and why
96
+ # Plain PATH discovery, no sandbox. Note this is a DIFFERENT VERSION
97
+ # from the Flatpak jobs -- Ubuntu ships an older PrusaSlicer -- so it
98
+ # controls for the install mode, not for the version.
99
+ run: uv run slicelab which prusaslicer
100
+
101
+ macos:
102
+ name: macOS / Homebrew cask (.app bundle)
103
+ runs-on: macos-latest
104
+ timeout-minutes: 45
105
+ steps:
106
+ - uses: actions/checkout@v7
107
+ - uses: extractions/setup-just@v4
108
+ - uses: astral-sh/setup-uv@v7
109
+ with:
110
+ enable-cache: true
111
+ - name: Install PrusaSlicer
112
+ run: |
113
+ set -euo pipefail
114
+ brew install --cask prusaslicer
115
+ ls -d /Applications/*rusa* || true
116
+ - name: Put the bundle's binary on PATH, and record what it is called
117
+ # A macOS .app is a directory, so installing the cask puts nothing on
118
+ # PATH. The binary inside carries the APPLICATION's name, not the
119
+ # command's -- discovery looking only for the POSIX name reported the
120
+ # engine absent on a runner that had just installed it. The listing is
121
+ # not decoration: the bundle's executable name is a packaging decision
122
+ # that can change, and this is where we would see it change.
123
+ run: |
124
+ set -euo pipefail
125
+ bundle=$(ls -d /Applications/*rusa*.app | head -1)
126
+ echo "executables in $bundle/Contents/MacOS:"
127
+ ls -l "$bundle/Contents/MacOS"
128
+ echo "$bundle/Contents/MacOS" >> "$GITHUB_PATH"
129
+ - run: just setup
130
+ - run: just test
131
+ - name: Report what discovery chose, and why
132
+ run: uv run slicelab which prusaslicer
133
+
134
+ windows:
135
+ name: Windows / official portable zip
136
+ runs-on: windows-latest
137
+ timeout-minutes: 45
138
+ steps:
139
+ - uses: actions/checkout@v7
140
+ - uses: extractions/setup-just@v4
141
+ - uses: astral-sh/setup-uv@v7
142
+ with:
143
+ enable-cache: true
144
+ - name: Install PrusaSlicer from the official portable zip
145
+ # The one native build of the EXACT version this project's findings
146
+ # were taken against (2.9.6). Windows is where the Flatpak-versus-
147
+ # native control actually lands.
148
+ shell: pwsh
149
+ run: |
150
+ $ErrorActionPreference = "Stop"
151
+ $url = "https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.9.6/PrusaSlicer-2.9.6.zip"
152
+ Invoke-WebRequest -Uri $url -OutFile prusaslicer.zip
153
+ Expand-Archive prusaslicer.zip -DestinationPath $env:RUNNER_TEMP\prusaslicer
154
+ $exe = Get-ChildItem -Recurse -Filter prusa-slicer-console.exe $env:RUNNER_TEMP\prusaslicer |
155
+ Select-Object -First 1
156
+ if (-not $exe) { throw "prusa-slicer-console.exe not found in the portable zip" }
157
+ Add-Content -Path $env:GITHUB_PATH -Value $exe.Directory.FullName
158
+ - run: just setup
159
+ - run: just test
160
+ - name: Report what discovery chose, and why
161
+ run: uv run slicelab which prusaslicer
@@ -0,0 +1,149 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI via Trusted Publishing (OIDC). No API token is stored.
4
+ #
5
+ # The publisher on PyPI is pinned to heibench/slicelab, this workflow filename,
6
+ # and the `pypi` environment -- all three must match or the publish is refused.
7
+ #
8
+ # This workflow is INERT until a release is published. It exists now because the
9
+ # PyPI pending publisher binds to a workflow filename, so the file must be on
10
+ # main before the name can be reserved.
11
+
12
+ on:
13
+ release:
14
+ types: [published]
15
+ workflow_dispatch:
16
+ inputs:
17
+ expected_version:
18
+ description: "Exact version the dist must carry (e.g. 0.1.0) — guards against publishing the wrong ref"
19
+ required: true
20
+ type: string
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ build:
27
+ name: Build and verify
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 15
30
+ steps:
31
+ - uses: actions/checkout@v7
32
+ - uses: extractions/setup-just@v4
33
+ - uses: astral-sh/setup-uv@v7
34
+ with:
35
+ enable-cache: true
36
+
37
+ - name: Gate on the same checks a pull request runs
38
+ run: |
39
+ just setup
40
+ just check
41
+ just test
42
+
43
+ - name: Build sdist and wheel
44
+ run: uv build
45
+
46
+ - name: Guard built version against the intended release
47
+ # A tag and a version are two statements of one fact; publishing when
48
+ # they disagree ships something nobody asked for under a name someone
49
+ # will trust.
50
+ run: |
51
+ set -eu
52
+ version=$(ls dist/*.tar.gz | sed -E 's|.*/slicelab-(.+)\.tar\.gz|\1|')
53
+ echo "built: $version"
54
+ case "${{ github.event_name }}" in
55
+ release) want="${GITHUB_REF_NAME#v}" ;;
56
+ workflow_dispatch) want="${{ inputs.expected_version }}" ;;
57
+ esac
58
+ if [ "$version" != "$want" ]; then
59
+ echo "::error::built version '$version' does not match intended release '$want'"
60
+ exit 1
61
+ fi
62
+ case "$version" in
63
+ *.dev*|*rc*|*a[0-9]*|*b[0-9]*)
64
+ echo "::error::refusing to publish pre-release version '$version'"
65
+ exit 1 ;;
66
+ esac
67
+
68
+ - name: Assert no distribution carries engine-derived bytes
69
+ # docs/DECISIONS.md D11. The repository half is tests/test_no_engine_data.py.
70
+ #
71
+ # This step used to inspect the wheel only. `packages = ["slicelab"]`
72
+ # means the wheel can only ever contain slicelab's own module, so that
73
+ # check was green over a stray it structurally could not see -- while
74
+ # the sdist, which takes everything git does not IGNORE, shipped a
75
+ # committed OrcaSlicer `result.json` from the repository root. A guard
76
+ # that cannot fail is not a guard. Both artifacts are read now.
77
+ run: |
78
+ set -eu
79
+ python3 - <<'PY'
80
+ import glob, sys, tarfile, zipfile
81
+
82
+ # The wheel is slicelab's own module and nothing else.
83
+ WHEEL_ALLOWED = (".py", ".typed", ".txt", ".md")
84
+ # The sdist is the working tree minus what git ignores, so it
85
+ # legitimately carries config and docs. What it must never carry is a
86
+ # format an engine writes -- the same vocabulary
87
+ # tests/test_no_engine_data.py uses, including `.log` for the
88
+ # `00000.log` that D20 and V13 are actually written about.
89
+ ENGINE_WRITTEN = (
90
+ ".json", ".ini", ".gcode", ".bgcode", ".log",
91
+ ".3mf", ".stl", ".obj", ".amf", ".step",
92
+ )
93
+
94
+ bad = []
95
+
96
+ wheels = glob.glob("dist/*.whl")
97
+ sdists = glob.glob("dist/*.tar.gz")
98
+ if not wheels or not sdists:
99
+ # Silence must not read as success: no artifacts means this step
100
+ # examined nothing, which is not the same as finding nothing.
101
+ print(f"::error::expected a wheel and an sdist, got {wheels + sdists}")
102
+ sys.exit(1)
103
+
104
+ for whl in wheels:
105
+ for name in zipfile.ZipFile(whl).namelist():
106
+ if name.endswith("/") or ".dist-info/" in name:
107
+ continue
108
+ if not name.endswith(WHEEL_ALLOWED):
109
+ bad.append(f"{whl}:{name}")
110
+
111
+ for sd in sdists:
112
+ with tarfile.open(sd) as tf:
113
+ for m in tf.getmembers():
114
+ if not m.isfile():
115
+ continue
116
+ if m.name.lower().endswith(ENGINE_WRITTEN):
117
+ bad.append(f"{sd}:{m.name}")
118
+
119
+ if bad:
120
+ print("::error::distribution carries unexpected files (D11):")
121
+ for b in bad:
122
+ print(" " + b)
123
+ sys.exit(1)
124
+ print(f"checked {len(wheels)} wheel(s) and {len(sdists)} sdist(s): D11 holds")
125
+ PY
126
+
127
+ - name: Check the metadata PyPI will see
128
+ run: uv run --with twine twine check dist/*
129
+
130
+ - uses: actions/upload-artifact@v7
131
+ with:
132
+ name: dist
133
+ path: dist/
134
+
135
+ publish:
136
+ name: Publish to PyPI
137
+ needs: build
138
+ runs-on: ubuntu-latest
139
+ timeout-minutes: 15
140
+ environment: pypi
141
+ permissions:
142
+ id-token: write # PyPI Trusted Publishing (OIDC)
143
+ steps:
144
+ - uses: actions/download-artifact@v8
145
+ with:
146
+ name: dist
147
+ path: dist/
148
+ - name: Publish
149
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,42 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .Python
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling caches
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .mypy_cache/
15
+ .coverage
16
+ htmlcov/
17
+
18
+ # Engine-derived data is NEVER committed -- see docs/DECISIONS.md D11.
19
+ # slicelab ships zero engine-derived bytes; characterisation output is
20
+ # generated on the user's machine into XDG cache and stays there.
21
+ /characterisation/
22
+ *.characterisation.json
23
+
24
+ # Generated locks and slicer output from local experiments.
25
+ # A real project's slice.lock IS committed -- these are ours, from scratch runs.
26
+ /scratch/
27
+ *.gcode
28
+ *.bgcode
29
+ slice.lock
30
+
31
+ # Engine litter. OrcaSlicer writes into the process CWD: 00000.log on a failing
32
+ # run (D20, V13), and result.json on any run at all -- including a successful
33
+ # `which` probe. It is swept now, but the one that got committed was result.json
34
+ # and it was NOT listed here, so nothing objected. Both are named, and both are
35
+ # in the D11 vocabulary that tests/test_no_engine_data.py enforces.
36
+ 00000.log
37
+ result.json
38
+
39
+ # Editors / OS
40
+ .DS_Store
41
+ .idea/
42
+ .vscode/
@@ -0,0 +1,22 @@
1
+ repos:
2
+ - repo: https://github.com/gitleaks/gitleaks
3
+ rev: v8.27.2
4
+ hooks:
5
+ - id: gitleaks
6
+
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v5.0.0
9
+ hooks:
10
+ - id: trailing-whitespace
11
+ - id: end-of-file-fixer
12
+ - id: check-yaml
13
+ - id: check-toml
14
+ - id: check-merge-conflict
15
+ - id: check-added-large-files
16
+
17
+ - repo: https://github.com/astral-sh/ruff-pre-commit
18
+ rev: v0.11.12
19
+ hooks:
20
+ - id: ruff
21
+ args: [--fix]
22
+ - id: ruff-format
@@ -0,0 +1,178 @@
1
+ # AGENTS.md — slicelab
2
+
3
+ Instructions for humans and AI coding agents working in this repository.
4
+
5
+ The org-wide contract at <https://github.com/heibench/.github/blob/main/AGENTS.md>
6
+ is the floor. This file carries what is specific to slicelab; where the two
7
+ conflict, this file wins.
8
+
9
+ ## Project
10
+
11
+ `slicelab` is a **driver**, not a checker. It puts a Slic3r-descended slicer under
12
+ program control and records exactly what that engine resolved. It does not
13
+ adjudicate G-code — that is `slicespec`'s job, and it does not exist yet.
14
+
15
+ **One mechanism justifies the whole tool and nothing else does:** ask the engine
16
+ what it *actually resolved*, diff that against what was requested, per key, and
17
+ refuse to call the run green when they disagree.
18
+
19
+ Everything else — the lock file, the multi-engine story, a native backend —
20
+ is downstream of that one move. If the readback diff is ever weakened, delete
21
+ the project rather than ship it.
22
+
23
+ **Status: pre-alpha. Two verbs work: `slicelab which` and `slicelab presets`.**
24
+ `which` discovers an installed engine, probes whether that engine's exit status
25
+ can be believed, and reports its identity. `presets` enumerates an engine's
26
+ printer presets, adjudicated on the JSON rather than the exit code. Nothing
27
+ slices, reads a `slice.toml`, or writes a lock.
28
+
29
+ Treat this section as code: the moment another verb works, this paragraph is
30
+ false and the change that made it work is not finished until it is corrected
31
+ (org AGENTS.md 2.5). It has been corrected once when the exit map landed, once
32
+ when `which` did, and once when `presets` did — stated as causes rather than as
33
+ a tally, because the tally in `SECURITY.md` kept being the stale claim.
34
+
35
+ ## Start here
36
+
37
+ 1. `docs/DECISIONS.md` — numbered decisions, with the reasoning that produced each.
38
+ Do not relitigate a numbered decision; if it is wrong, add a superseding entry.
39
+ 2. `docs/RESEARCH.md` — what was empirically established, what was refuted, and
40
+ what remains unverified. Read the third list before planning anything.
41
+ 3. `notes/` — the frozen dossier the decisions cite. `notes/evidence.md` carries
42
+ the reproductions; issues and decisions cite them by tag.
43
+
44
+ ## Stack
45
+
46
+ - **Python >= 3.11** (`tomllib`), packaged with `hatchling`, `uv` against a
47
+ committed `uv.lock`.
48
+ - **No runtime dependencies.** `tomllib`, `hashlib`, `subprocess` and `argparse`
49
+ are stdlib. Adding one is org AGENTS.md section 10 — escalate, do not decide.
50
+ - **Tooling** — `ruff` (format + lint), `mypy` (types), `pytest`, `just`,
51
+ `pre-commit`.
52
+
53
+ ## Commands
54
+
55
+ ```sh
56
+ just setup # uv sync
57
+ just fmt # format + autofix
58
+ just check # fmt-check + lint + typecheck (CI-equivalent)
59
+ just test # run tests; engine tests skip when no slicer is installed
60
+ just test-engine # run tests and FAIL if no engine is installed
61
+ just hooks # every pre-commit hook over the whole tree
62
+ ```
63
+
64
+ Run `just check && just test` before every commit. Never `--no-verify`.
65
+
66
+ ## The rules that are not negotiable
67
+
68
+ Each is a decision with evidence behind it. The tag in brackets is the
69
+ reproduction in `notes/evidence.md`.
70
+
71
+ 1. **The readback diff is the mechanism; a compatibility flag is not** (D4).
72
+ Every authored override is compared against the engine's own resolved output.
73
+ PrusaSlicer silently coerces `--perimeters 4.7` to `4` at exit 0 with zero
74
+ bytes on stderr [V1], and silently drops an unknown key from a `--load`ed ini
75
+ the same way [V2]. `--config-compatibility=disable` catches neither [V3], and
76
+ does not exist on OrcaSlicer.
77
+ 2. **Capture is gated on the artifact; the artifact is staged then promoted**
78
+ (D7). `--save` runs before the slice block and is not conditioned on it: an
79
+ out-of-bounds object gives exit 0, no G-code, and a complete ini anyway [V4].
80
+ A run that did not write the file must never report as one that did.
81
+ 3. **The core vocabulary is EMPTY** (D2). Authored keys are the engine's own
82
+ native names under an engine-namespaced table. `CORE_KEYS == frozenset()`,
83
+ asserted by a test. A key enters only through D3's recorded admission
84
+ procedure. A vocabulary written from one engine *is* the PrusaSlicer-shaped
85
+ abstraction this project exists to avoid.
86
+ 4. **Never adjudicate on an engine's exit code without probing it first** (D17,
87
+ D18). `--query-printer-models` returns exit 1 with 6550 bytes of valid JSON —
88
+ the same code it returns for "not found" [V5]. And the Flatpak entrypoint
89
+ backgrounds its child, so *every* invocation returns 0 unless the
90
+ `--command=` bypass is used [V10]. That bypass is mandatory for PrusaSlicer
91
+ and harmful for OrcaSlicer. Probe per package; never copy it as a constant.
92
+ 5. **Reproducibility is a measured value, never an assumption** (D8). Three runs
93
+ of one part with `--fuzzy-skin all` gave three distinct normalized hashes and
94
+ three distinct filament-used values [V8]. Without the measurement, a hash
95
+ mismatch from fuzzy skin is indistinguishable from real drift and `verify`
96
+ reports *violated* — a false red, which is the silence rule inverted.
97
+ 6. **Ship zero engine-derived bytes** (D11). Engine help text, default key sets,
98
+ preset catalogues and option metadata are generated on the user's machine into
99
+ XDG cache and gitignored. The subprocess argument settles *linking* and says
100
+ nothing about *copying*.
101
+ 7. **No escape hatches** (org 2.1). No `--allow-coercion`, no
102
+ `--allow-engine-drift`, no `--force-lock`. Shipping the escape hatch alongside
103
+ the discipline means the discipline is never tested. `--report PATH` is the
104
+ answer: machine-readable, naming exactly which key diverged.
105
+
106
+ ## Outcomes and exit codes
107
+
108
+ Two levels, never one flat list — matching partspec's Status-per-check /
109
+ Verdict-per-part and netspec's D9/D26 split.
110
+
111
+ | Outcome | Exit | Meaning |
112
+ |---|---|---|
113
+ | `sliced` | 0 | Artifact exists, non-empty, **promoted by this run**, its config export captured in the *same invocation*, every requested key `applied`, `adapter.exact` true. |
114
+ | `refused` | 1 | slicelab established the intent was not honoured. The driver's analogue of *violated*: we looked, the answer is no. |
115
+ | `incomplete` | 2 | slicelab ran but cannot stand behind the result. **Never reachable from an unexamined success path.** |
116
+ | `empty` | 3 | The run verified nothing, because nothing was requested. Neither success nor a finding (D24). |
117
+ | `error` | 4 | Environment fault. Not a verdict on the intent. |
118
+ | — | 64 | Usage: slicelab's own argv. |
119
+
120
+ These are org contract **section 6.2**'s codes, which is now the settled org-wide
121
+ vocabulary: A1, A2 and A3 closed on 2026-09-06 and partspec, netspec and gerberdiff
122
+ all answer on the same five. slicelab conforms rather than chooses. Only the *words*
123
+ diverge, and deliberately — see D14.
124
+
125
+ **`empty` (3) is not `sliced`.** A `slice.toml` with a `[base]` triple and no
126
+ `[set]` keys produces a real artifact and verifies nothing — "every requested key
127
+ was applied" is vacuously true over zero keys. That run is `empty`, the artifact
128
+ is still promoted, and no lock is written. `3` is partspec's code for the same
129
+ idea and is **not** in §6.2's table; slicelab is the second member using it, and
130
+ that is escalated rather than assumed. See D24.
131
+
132
+ **`refused` outranks `incomplete`.** When one requested key is `coerced` and another
133
+ is `absent`, the run is `refused` (1), not `incomplete` (2). A finding about the
134
+ request stays a finding even when some other key could not be evaluated; the reverse
135
+ would let one unreadable key mask a real one. Taken from netspec D26, which settled
136
+ the same precedence for the verify layer — this is the driver's form of it, adopted
137
+ rather than re-derived.
138
+
139
+ The renderer prints the outcome word as the **first token** of output (D14). A
140
+ past-tense verb leading a non-zero run reads green to a human skimming CI logs.
141
+
142
+ ## Constraints
143
+
144
+ - **`readback.py` knows no engine's option names.** Engine-specific logic lives
145
+ under `adapters/<engine>/`. A structural test confirms it — scoped to the FFF
146
+ config-key namespace, not to every string constant.
147
+ - **The `sliced` outcome must be unreachable with an empty subject set.** With
148
+ zero requested keys, "every requested key was applied" is vacuously true, and
149
+ a base-only `slice.toml` is the first file anyone writes. See G1 in
150
+ `notes/critique.md`; this is the highest-priority correctness constraint in
151
+ the project.
152
+ - **Map option names to config keys by probing, never by string transform.**
153
+ 411 CLI options, 343 config keys, 70 options with no matching key.
154
+ `--after-layer-gcode` writes `layer_gcode`, so dash-to-underscore reports a
155
+ false `absent` on a perfect run. See G2 in `notes/critique.md`.
156
+ - **Do not reimplement the engine's arithmetic.** The premise is that the engine
157
+ knows what the slice is and we do not.
158
+ - **Do not write a test that reads a doc, reads the code, and diffs them**, and
159
+ do not assert that a phrase appears in prose. A doc test must assert something
160
+ executable.
161
+ - Always name an encoding when reading or writing engine output, and decode the
162
+ engine itself with `errors="replace"` (inherited from prusaslicer-py D5).
163
+ - Do not add AI attribution to commits or PR descriptions — no co-author
164
+ trailers, session links, or "generated with" footers.
165
+ - Do not name, link, or describe any private repository in public output.
166
+
167
+ ## Related
168
+
169
+ Siblings, deliberately non-overlapping:
170
+
171
+ - `prusaslicer-py` — a narrow, finished, single-engine PrusaSlicer driver. It is
172
+ **maintained separately and slicelab does not import it** (org section 5: the
173
+ stable surface is never the Python API). Two files' worth of Flatpak path
174
+ handling were copied with attribution and one fix; see D19.
175
+ - `partspec` — verifies CAD-as-code parts. `netspec` — PCB connectivity.
176
+ `gerberdiff` — fabrication output. `orlab` — the reference DRIVE-layer member.
177
+ - `slicespec` — does not exist. When it does, it reads `slice.lock` as a **file**
178
+ and never imports slicelab (D22).