Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ jobs:
- 'scripts/trigger-komodo-rollout.mjs'
- 'scripts/prepare-forum-rollout.mjs'
- 'scripts/schema-upgrade-*.mjs'
- 'scripts/host-release*.mjs'
- 'scripts/verify-memory-backup*.mjs'
- 'scripts/sync-komodo-config.mjs'
- 'scripts/test-distributed-realtime.mjs'
Expand All @@ -204,6 +205,7 @@ jobs:
- 'scripts/trigger-komodo-rollout.mjs'
- 'scripts/prepare-forum-rollout.mjs'
- 'scripts/schema-upgrade-*.mjs'
- 'scripts/host-release*.mjs'
- 'scripts/verify-memory-backup*.mjs'
- 'scripts/sync-komodo-config.mjs'
- 'deploy/**'
Expand Down Expand Up @@ -315,7 +317,9 @@ jobs:
working-directory: third_party/open-notebook
run: uv run --frozen --python 3.12 pytest -q
- if: matrix.component == 'deploy-contract'
run: node --test scripts/production-rag-contract.test.mjs scripts/release-smoke.test.mjs scripts/schema-upgrade-maintenance.test.mjs scripts/schema-upgrade-host-source.test.mjs
run: node --test scripts/production-rag-contract.test.mjs scripts/release-smoke.test.mjs scripts/schema-upgrade-maintenance.test.mjs scripts/schema-upgrade-host-source.test.mjs scripts/host-release.test.mjs scripts/host-release-transport.test.mjs scripts/host-release-komodo.test.mjs
- if: matrix.component == 'deploy-contract'
run: node --test --test-name-pattern 'effective candidate' scripts/host-release.local.test.mjs
- if: matrix.component == 'release'
run: npm run version:check

Expand Down Expand Up @@ -532,6 +536,6 @@ jobs:
KOMODO_API_SECRET: ${{ secrets.KOMODO_API_SECRET }}
steps:
- uses: actions/checkout@v5
with: { ref: main }
with: { ref: '${{ needs.update-manifests.outputs.commit-sha }}' }
- name: Trigger Komodo rollout
run: node scripts/trigger-komodo-rollout.mjs
11 changes: 11 additions & 0 deletions docs/evidence/issue-229-canvas-trim/FAILURE-MODEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# #229 glibc Canvas trim: failure model before implementation

Scope is the **server production dependency stage** of the frozen `d1075e35` candidate, on `linux/amd64` Node 22 Bookworm/glibc. The SPA build stage and its dependencies stay complete. This is a separate draft; it does not change the published image or production.

1. **Wrong platform or changed package.** A future musl base, non-x64 image, changed `@napi-rs/canvas` version, loader or native binaries must stop the build before deletion. Check `process.platform`, `process.arch`, glibc runtime, package versions, directory layout and the audited file SHA-256 values.
2. **Active GNU binary is accidentally removed or cannot load.** Delete only `@napi-rs/canvas-linux-x64-musl/skia.linux-x64-musl.node`; retain both package manifests, README files, the parent `@napi-rs/canvas` LICENSE, and the GNU binary. In separate Node processes before/after deletion, load Canvas, inspect which `.node` file was selected, and draw an actual PNG.
3. **PDF.js needs native Canvas through a dynamic path.** Run the packaged LingxiOS document-worker on the unchanged PDF fixture with the musl binary absent, compare exact output hash with the unmodified image, and retain the PDF.js parser, worker, CMaps, standard fonts and WASM.
4. **A future consumer requires musl portability or another libc.** The trim is fixed to the current glibc-only final runtime stage and guarded by the audited loader hash. Moving to musl or expanding image platforms requires a new audit. A build that cannot prove this condition fails.
5. **The apparent 30 MB gain is mistaken for whole-image savings.** Record regular bytes and reproducible single-file gzip only as an upper-level proxy; actual candidate OCI layer and full-image deltas require a completed build. Hardlinks and tar compression cannot be inferred from raw file size.

Stop this round after either this single bounded candidate has a passing isolated short check and draft PR, or its checks fail. Other large packages remain documented candidates rather than speculative deletions.
48 changes: 48 additions & 0 deletions docs/evidence/issue-229-canvas-trim/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# #229 bounded remaining-slimming review (2026-10-20 UTC)

This is a separate draft based on the coordinator's frozen product source `d1075e35e16721979b484c2d9e33eeb56468ce9d`, not a deployment. Its immutable `linux/amd64` image index is `sha256:697d5a3e3d34629e603c120cd068abde3a2add611f583d5444d77a947887bf69`. The [final OCI receipt](../release-215-216-219/image-final/verified-oci/receipt.json), [verified merged rootfs inventory](../release-215-216-219/image-final/verified-oci/rootfs-inventory.json) and [path analysis](../release-215-216-219/image-final/analysis/pr236-d107-rootfs-analysis.json) are inputs. The inventory file's locally rechecked SHA-256 is `3d537b85a17d2dd22790264678a83ce64e147f2f0192f4e2bfda40d666f8c96a`. No image layer was downloaded for this follow-up; the previously pulled historical image was used only for focused native/PDF checks. The final image is **published but not deployed** in the coordinator's current receipt.

