Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b47210b
fix(perf): the memory number carries the name of what it measures
claude Sep 16, 2026
5e7215a
docs(calibration): re-bind steps 4/5/6 to the repaired instrument
claude Sep 16, 2026
b6c472e
docs(perf): §9 names the quantity, not just the mechanism
claude Sep 16, 2026
99a0600
feat(step7): host qualification and the execution binding, kept apart
claude Sep 16, 2026
7e8dfe0
fix(step7): a qualification is versioned by the T0 it claims to satisfy
claude Sep 16, 2026
830c241
fix(step7): prove the artifact before reading it, and verify the whol…
claude Sep 16, 2026
d0cb8fa
fix(step7): a declaration of false is evidence, not damage
claude Sep 16, 2026
a40bdfd
feat(step7): join the campaign to the freeze, and make the authority …
claude Sep 16, 2026
5c9f2ee
feat(step7): the merge order becomes a predicate on the target tree
claude Sep 17, 2026
81b3a13
fix(step7): the merge gate must run the machinery, not read its name
claude Sep 17, 2026
4ce4101
fix(step7): the witness needs positive controls, or refusal proves no…
claude Sep 17, 2026
96f6377
ci(step7): the merge gate becomes a check, not a script in a drawer
claude Sep 17, 2026
61cfea9
style(step7): drop a noqa the linter does not need
claude Sep 17, 2026
8fd6a1b
style: the linter yields to the instrument, not the other way round
claude Sep 17, 2026
b7216dd
merge: carry the instrument-aware lint fix forward
claude Sep 17, 2026
2e6683d
style(step7): satisfy the linter without moving any meaning
claude Sep 17, 2026
b72cef5
merge: carry the lint fixes forward
claude Sep 17, 2026
9bed20f
style(step7): the gate's own source passes its own repository's linter
claude Sep 17, 2026
346e164
merge: carry the lint fixes forward
claude Sep 17, 2026
1e488fd
fix(step7): an unreachable base is a question unasked, not a refusal
claude Sep 17, 2026
a49ef6c
fix(tests): the wiring suite exposes run(), so the runner can run it
claude Sep 17, 2026
80587c8
fix(perf): the rename left three callers holding a name that no longe…
claude Sep 17, 2026
d2278be
merge: carry the caller repair forward
claude Sep 17, 2026
40c7efa
merge: carry the caller repair forward
claude Sep 17, 2026
36c4c50
merge: carry the caller repair forward
claude Sep 17, 2026
715bbe7
fix(tests): rename the helper, not the entry point
claude Sep 17, 2026
1ba9f60
Merge pull request #358 from PhysShell/claude/p022-merge-gate-ci
PhysShell Sep 17, 2026
bc9eb3c
Merge pull request #357 from PhysShell/claude/p022-merge-gate
PhysShell Sep 17, 2026
157f53f
Merge pull request #356 from PhysShell/claude/p022-hostqual
PhysShell Sep 17, 2026
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
70 changes: 70 additions & 0 deletions .github/workflows/p022-merge-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: P-022 merge gate

# The merge order for the #263 preregistration, as a check rather than an
# agreement. `mergegate.py` is run against the commit a merge would PRODUCE —
# for a pull_request event that is `github.sha`, the merge ref, not the branch
# head — because a PR number proves someone pressed a button and says nothing
# about what the merged tree contains.
#
# Two jobs, and the gate needs the controls: a gate whose own controls are
# failing must not be the thing that says a merge is safe. A failed controls job
# leaves the gate job unreported, which a required check reads as unsatisfied.
#
# This workflow is one half of the wiring. The other half is a ruleset requiring
# these two checks; without it the job reports and nothing is prevented. The
# names below are the contexts that ruleset must name:
#
# P-022 merge gate controls
# P-022 merge gate
#
# Least privilege as elsewhere in this repository: read-only, and every
# third-party `uses:` pinned to a commit SHA with its version in a comment.

permissions:
contents: read

on:
pull_request:
workflow_dispatch:

jobs:
controls:
name: P-022 merge gate controls
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: the gate's own controls (fixtures ship the real tools and mutate them)
run: python tests/test_step7_mergegate.py
- name: the wiring's controls (applicability and co-change)
run: python tests/test_step7_mergegate_ci.py

