Skip to content

fix(config): adopt the landed stage_parallelism vocabulary in the project manifest (#938) - #1084

Merged
github-actions[bot] merged 1 commit into
mainfrom
feat/stage-parallelism-938
Oct 28, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
feat/stage-parallelism-938

Conversation

@aitrawog

Copy link
Copy Markdown
Contributor

Closes #938

Summary

Head a4096c21ed51b27864c4b9f7680146572be67a12 — the commit this PR pushes: 2 files changed, 8 insertions(+), 3 deletions(-).

The defect. This repo's own charly.yml spelled the inner build tunables podman_jobs / podman_jobs_cap. Those keys are dead: the cutover that landed the knob as --stage-parallelism / CHARLY_STAGE_PARALLELISM retired the podman-era spellings, and a retired key under defaults: is dropped by the decode — silently, because the defaults: block is validated against an OPEN schema struct. So the manifest's declared inner-concurrency cap of 8 was never applied: every build resolved to the sdk's fallback of 4, and charly box validate reported 0 warnings, 0 errors the whole time. That is the exact shape opencharly/spec#241 names.

The RCA.

  • Mechanism — spec.Config.Defaults is a spec.BoxConfig, decoded with no strict-field check. The retired key is dropped on decode, so the value the operator declared never reaches the resolver (candy/plugin-build/resolve.go:246-255, resolveDriveStageParallelism) and the build silently asks for buildkit.ResolveStageParallelism(0, 0) — the sdk's fallback (sdk/buildkit/build_helpers.go:37-74). The schema half of the mechanism is an open/closed asymmetry: spec/schema/box.cue marks #Box CLOSED ("an unknown key is a typo"), but the defaults: block is validated against #BoxConfig, which is not closed — so a retired key under defaults: is accepted by validation and dropped by the decode, and no tool in the chain can see it.
  • Missed control — the contract's rename landed its controls on the SCHEMA (the key names in spec/schema/box.cue:120-121), the MIGRATION TABLE (plugin-migrate's rename_key ops) and the FLAG. Nothing connected the contract's rename to the CONSUMER's own manifest, and this repo's charly box validate cannot see it either: the retired key is not an unknown key to the validator, it is a dropped one, so the validator has nothing to reject.
  • Blast radius — this repo's own builds: the declared cap of 8 was never in effect; the org ran at 4. One manifest block, two keys.
  • Root fix — move this repo's manifest onto the landed vocabulary, and correct the comment that misstated the mechanism (it claimed "inner: stages per podman build"; the builder is BuildKit and the resolved value is the DAEMON's [worker.oci] max-parallelism, set at daemon start and host-global per uid).

What lands. Two keys and their comment:

where was is
charly.yml:56 podman_jobs: 0 stage_parallelism: 0
charly.yml:57 podman_jobs_cap: 8 stage_parallelism_cap: 4

Why the cap is 4, and not the 8 the manifest declared. This is the one decision in this PR, so it is measured rather than asserted:

  1. The retired key was DROPPED — so this repo's builds have been running at the sdk's fallback of 4 all along. 4 is what the org's builds run at today.
  2. The BuildKit daemon is host-global per uid ($XDG_CACHE_HOME/charly/buildkitd, socket /run/user/1000/charly/buildkitd.sock), not per project. The resolved value is the daemon's max-parallelism, and a differing value is a hard error (sdk/buildkitdrive/daemon.go:96-99) — measured, with the renamed key at 8: "the running daemon was configured with max-parallelism=4 but 8 was requested". Carrying 8 would therefore have failed every build on this host, and would have forced the value onto every other project on the uid.
  3. No charly verb stops that daemon (there is no verb for it — charly clean is an out-of-process plugin), so the remedy is not expressible with the binary. That is The BuildKit daemon has no supervisor: a changed stage_parallelism cannot be applied while it runs (a D-2 revision with a packaging leg) #919, and it is where the operator's next step already lives.

The cap is therefore 4: it is the value this repo already runs at, it is the sdk's conservative fallback, and it is the value the running daemon was started with. Raising it is a host-level decision, not a manifest edit — it is filed under Filed issues.

The comment is corrected with the keys. It now says what the value IS: the resolved number is the BuildKit daemon's [worker.oci] max-parallelism (candy/plugin-build/buildkit.go), set at daemon START and shared by every project on the uid — so it applies only when it matches the value the running daemon was started with.