## Largest ten production dependency packages and consumers

These are the ten largest **non-overlapping top-level npm package directories**, ranked by regular-file bytes in the final image. Together they contain 176,256,490 of the production dependency tree's 243,634,898 regular bytes (72.34%). Figures are uncompressed content, not OCI transfer bytes. Node itself is a separate 124,827,920-byte binary, and the SPA `/app/dist` is 66,290,700 regular bytes; neither is double-counted in this package ranking.

| Rank | Package | Regular bytes | Actual consumer / deletion decision |
| ---: | --- | ---: | --- |
| 1 | `@napi-rs/canvas-linux-x64-gnu` | 33,975,707 | PDF.js legacy Node loader calls `@napi-rs/canvas`; on Bookworm/glibc it loads the GNU binding. The native 33,974,784-byte file is active; retain. |
| 2 | `@napi-rs/canvas-linux-x64-musl` | 30,316,535 | Same loader selects this branch only on musl. Final target is Bookworm/glibc x64; one 30,315,608-byte native file is the bounded candidate below. Keep package metadata; trim only the binary after a platform, version, loader and hash check. |
| 3 | `pdfjs-dist` | 24,272,468 | Formal LingxiOS 3.5.8 document-worker imports `legacy/build/pdf.mjs` and resolves CMaps/fonts/WASM. The prior audited viewer trim is already present; parser/worker/resources remain. Some active legacy maps need a separate diagnostics policy, so this pass retains them. |
| 4 | `js-tiktoken` | 22,432,828 | Openlit's `dist/helpers.js` requires it and calls `encodingForModel` with a fallback. Its CJS/ESM files and model data appear duplicated, but package conditional exports and other model families have not been qualified for per-file deletion. |
| 5 | `openai` | 12,468,421 | `server/src/llm-client.ts` constructs the shared provider SDK client. Removing it would remove current model calls. |
| 6 | `@opentelemetry/semantic-conventions` | 12,006,437 | Openlit's `dist/manual-trace.js` imports it; product initializes Openlit when an OTLP endpoint is configured. Retain telemetry semantics and current sensitive-content settings. |
| 7 | `@opentelemetry/exporter-metrics-otlp-http` | 11,971,470 | Openlit's metrics module constructs the OTLP metrics exporter; removing it changes configured observability. |
| 8 | `@esbuild/linux-x64` | 11,428,465 | `npm` starts Web/Worker/migrate through `tsx`, whose runtime transform imports `esbuild` and selects this binary. `esbuild/bin/esbuild` is a hardlink to this file, not another 11.4 MB copy. |
| 9 | `@opentelemetry/otlp-transformer` | 9,638,682 | The OTLP exporter uses its serializer. The lock contains some nested OpenTelemetry versions; replacing them or deleting alternate formats requires API/version and live telemetry qualification. |
| 10 | `@dicebear/styles` | 7,745,477 | Server `profile-avatar.ts` imports `marbles.json` and `planets.json` for generated user/course avatars. Other styles may be another small package-specific candidate, but not a proved safe whole-package removal. |

The final compressed OCI layer budget is 96,721,079 bytes of base/runtime-APT layers, 70,347,423 bytes of production dependencies, and 37,786,863 bytes of SPA dist. These three disjoint areas already total **204,855,365 of 205,959,232 bytes (99.46%)**. Node, Python, bubblewrap/tini, GNU Canvas, the TypeScript launcher, PDF parsing, provider SDK, telemetry when configured, and the full SPA have present consumers. Source-only trimming cannot produce a large whole-image percentage. The remaining 6,050 dependency source maps (43,257,857 raw bytes), test/fixture-path candidates, old avatars and alternate package formats are *not* collectively proven removable; the categories overlap and cannot be summed as savings.

## One proved platform-only trim and bounded verification

The final OCI inventory reports the exact same `@napi-rs/canvas@1.0.8` loader and GNU/musl binary SHA-256 values as the previously pulled, immutable `ac906946` image. A no-network scan of 18,009 code/JSON/TS files in that exact historical image found five references to the musl package: two lock files, two package manifests and the Canvas loader, with no product code caller. Frozen d107 product source also has no direct musl-package import. The loader's Linux x64 branch checks libc and loads **GNU** on glibc; the final Dockerfile's dependency and runtime stages use Node 22 Bookworm slim. The new script deletes only `node_modules/@napi-rs/canvas-linux-x64-musl/skia.linux-x64-musl.node` in the **server production dependency stage**. It aborts before deletion on another platform, libc, version, layout or hash. The package manifests, README files, parent Canvas `LICENSE`, GNU binary, PDF.js and SPA stage are untouched. The final-image packaged check also requires a working GNU Canvas render and confirms the musl binary is absent. [Failure model](FAILURE-MODEL.md) was written before the source change.