gate:
name: P-022 merge gate
needs: controls
runs-on: ubuntu-latest
steps:
# Full history: the co-change rule compares the gate's own files against
# the base commit, which a shallow clone would not carry.
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: the gate, against the commit this merge would produce
env:
# Read through the environment rather than interpolated into the
# script body, so nothing from the event can be read as shell.
MERGE_COMMIT: ${{ github.sha }}
BASE_COMMIT: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
if [ -n "${BASE_COMMIT}" ]; then
python scripts/step7/mergegate_ci.py --repo . \
--commit "${MERGE_COMMIT}" --base "${BASE_COMMIT}"
else
python scripts/step7/mergegate_ci.py --repo . --commit "${MERGE_COMMIT}"
fi
2 changes: 1 addition & 1 deletion docs/evidence/calibration/p022-263a-design-constants.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"artifact": "p022-263a-calibration-design-constants",
"bound_measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3",
"bound_measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f",
"bound_policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf",
"constants": {
"G": [
Expand Down
2 changes: 1 addition & 1 deletion docs/evidence/calibration/p022-263a-policy-freeze.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"artifact": "p022-263a-calibration-policy-freeze",
"measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3",
"measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f",
"policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf",
"policy_implementation_digest_framing": "sha256 over the source set ordered by the UTF-8 bytes of each repo-relative POSIX path. Each file contributes, with no header and no separator: its path byte length as an 8-byte big-endian unsigned integer, its path's exact UTF-8 bytes, its blob byte length as an 8-byte big-endian unsigned integer, and its exact git blob bytes.",
"policy_source_commit": "b4f657a0abdfdfaae199cbc7eee0c47acd8b0057",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
"anchor_commit": "eedf6d3ed44ecf7bda69fa509dcd23b45960cf76",
"artifact": "p022-263a-calibration-training-preregistration",
"bindings": {
"design_constants_blob_sha1": "d03c8455cbcaf71446f88aeea0957b8e0c56c880",
"measurement_harness_digest": "562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3",
"design_constants_blob_sha1": "fa02f43bdf795e2f47d8ce781ec3a84f1dee64b5",
"measurement_harness_digest": "104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f",
"policy_implementation_digest": "c3068ed7fa880a7083866ead25fe8bf65c87889d242d8af1f7eee01582cd5cbf",
"training_scope_implementation_digest": "614bf9efe6ba2bb10e26a251c10d6c4a8fdaa99985d43ea03d76c6774447d25b",
"training_scope_root": "scripts/training/"
Expand Down
50 changes: 40 additions & 10 deletions docs/notes/p022-263a-instrument.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,19 +222,49 @@ into. **Both halves of every pair are committed**, and run B records run A's
path and sha256, so the verdict can be recomputed from evidence rather than
trusted.

## 9. Peak RSS
## 9. Peak memory

By a named mechanism, recorded, never eyeballed:
By a named mechanism **and a named quantity**, recorded, never eyeballed. The
quantity is not the same on both platforms, and the field says which one it is:

- POSIX: `os.wait4` — the kernel's per-child `ru_maxrss`, for exactly the
process spawned. Chosen over `/usr/bin/time -v` as primary because GNU time is
a package that may simply be absent, and "the tool was missing" is not a
memory measurement. `/usr/bin/time -v` remains the documented fallback.
- Windows: a Job Object, `PeakProcessMemoryUsed` via `QueryInformationJobObject`.
| platform | mechanism | `memory_metric` | what it counts |
|---|---|---|---|
| POSIX | `os.wait4` — the kernel's `ru_maxrss` | `max_process_peak_resident` | peak **resident** set, over the child and the descendants it waited for |
| POSIX fallback | `/usr/bin/time -v` | `max_process_peak_resident` | the same quantity |
| Windows | Job Object `PeakProcessMemoryUsed` via `QueryInformationJobObject` | `max_process_peak_commit` | peak **committed** memory of any process ever associated with the job |

`os.wait4` was chosen over `/usr/bin/time -v` as primary because GNU time is a
package that may simply be absent, and "the tool was missing" is not a memory
measurement.

Where nothing is available the value is `null` **with a reason**, so a silent
absence can never be read as a measured zero. Allocation counts are not yet
captured — recorded as owed on population B's track rather than quietly dropped.
**This used to be one field called `peak_rss_bytes`, and on Windows that was
false.** A witness settles it rather than a manual page: a child that commits
256 MiB and never touches a page is reported as 262.5 MiB through the job
object, while on Linux nothing becomes resident — job and process memory limits
are defined on committed virtual memory. The same witness refuted the other half
of the suspicion: `wait4` on the immediate child does *not* stop at a wrapping
shell. `shell -> heavy child` and `shell -> shell -> heavy child` both returned
the descendant's 260 MiB against 13 MiB for a light control.

So the two strata answer different, honest questions. The numbers are comparable
Rust-vs-Python **within** a platform and **may not be pooled or compared across
platforms**. The resident analogue on Windows is `PeakWorkingSetSize`, and
obtaining it for a tree of processes that come and go needs handle tracking or
polling; this instrument does not sample, and will not perturb the elapsed-time
interval so that two operating systems can pronounce the same noun.

A value produced under any metric kind outside that closed set raises rather
than being recorded, and no alias lets a Windows commit number go on being read
as an RSS number. Where nothing is available the value is `null` **with a
reason**, so a silent absence can never be read as a measured zero. Allocation
counts are not yet captured — recorded as owed on population B's track rather
than quietly dropped.

The repair moved `measurement_harness_digest` from `562a7f7232da` to
`104c384d01bf`. Steps 4, 5 and 6 were re-bound to the new digest in their own
commit; the committed evidence of runs that already happened was **not** rewritten
and still carries the old field name, because it records what the old instrument
produced.

On POSIX the same `wait4` call also carries the child's CPU split, fault counts
and context-switch counts. Those are now kept rather than discarded — see *The
Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ target-version = "py311"
# UP pyupgrade · C4 comprehensions · RUF ruff-native
select = ["E", "W", "F", "I", "B", "UP", "C4", "RUF"]

[tool.ruff.lint.per-file-ignores]
# scripts/perf_baseline.py is an INSTRUMENT SOURCE: its bytes are hashed into
# the measurement harness digest that the frozen #263 preregistration names
# (104c384d01bf), and steps 4, 5 and 6 are bound to that same value. Rewrapping
# a line there is not a cosmetic edit — it moves the digest and makes the frozen
# contract point at an identity the tree no longer contains, which is exactly
# what scripts/step7/mergegate.py exists to refuse. So the one over-long line
# stays and the linter is told why, rather than the instrument being reformatted
# to please it. Lift this only together with a re-binding of steps 4/5/6.
"scripts/perf_baseline.py" = ["E501"]

[tool.mypy]
# The package is held to --strict. Tests/fuzzers are not (they lean on dynamic
# construction and an AST oracle); ruff still covers them. The evidence tooling
Expand Down
Loading
Loading