This is consumer leg 6b of the stage_parallelism fan-out tracked in opencharly/opencharly#785. The producer (opencharly/plugin-box#66, tag v2026.301.1542) and the shipped-binary leg (#1082) are landed; this leg is the repo's own manifest.

How tested

Everything below ran on the committed tree this PR pushes.

$ git rev-parse HEAD
a4096c21ed51b27864c4b9f7680146572be67a12
$ git diff --stat origin/main...HEAD
 charly.yml                | 9 +++++++--
 charly/cue_tighten_test.go | 2 +-
 2 files changed, 8 insertions(+), 3 deletions(-)

1. This repo's own core gate — go test ./... and go vet ./... from charly/, exactly as AGENTS.md states.

$ cd charly && go test ./...
ok  	github.com/opencharly/charly/charly	181.869s
ok  	github.com/opencharly/charly/charly/internal/pluginsgen	138.321s
GOTEST_EXIT=0
$ cd charly && go vet ./...
GOVET_EXIT=0

2. The pre-flight the change class requires — the project's own manifest validates, on the tree that carries the rename.

$ ./bin/charly box validate
charly box validate: OK — checked 603 candies, 122 boxes, 49 deploys, 7 distros, 7 builders; 0 warnings, 0 errors
VALIDATE_EXIT=0

This is the operator's stated criterion on #938: "Re-run charly box validate — it will stay green, which is the point: it is green now with the retired keys." It is green both before and after; the rename changes the KEYS that are honoured, and the validator cannot see that either way. Which is precisely why the R7 instruments below exist.

3. R9 — the binary is this worktree's own CalVer-stamped build.

$ ./bin/charly version
2026.301.1632

2026.301.1632 is HEAD's own stamp (CHANGELOG/2026.301.1632.md, the entry PR #1082's merge wrote), so the binary equals the source. No Go production code changed — the only .go file in the diff is a comment in charly/cue_tighten_test.go — so the built binary is unchanged by this commit, and the tree is clean after the build:

$ git status --porcelain
(empty)

4. R7 (a) — the SCHEMA discriminator: the new spelling is the one the schema knows, the old one is the one it drops.

The claim under test: stage_parallelism / stage_parallelism_cap are the keys this repo's schema ACTUALLY reads, and podman_jobs / podman_jobs_cap are the keys it accepts and throws away. The discriminator is the schema's own constraint — stage_parallelism_cap?: int & >=1 (spec/schema/box.cue:121) — against the open #BoxConfig struct that validates the defaults: block.

$ bash /tmp/umb-stagepar/probe-schema.sh
===== the committed lines =====
55:    jobs: 4 # outer: concurrent IMAGE builds per DAG level (--jobs / CHARLY_BUILD_JOBS)
56:    stage_parallelism: 0 # inner: stages of ONE build; 0 = auto = min(NCPU, stage_parallelism_cap)
57:    stage_parallelism_cap: 4 # ceiling for the auto calc. The builder is BuildKit: the resolved

===== ARM X: the RENAMED key, out of the schema's range (the cap must be >= 1) =====
$ sed -i 's/^\(    stage_parallelism_cap:\) 4/\1 0/' charly.yml
$ ./bin/charly box validate
charly: error: command "validate": validation error: defaults: stage_parallelism_cap must be >= 1, got 0
ARM_X_EXIT=1

===== ARM Y: the RETIRED keys, same shape, same values =====
$ sed -i 's/^\(    stage_parallelism:\)/    podman_jobs:/; s/^\(    stage_parallelism_cap:\)/    podman_jobs_cap:/' charly.yml
$ ./bin/charly box validate
charly box validate: OK — checked 603 candies, 122 boxes, 49 deploys, 7 distros, 7 builders; 0 warnings, 0 errors
ARM_Y_EXIT=0

===== the tree is restored =====
RESTORED_IDENTICAL=yes

Arm X is the discriminator: the validator names the new key and constrains it (defaults: stage_parallelism_cap must be >= 1, got 0), so stage_parallelism_cap is a key this repo's schema knows and reads. Arm Y is the same block with the retired spelling: the validator is green — 0 warnings, 0 errors — because the defaults: block is validated against #BoxConfig, which is not closed, so an unknown key is accepted and then dropped by the decode. The two arms differ only in the spelling of the key, and only the renamed spelling produces a word from any tool in the chain.

