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.
- slicelab-0.0.1/.editorconfig +28 -0
- slicelab-0.0.1/.github/dependabot.yml +24 -0
- slicelab-0.0.1/.github/workflows/ci.yml +81 -0
- slicelab-0.0.1/.github/workflows/engine.yml +161 -0
- slicelab-0.0.1/.github/workflows/release.yml +149 -0
- slicelab-0.0.1/.gitignore +42 -0
- slicelab-0.0.1/.pre-commit-config.yaml +22 -0
- slicelab-0.0.1/AGENTS.md +178 -0
- slicelab-0.0.1/CHANGELOG.md +153 -0
- slicelab-0.0.1/CONTRIBUTING.md +105 -0
- slicelab-0.0.1/LICENSE +216 -0
- slicelab-0.0.1/PKG-INFO +184 -0
- slicelab-0.0.1/README.md +161 -0
- slicelab-0.0.1/SECURITY.md +149 -0
- slicelab-0.0.1/docs/DECISIONS.md +602 -0
- slicelab-0.0.1/docs/RESEARCH.md +95 -0
- slicelab-0.0.1/justfile +65 -0
- slicelab-0.0.1/notes/README.md +40 -0
- slicelab-0.0.1/notes/critique.md +224 -0
- slicelab-0.0.1/notes/evidence.md +256 -0
- slicelab-0.0.1/notes/refuted.md +359 -0
- slicelab-0.0.1/pyproject.toml +84 -0
- slicelab-0.0.1/slicelab/__init__.py +11 -0
- slicelab-0.0.1/slicelab/__main__.py +7 -0
- slicelab-0.0.1/slicelab/adapters/__init__.py +29 -0
- slicelab-0.0.1/slicelab/adapters/base.py +68 -0
- slicelab-0.0.1/slicelab/adapters/orcaslicer.py +22 -0
- slicelab-0.0.1/slicelab/adapters/prusaslicer.py +22 -0
- slicelab-0.0.1/slicelab/cli.py +243 -0
- slicelab-0.0.1/slicelab/engine/__init__.py +8 -0
- slicelab-0.0.1/slicelab/engine/discover.py +197 -0
- slicelab-0.0.1/slicelab/engine/flatpak.py +95 -0
- slicelab-0.0.1/slicelab/engine/identity.py +142 -0
- slicelab-0.0.1/slicelab/engine/launch.py +241 -0
- slicelab-0.0.1/slicelab/presets.py +101 -0
- slicelab-0.0.1/slicelab/py.typed +0 -0
- slicelab-0.0.1/slicelab/report.py +32 -0
- slicelab-0.0.1/slicelab/status.py +206 -0
- slicelab-0.0.1/tests/__init__.py +0 -0
- slicelab-0.0.1/tests/conftest.py +81 -0
- slicelab-0.0.1/tests/test_boundaries.py +447 -0
- slicelab-0.0.1/tests/test_discovery.py +225 -0
- slicelab-0.0.1/tests/test_exit_codes.py +122 -0
- slicelab-0.0.1/tests/test_no_engine_data.py +127 -0
- slicelab-0.0.1/tests/test_presets.py +206 -0
- slicelab-0.0.1/tests/test_public_surface.py +49 -0
- slicelab-0.0.1/tests/test_report.py +44 -0
- slicelab-0.0.1/tests/test_status_total.py +45 -0
- slicelab-0.0.1/tests/test_usage_exit_64.py +73 -0
- 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
|
slicelab-0.0.1/AGENTS.md
ADDED
|
@@ -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).
|