Measured candidate file: **30,315,608 regular bytes**, SHA-256 `00790d510c990e1972f33d3c587d3d9faccddd4bf49c2bbe1938a05c40844034`; fixed single-file `gzip -n -9` is **12,496,273 bytes**. This gzip number is a rough compressed-size proxy, **not** the new dependency layer or whole-image delta. A full candidate image has not been built. The unchanged old image's Canvas loader hash, GNU/musl binary hashes and versions match the final OCI inventory exactly; it is a native-library surrogate, not an exact final-image business test.

Checks already run without network or production access:

- Exact historical image Node 22 dry run selected GNU and reported the one target file. A task-owned package copy with a deliberately changed musl version caused `--apply` to exit 1 while the binary remained. Restoring the version and running `--apply` against that copy removed the one binary; package metadata and GNU remained.
- With the pruned package copy read-only-mounted into a separate container, Canvas drew a PNG and `require.cache` contained only the GNU `.node`. The real LingxiOS document-worker processed `e2e/homework/triangle-reference.pdf` before and after: identical SHA-256 `30ee46cdb0878604ece1e00fab7a9807f3db64ff443a48e6dc2fe4a5646a5e04` (201 output bytes, as in the prior PDF audit).
- `node --check` for both changed scripts and `git diff --check` passed. A local two-file source test was **8 pass, 1 fail** because this checkout's installed LingxiOS is stale **3.5.2** while frozen source/lock requires **3.5.8**; the failed import is `consumePublicTextAttempts`, unrelated to Canvas. The frozen d107 upstream CI passed before this draft, but it does not certify this change. Exact-branch CI with a clean dependency install is required.

## Size limit and stop condition

If the isolated 12,496,273-byte gzip proxy translated one-for-one into an OCI-layer reduction, the whole image would be about **193,462,959 bytes**, or **6.47% below** the historical 206,838,733-byte image. This is an illustration, not a candidate OCI measurement or guarantee. The retained base/APT plus SPA layers alone are **134,507,942 compressed bytes**. A hypothetical 32% whole-image reduction from the historical image would target 140,650,338 bytes, leaving only 6,142,396 bytes for **all** runtime dependencies if those two areas stayed fixed; the current dependency layer is 70,347,423 bytes and contains several proven runtime consumers. Thus the prior ~32% *source COPY* reduction cannot be extended into a safe 32% whole-image claim through small dependency deletions.

This round stops with the one pinned, functional platform-only candidate. Further savings from source maps, telemetry versions, tokenizer formats, avatars or SPA assets need their own consumer and regression audits; a base/runtime or SPA redesign is a separate product change. No source, license, isolation support, diagnostic map or other runtime resource is removed to chase a percentage. #229 still requires controlled build/disk measurements and final configured acceptance; this draft does not close it.

## Feasible cold/warm build and peak-disk measurement, not run here

Use a dedicated ephemeral **linux/amd64** nonproduction builder with `overlay2`, at least **80 GiB free before starting**, a task-owned scratch filesystem, and no unrelated Docker workloads. The current executor has about 20 GB free and Docker `vfs`; no full candidate build is attempted here. Before either run, record `df -B1`, Docker root path/driver, Buildx version, frozen source SHA, image platform, build args, base-image digest, npm lock hashes, and the APT/npm repository or snapshot identities. Hold all those inputs fixed; if the APT feed cannot be frozen, label the result accordingly.

Create a dedicated empty BuildKit builder. Build the frozen source once into a task-owned OCI archive (cold), then repeat the identical command with the **same builder/cache** into a second archive (warm), using the existing npm secret mount without printing its value. Capture monotonic wall time, `--progress plain` cache-hit lines, archive digest/size, and exit status for each. Sample `df -B1` on the Docker-root/scratch filesystem every second from before build start through export completion; peak disk is the largest increase in used bytes over the exclusive runner's baseline. Also record `docker system df --verbose` before/after for attribution, but do not substitute that endpoint value for the sampled peak. Abort only the task-owned build if free space falls below 20 GiB; retain logs and mark the pair incomplete. Repeat cold/warm pairs on clean ephemeral runners to report spread rather than treating a single CI `build/push` step duration as a controlled comparison. Existing 328-second publication step and 1.68 GB root dev-install observation do not measure this pair or its disk peak.

No production cleanup, service stop, data/backup deletion, image rollback, merge, deploy or issue closure was performed.
Loading
Loading