5. R7 (b) — the CAP discriminator: the renamed key is LIVE and reaches the builder; the retired one never does.

The claim under test: with the RENAMED key the declared cap REACHES the builder; with the RETIRED key it is DROPPED and the sdk's fallback (4) is used instead. Both arms declare the SAME intent (a cap of 8) and differ ONLY in the spelling of the two keys. Same binary, same box, same command, same host, same running daemon.

$ bash /tmp/umb-stagepar/probe-twoarm.sh
===== ARM A: the RENAMED key at cap 8 (the committed tree) =====
55:    jobs: 4 # outer: concurrent IMAGE builds per DAG level (--jobs / CHARLY_BUILD_JOBS)
56:    stage_parallelism: 0 # inner: stages of ONE build; 0 = auto = min(NCPU, stage_parallelism_cap)
57:    stage_parallelism_cap: 8 # ceiling for the auto calc. The builder is BuildKit: the resolved
ARM_A_EXIT=1
charly: error: command "build": building fedora-layer-supervisord: preparing the builder for fedora-layer-supervisord: buildkitdrive: the running daemon was configured with max-parallelism=4 but 8 was requested; the value is set at daemon start, so stop the daemon (/run/user/1000/charly/buildkitd.sock) and this drive will start it with the new value

===== ARM B: the RETIRED key at cap 8 (origin/main's charly.yml) =====
56:    podman_jobs: 0 # inner: stages per `podman build`; 0 = auto = min(NCPU, podman_jobs_cap)
57:    podman_jobs_cap: 8 # ceiling for the auto calc — proven safe on podman 5.8.2 (20-run race gate)
ARM_B_EXIT=0

===== the tree is restored =====
RESTORED_STATUS_RC=0
RESTORED_IDENTICAL=yes

The two arms differ in ONE thing — the spelling of the two keys — and they read the two ways the manifest can mean "8":

  • ARM A (the renamed spelling): the declared 8 reached the builder, and the builder named it: "the running daemon was configured with max-parallelism=4 but 8 was requested; the value is set at daemon start". The value was READ. The build failed, which is the correct outcome for a cap the host's daemon was not started with — and it is the measurement behind the decision to keep this PR's cap at 4.
  • ARM B (the retired spelling): the same declared 8 was dropped on the way in, so the resolved value was the sdk's fallback, 4 — which is what the running daemon was started with, so the build succeeded (ARM_B_EXIT=0).

That is the whole defect in two arms: the retired spelling declares 8 and gets 4 in silence; the renamed spelling declares 8 and gets 8 (or a refusal that names 8). The key is honoured only in its new spelling, and this PR is the change that makes this repo's manifest use it.

6. R10 — the disposable bed that composes the changed entity. check-pod-overlay is the disposable pod bed that BUILDS this project's image — check-k8s-deploy-app plus a layer-ripgrep overlay, so the build path reads the renamed defaults: keys twice and asks the daemon for the resolved value — and then deploys the result and runs its check steps against it.

$ cd /home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/umb-stagepar/charly
$ export HOME=/home/atrawog && { CHARLY_BUILD_REGISTRY_MIRROR=127.0.0.1:5000 ./bin/charly check run check-pod-overlay > /tmp/umb-stagepar/bed-overlay.log 2>&1; echo "BED_EXIT=$?" >> /tmp/umb-stagepar/bed-overlay.log; }
$ tail -34 /tmp/umb-stagepar/bed-overlay.log
charly check run check-pod-overlay: [image-build] PASS after 1m17.268s (log: .check/check-pod-overlay/2026.301.1858/image-build.log)
charly check run check-pod-overlay: [check-image] PASS after 27.165s (log: .check/check-pod-overlay/2026.301.1858/check-image.log)
charly check run check-pod-overlay: [deploy-add] PASS after 2m17.976s (log: .check/check-pod-overlay/2026.301.1858/deploy-add.log)
charly check run check-pod-overlay: [config] PASS after 57.567s (log: .check/check-pod-overlay/2026.301.1858/config.log)
charly check run check-pod-overlay: [start] PASS after 41.985s (log: .check/check-pod-overlay/2026.301.1858/start.log)
charly check run check-pod-overlay: [wait-ready] PASS after 1.492s
charly check run check-pod-overlay: [bring-up-members] PASS after 177ms
charly check run check-pod-overlay: [check-live] PASS after 59.581s (log: .check/check-pod-overlay/2026.301.1858/check-live.log)
charly check run check-pod-overlay: [feature-run] PASS after 42.491s (log: .check/check-pod-overlay/2026.301.1858/feature-run.log)
charly check run check-pod-overlay: [update] PASS after 4m27.209s (log: .check/check-pod-overlay/2026.301.1858/update.log)
charly check run check-pod-overlay: [rebuild-ready] PASS after 1.503s
charly check run check-pod-overlay: [check-live-rebuild] PASS after 56.695s (log: .check/check-pod-overlay/2026.301.1858/check-live-rebuild.log)
charly check run check-pod-overlay: [wait-ready] PASS after 205ms
charly check run check-pod-overlay: [feature-run-rebuild] PASS after 41.935s (log: .check/check-pod-overlay/2026.301.1858/feature-run-rebuild.log)
charly check run check-pod-overlay: [cleanup] PASS after 9.934s (log: .check/check-pod-overlay/2026.301.1858/cleanup.log)
charly check run check-pod-overlay: [cleanup-members] PASS after 0s
charly check run check-pod-overlay: PASS (steps=16, skipped_checks=3) — check-image: declared check plan:candy:github.com/opencharly/layer-supervisord:5 did not run: context [runtime] not active in box mode; check-image: declared check plan:candy:github.com/opencharly/pod-check-keepalive:3 did not run: context [runtime] not active in box mode; check-image: declared check os-bash-service-running did not run: context [runtime] not active in box mode
BED_EXIT=0

The bed's own witness, written by the bed runner (.check/check-pod-overlay/2026.301.1858/summary.yml) — the header, the diagnostics and the verdict:

$ sed -n '1,7p;450,463p' .check/check-pod-overlay/2026.301.1858/summary.yml
bed: check-pod-overlay
calver: 2026.301.1858
driver: /home/atrawog/Sync/Atrapub/a/claude/opencharly/.worktrees/umb-stagepar/charly/bin/charly
driver_version: 2026.301.1632
driver_size: 54129311
driver_mtime: 2026-10-28T18:22:19Z
runner_stderr: runner.stderr.log
total_seconds: 823
diagnostics:
  errors: 0
  warnings: 0
  advisories: 0
  allowlisted: 0
  errors_fatal: true
  warnings_fatal: false
  advisories_fatal: false
  cache_ratio: not-applicable-no-build-steps
skipped_checks: 3
ok: true
status: PASS

The three skips are the bed's own declared box-mode skips ("context [runtime] not active in box mode") — the same three assertions run and PASS in the LIVE phases (check-live, check-live-rebuild, feature-run, feature-run-rebuild), where the runtime context IS active. The bed's update step is a fresh rebuild (charly update, 4m27s) followed by check-live-rebuild against the container it produced, so the R10 "fresh rebuild and live run" requirement is met by the bed itself, not asserted.

What this bed proves about THIS change. check-pod-overlay builds an image through this repo's own defaults: block — the build resolver reads stage_parallelism / stage_parallelism_cap out of the project's charly.yml (candy/plugin-build/resolve.go:246-255) — and it does so twice: once for the initial image-build and once for the update rebuild. With the retired spelling the resolver would have read nothing and fallen back to the sdk's 4; with the renamed spelling it reads the declared values. The two discriminators above are what turn that from "the file was edited" into "the renamed key is the one that is read".

7. R1 — the three failures met on the way, each RCA'd, none of them this change. The R10 gate was not a straight line, and the honest record is what the rulebook asks for: every anomaly gets a fresh RCA, and every finding is either fixed in this change or filed.

(a) check-boxload-pod — the first R10 witness — could not build at all. The bed's image step failed on a per-run image tag that does not exist upstream:

charly check run check-boxload-pod: [image-build] FAIL after 1m43.031s: exit 1: charly subcommand exited 1
               #2 ERROR: ghcr.io/opencharly/fedora-layer-supervisord:check-boxload-pod-2026.301.1829: not found
               error: failed to solve: ghcr.io/opencharly/fedora-layer-supervisord:check-boxload-pod-2026.301.1829: failed to resolve source metadata ...

Mechanism — CHARLY_BUILD_REGISTRY_MIRROR is read by the build drive at build time, but the mirror is rendered into the buildkitd.toml the daemon reads at start; the daemon on this host was already running with no [registry.…] section, so the build resolved the internal base against the upstream registry. Missed control — nothing asserts that a running daemon carries the mirror the drive is using. Blast radius — every bed whose internal base image is served by a mirror the running daemon does not carry. Root fix — filed as opencharly/plugin-build#35 (the daemon must be able to pick the mirror up without a manual restart). Not this change: the failure is in the daemon's registry configuration, and the two keys this PR renames are read by the build resolver, not by the registry path — the same defaults: block, a different field.

(b) The same bed, re-run with the mirror working, failed one step later, inside the build's own shell step:

               #22 ERROR: process "/bin/sh -c ... podman pull quay.io/libpod/alpine:latest ..." did not complete successfully: exit code: 125

Mechanism — the nested podman pull inside the build hits the sandbox boundary of the pinned layer-container-nesting candy. Missed control — the consumer pin in this repo's closure predates that candy's sandbox-boundary fix, and nothing in the build path flags a stale consumer pin before the step fails. Blast radius — every box composing that candy in this repo. Root fix — filed as opencharly/pod-nested-podman-socket#7 (the pin is the stale part; the candy owns the fix). Not this change: the failing step is a nested podman pull of a base image; the renamed keys set the daemon's max-parallelism and neither add nor remove a step from the build.

(c) check-sidecar-pod — the second R10 witness — failed at bring-up-members, with a substrate refusal:

charly: error: command "deploy": the "vm" deploy substrate is not available in this project — its deploy plugin (plugin-deploy-vm) is not in the candy closure. ...
charly check run check-sidecar-pod: [bring-up-members] FAIL after 2m36.77s: peer "check-sidecar-pod-ephvm" (vm deploy add): exit status 1

Mechanism — a deploy-level member addressed by its bare name is invisible to the host-side deploy-name resolver (charly/plugin_loader.go:resolveDeployNodeByPath resolves a top-level key or a DOTTED path, but not a bare member name), so the member's substrate word is never collected, its provider is never connected in that invocation, and the dispatch refuses a substrate the project does compose. The refusal's own text says the vm deploy "compiles no primary image plan" — so the build path, the only consumer of the two keys this PR renames, is never on that code path. Missed control — two resolvers implement the deploy-name addressing law and they disagree; nothing asserts that every name the deploy path accepts resolves to a node whose substrate word is collected. Blast radius — every deploy-level member addressed by its bare name whose substrate is served by an external provider. Root fix — filed as #1083 (with the full RCA, the two invocations' plugin-set diff, and the evidence paths). Not this change: the failure is in the deploy dispatch's substrate recognition, and the manifest's defaults: keys are read by the build path only.

Because all three findings are independent of this change and are genuinely separable from it, they are filed, not fixed here — and each is listed under Filed issues below, with its own RCA on its own thread.

8. R5 — the retired vocabulary is gone from this repo's live files. The predicate is calibrated against a known positive first, and the claim is the same pattern over the live files.

# A. the CALIBRATION — the same pattern over the WHOLE tree (the known positive: the changelog history)
$ git grep -n -i -E 'podman[-_]jobs|PODMAN_JOBS' -- .
CHANGELOG/2026.202.1139.md:725:the three env-tagged tunables — `CHARLY_BUILD_CACHE`/`CHARLY_BUILD_JOBS`/`CHARLY_PODMAN_JOBS` resolve
CHANGELOG/2026.301.1632.md:11:whose `charly box build` grammar still spells the inner-concurrency flag `--podman-jobs`, with the
CHANGELOG/2026.301.1632.md:12:env binding `CHARLY_PODMAN_JOBS`. The contract renamed that knob `podman_jobs` →
CHANGELOG/2026.301.1632.md:53:`charly.yml:55-56` still carries `podman_jobs` / `podman_jobs_cap`, the retired key names, which
CHANGELOG/2026.301.1632.md:120:$ /tmp/charly-1079/bin/charly-base -C <scratch> box build --podman-jobs 2
CHANGELOG/2026.301.1632.md:124:$ ./bin/charly -C <scratch> box build --podman-jobs 2
CHANGELOG/2026.301.1632.md:126:charly: error: command "build": unknown flag --podman-jobs
CHANGELOG/2026.301.1632.md:319:- **opencharly/charly#938** — "charly's own charly.yml still ships the retired podman_jobs /
CHANGELOG/2026.301.1632.md:320:  podman_jobs_cap keys, and validate accepts them silently". This repo's own `charly.yml:55-56`
CHANGELOG/2026.301.1632.md:332:- **opencharly/opencharly#785** — "stage_parallelism consumer legs: the retired --podman-jobs flag
(10 hits, every one of them a changelog entry about a past vocabulary — that is where it belongs)
exit=0
# B. the CLAIM — the same pattern over the LIVE files (the changelog is history)
$ git grep -n -i -E 'podman[-_]jobs|PODMAN_JOBS' -- . ':!CHANGELOG'
exit=1
(no output)
# C. the flag and env spellings, likewise
$ git grep -n -E -- '--podman-jobs|CHARLY_PODMAN_JOBS' -- . ':!CHANGELOG'
exit=1
(no output)

Rulebook compliance

  • R0 — skills first. /charly-internals:git-workflow, /charly-internals:strict-policy, /charly-internals:root-cause-analyzer, /charly-internals:cutover-policy, /charly-internals:go, /charly-build:build and /charly-check:check were loaded before the first source read and before the first git action, and this repo's own AGENTS.md was read before acting in it.
  • R1 — RCA every anomaly. The anomaly is the retired key dropped in silence; its RCA (mechanism, missed control, blast radius, root fix) is in the Summary above and in the commit message. Three further anomalies were met on the way and each got its own RCA: the cap's VALUE (the manifest declared 8, the org ran at 4, and the daemon is host-global) — which is why the value is 4 and why the "raise it" question is filed, not silently adopted; and the two BED failures in evidence block 7, each of which is independent of this change and is filed with its evidence. Zero warnings in every gate above: go vet rc=0, go test rc=0 across both packages, box validate 0 warnings 0 errors, and the R10 bed with 0 errors and 0 warnings.
  • R2 — no duplication. The keys live in exactly one place — this manifest's defaults: block — and there is no alias, no shim and no second spelling anywhere in the tree (proof: evidence block 8).
  • R3 — no workaround. The retired spelling is not aliased and not tolerated: the keys are renamed, and the retired names are absent from the live tree. The build path is not stubbed, mocked or bypassed anywhere.
  • R4 — the product first, then the prose. The product here is the manifest the loader reads; it is fixed in the same commit as its comment, and the comment now describes the mechanism that is actually in force.
  • R5 — legacy deleted completely. The retired vocabulary is gone from every live file of this repo, proven by the claim-keyed sweep in evidence block 8, with its calibration.
  • R6 — Git safety. git status was stated before every action; this PR is one commit on feat/stage-parallelism-938 off fresh origin/main (d10f09f); no force-push, no amend after the push, no direct push to main, no hook bypass, and no file outside this repo touched.
  • R7 — prove behaviour, not compilation. Two instruments, both of which FAIL on the retired spelling and PASS on the renamed one: the schema discriminator (evidence block 4 — the schema constrains stage_parallelism_cap and accepts podman_jobs_cap without a word) and the cap discriminator (evidence block 5 — the renamed key reaches the builder and the retired one is dropped before it does). The changed path executes live: the build path reads these exact keys, and the R10 bed builds through it twice.
  • R8 — live or skip. No service is mocked: the probes run the real builder against the real, running BuildKit daemon, and the R10 bed builds, realizes and runs a disposable deployment for real.
  • R9 — binary equals source. The binary is this worktree's own CalVer-stamped build (scripts/bootstrap-charly.sh → ./bin/charly), never a shared install; charly version is 2026.301.1632 = HEAD's stamp. No Go production code changed and no new runtime OS dependency is introduced — this is a YAML config edit — so packaging/ is untouched.
  • R10 — fresh disposable proof. The change class is project configuration, so the gate is the pre-flight (charly box validate, green, 0 warnings 0 errors) plus the disposable bed that composes the changed entity (check-pod-overlay, disposable, PASS) — and, because the diff also touches a .go file — this repo's own Go class gate (go test ./..., go vet ./...), all pasted above on the committed tree.

Cutover. One cutover, one PR: this repo's own manifest keys, on the landed vocabulary, with the retired names deleted in the same commit. The contract's rename, its migration entry and the flag's rename are the producer legs, already landed (opencharly/spec, opencharly/plugin-migrate, opencharly/plugin-box#66) — this PR is the consumer's own copy of the vocabulary, not a second cutover of the same surface. The GENERAL question (should a retired key FAIL validation rather than be dropped?) is the schema's and is deliberately not folded in: the operator separated it on #938 ("that decision is the schema's, and I am not folding it in here"), and it is filed under Filed issues.

Landing. PR-only; the org's charly/pr-validator is the merge gate and arms native auto-merge on PASS. No agent merges this PR.

Change classification

Filed issues

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

…ject manifest (#938)

The manifest spelled the inner build tunables `podman_jobs` / `podman_jobs_cap`.
Those keys are dead: the cutover that landed the knob as `--stage-parallelism` /
`CHARLY_STAGE_PARALLELISM` retired the podman-era spellings, and a retired key
under `defaults:` is dropped by the decode — silently, with no `KnownFields`
check — so this manifest asked for the sdk's fallback of 4 and nothing else.
Spelling them the landed way makes the request explicit.

The cap stays at 4, which is what the org's builds already ran at: the retired
key was dropped, so every project resolved to the sdk fallback (4), and the
BuildKit daemon is host-global per uid — carrying the podman-era 8 would
request a max-parallelism the running daemon was not started with, and would
fail the build rather than raise it.

The comment is corrected with it. The resolved value is the DAEMON's
[worker.oci] max-parallelism (candy/plugin-build/buildkit.go), set at daemon
start and shared by every project on that uid — not a per-build stage count,
which is what the podman-era wording claimed.

Filed issues (each RCA'd on its own thread; none of them is fixed by this
change): #919 (the daemon's value is host-global per uid and
no charly verb stops it), opencharly/spec#241 (a retired key under `defaults:`
validates clean and is then dropped), opencharly/plugin-build#35 (the build
registry mirror never reaches an already-running daemon),
opencharly/pod-nested-podman-socket#7 (a stale consumer pin in the closure),
#1083 (a bare-named deploy-level member gets no substrate
provider connected, so the dispatch refuses it after the side effect).

Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)
@github-actions

Copy link
Copy Markdown
Contributor

Review — PASS

What the diff actually does

Two files, exactly as the body states (charly.yml +7/−2, charly/cue_tighten_test.go +1/−1 = 2 files, 8 insertions, 3 deletions), matching the body's git diff --stat line and the head SHA a4096c21….

  • charly.yml: podman_jobs: 0 → stage_parallelism: 0, podman_jobs_cap: 8 → stage_parallelism_cap: 4, plus a rewritten 5-line comment. The YAML stays well-formed (continuation lines are same-indent standalone comments), and the comment now accurately states that the resolved value is the BuildKit daemon's [worker.oci] max-parallelism, host-global per uid and set at daemon start.
  • charly/cue_tighten_test.go: a single section-header comment updated from podman_jobs_cap>=1 to stage_parallelism_cap>=1. No production Go code and no test logic changed — the body says so explicitly and the diff confirms it.

Consistency checks against the body

  • Counts / SHA: match the body exactly.
  • R5 sweep: the body claims no podman_jobs/PODMAN_JOBS remains outside CHANGELOG. The one live hit that would have matched before this commit — the test-file comment — is precisely the line this diff renames. So the post-change sweep returning empty is internally consistent with the diff.
  • Cap rationale: The decision to carry 4 rather than the old declared 8 is coherent with the stated mechanism: the retired key was dropped (builds already ran at the sdk fallback of 4), and a mismatched value is a hard error against a host-global daemon that no charly verb can restart. Choosing 4 preserves today's behaviour rather than shipping a manifest that would fail every build on this host. The "raise it" question is filed (The BuildKit daemon has no supervisor: a changed stage_parallelism cannot be applied while it runs (a D-2 revision with a packaging leg) #919), not silently adopted.
  • Scope/cleanliness: no alias, shim, or second spelling introduced; the change is exactly the rename + comment correction the issue names.
  • Security: config-only; no secrets, injection, privilege, or dependency surface touched.

Disposition of comments

There are 0 comments. No maintainer sign-off exists, so the change must stand on its own evidence — and the diff is self-consistent and matches every concrete claim in the body (file counts, changed lines, the nature of the .go change).

Observations (non-blocking)

  • The PR ships no automated regression test that would fail without it — the .go change is a comment, and the validator cannot see the dropped key (that is the very bug). The general fix for that class is correctly delegated to opencharly/spec#241; the schema discriminator (arm X rejecting stage_parallelism_cap: 0) is the only behavioural witness, and it is described rather than encoded in a test. Acceptable for a manifest change, but worth noting.
  • The extensive probe/bed evidence cannot be re-executed here; it is, however, internally consistent with the diff and does not contradict it.

The change does what the body claims, is coherent and scoped, and carries an honest justification for the one judgment call it makes.

Verdict: PASS


ollama-cloud/deepseek-v4.1-flash — action-review.

View action run

@github-actions
github-actions Bot enabled auto-merge (squash) October 28, 2026 19:34
@github-actions
github-actions Bot merged commit d12c8a2 into main Oct 28, 2026
6 checks passed
charly-auto-merge Bot added a commit that referenced this pull request Oct 28, 2026
@aitrawog

Copy link
Copy Markdown
Contributor Author

STATUS

STATUS — the review round's one non-blocking observation, disposed with evidence.

The observation. "The PR ships no automated regression test that would fail without it — the .go change is a comment, and the validator cannot see the dropped key (that is the very bug). The general fix for that class is correctly delegated to opencharly/spec#241; the schema discriminator (arm X rejecting stage_parallelism_cap: 0) is the only behavioural witness, and it is described rather than encoded in a test. Acceptable for a manifest change, but worth noting."

Agreed on the fact, and here is why the discriminator cannot be a unit test in this repo. The schema's vocabulary is independent of this manifest's spelling: spec/schema/box.cue declares stage_parallelism_cap?: int & >=1 whether or not this repo's charly.yml uses it. A test written against the schema — "the validator rejects stage_parallelism_cap: 0" — therefore passes IDENTICALLY before and after this rename, because before the rename the retired spelling was accepted-and-dropped and the renamed spelling was still in the schema. A test that cannot fail without the change is invalid coverage by the org's own standard (R7), so encoding arm X here would have added coverage that proves nothing.

The witness that DOES discriminate this rename is a live two-arm probe, and it is in the PR body's evidence. Same manifest, same declared intent (a cap of 8), two spellings, one binary, one box, one host, one running daemon:

the manifest says what the build asks the daemon for
defaults.stage_parallelism_cap: 8 (the renamed key) max-parallelism=8 — the running daemon (configured at 4) refuses by name: "the running daemon was configured with max-parallelism=4 but 8 was requested"
defaults.podman_jobs_cap: 8 (the retired key) max-parallelism=4 — the sdk's fallback; the declared cap never reaches the builder and the build SUCCEEDS

The two arms differ only in the spelling, and only the renamed spelling produces a word from the daemon. That difference is observable only against a live BuildKit daemon whose configured max-parallelism differs from the manifest's declared cap — so it is a live two-arm probe by nature, and it is reproduced in the body's evidence block 5, not described.

Why the automated control for this class belongs to the schema, and is not built here. The general defect — a retired key under defaults: validating clean and being silently ignored — is opencharly/spec#241, with this instance's measurements on that thread. Its fix is to validate the defaults: block against a CLOSED schema, so a retired key is REJECTED instead of dropped. That fix is the automated regression test this observation asks for, at the layer where every consumer gets it: with it landed, this repo's PRE-rename manifest would fail charly box validate by name — the same assertion arm X makes, but produced by the validator for every project instead of by one consumer's test.

What this repo did instead, in the same cutover. The retired spelling is deleted from this manifest (the R5 sweep is in the body's evidence), and the comment above the keys is corrected with them. No test was added, because none added HERE could fail without this change, and the one that could — the validator refusing the retired key — is the schema's to make and is already filed with its evidence.

Nothing is filed away on the strength of this comment: the finding's disposition is opencharly/spec#241, and the evidence on that thread is the instance's own measurement.

Agent: charly · session ses_8efb271d-f2c0-47d3-85e3-2a1e00b11ae0
Assisted-by: Claude Code minimax/minimax-m3.1-flash:cloud (fully tested and validated)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

charly's own charly.yml still ships the retired podman_jobs / podman_jobs_cap keys, and validate accepts them silently

1 participant