feat(ci): #262 Stage 2 — Own.NET's own CI and dogfood select Rust; the public contract does not move - #350
Merged
Conversation
… does not #262 Stage 2 is deliberately thin. Stage 1 proved the Rust core works behind the launcher; Stage 2 proves only that Own.NET can RUN on it internally, and that doing so moved nothing a user can see. What did NOT change, because changing it would be Stage 3 wearing a disguise: EngineSelection.Default is still Engine.Python, action.yml's engine input still defaults to python, and a bare `owen` / `own-check.sh` / `own-check.ps1` / Action invocation still resolves Python. Stage 2 is not implemented by moving a default — it is implemented by every internal dogfood call site ASKING for Rust, which is why the ask is written at each call site where a control can read it back. The census, because the claim needs a denominator. "Own.NET CI is Rust-default" is unfalsifiable if it means "we found one job that runs Rust", so docs/evidence/p022-stage2-census.json names EVERY call site in every workflow that executes the analysis core or a production launcher surface — twenty of them across five workflows — and the role each plays: 7 public-contract verifiers that stay Python-default on purpose, 6 explicit references, 5 compare gates, 2 operational dogfood. tests/test_stage2_dogfood.py enumerates the workflows itself and fails in BOTH directions: an unclassified call site, and a ledger entry for a job that no longer invokes anything. A census that can be satisfied by editing prose is not a census. Class D — the Rust-default population — was exactly one job before this change: own-check-codescan, which calls itself dog-food in its own name. It now builds the production own-cli, records the candidate's sha256 and byte length, and passes `engine: rust`. The Action's own public default is untouched and is still exercised bare by own-check-surface and by the marketplace consumer simulation, so switching the dogfood deleted no public-contract evidence. stage2-dogfood is added because codescan is ubuntu-only and uploads a single code-scanning category, so it cannot carry Windows — and the two launcher surfaces differ in exactly the mechanics that cost Stage 1 six CI rounds. It runs this repository's own tree through own-check.sh on both platforms and through own-check.ps1 on Windows, and then breaks the candidate on purpose to show the run fails visibly rather than being rescued by Python. Nine controls, positive and negative: the census; every Class-D site selects Rust explicitly with a candidate; the public defaults have not moved; a forced Rust failure is never rescued; the candidate is the production own-cli and the job says which one; no PATH or target-dir discovery; the reference paths still exist and `--engine python` still answers; no compare gate was traded for Rust exposure; the claim covers Linux AND Windows. The public-default control is a falsifier, not a grep. It runs a BARE invocation with OWEN_RUST_CORE pointing at something that cannot work: if the default had moved, the run would die on the locator; because it has not, the run ignores the candidate entirely and produces a verdict. Written that way after the first version failed for the wrong reason — it assumed own-check.sh honours OWEN_PYTHON, and it does not, which is a real asymmetry between the launcher surfaces and is recorded in the control rather than papered over. And the relabel escape is closed: every other rule is satisfied by moving a job OUT of Class D, so a job that calls itself dog-food is Class D by declaration and the ledger does not get to disagree with the workflow about what the job is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
…urvived
The first campaign run was 9/10, and the survivor is the interesting one.
S03 deletes `windows-latest` from the Rust-default dogfood matrix, which is
exactly the regression `platform-leg-lost` exists to catch. It survived,
because the control asked whether the string `windows-latest` appeared
anywhere in the job — and after the deletion it still does, in every
`if: matrix.os == 'windows-latest'` step guard left behind. A job that could no
longer run on Windows still looked like one that did.
That is the same defect class as the Stage-1 shell locator whose bracket
matched nothing, and as the agreement control whose stderr assertion was
switched off by empty data: a validator that reads a proxy for the thing
instead of the thing. So the control now reads the job's ACTUAL runner images
— a literal `runs-on`, or `runs-on: ${{ matrix.os }}` resolved through
`strategy.matrix.os` in either its flow or block form — and reports them by
name. Verified against the mutant before recording anything: with S03 applied
it fails and names the two Linux legs it could still see.
Parsed rather than loaded, because PyYAML is not a declared dependency of this
repository and a control that only runs where an undeclared package happens to
be installed is the same kind of accident.
The same hole existed by construction in every other control that scanned a
job as text, so they now read a comment-stripped view: a mutation that comments
a line out must not leave the words behind for a grep to find. S11 is that
mutant — the engine selection commented out rather than deleted, which is the
shape a temporary rollback actually takes — and its twin S01 still deletes the
line, so both directions are covered.
The earlier result is deleted rather than kept: it measured a control that has
since changed, and evidence that describes a tree nobody can check out is not
evidence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
p022-stage2-1 @ e02e209, dirty:false — 11/11 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error, every expected catcher hit, and M00 surviving the unmutated tree. S01, S11 -> stage2::internal-default-not-rust engine selection deleted / commented out S02 -> stage2::wrong-rust-candidate candidate becomes the #260 dev adapter S03 -> stage2::platform-leg-lost the matrix loses its Windows leg S04 -> stage2::locator-contract-bypassed the candidate is found on PATH S05 -> stage2::rust-job-falls-back the dogfood swallows its exit code S06, S07 -> stage2::public-default-moved the product / the Action defaults to Rust S08 -> stage2::compare-gate-dropped a compare gate is disabled S09, S10 -> stage2::stage2-census a bare call site appears / a job is relabelled One layer on Linux, and that is a claim rather than a convenience. Every mutation here edits DECLARATIVE text — a workflow key, a ledger entry, an action input, one C# constant — and every catcher reads that text, so the verdict is identical on either platform. This is not the Stage-1 situation, where mutants of own-check.ps1 changed PowerShell semantics that only a Windows runtime can evaluate; those keep their Windows-native campaign and this one does not inherit their requirement. The one mutation with a platform dimension, S03, DELETES the windows-latest leg — and deleting it is precisely what a Windows runner could not have reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…Windows saw it `enumerate_call_sites` built its ledger key with `str(path.relative_to(ROOT))`. On Windows that is `.github\workflows\ci.yml`; the committed ledger stores forward slashes. Every key missed, so the census read all twenty call sites as unclassified AND all twenty ledger entries as stale, and `python-reference-lost` and `compare-gate-dropped` failed behind it for the same reason. Green on Linux, red on Windows, on a control whose entire job is to be the denominator. This repository has paid for a host separator once already: #260's mutation harness took it into a catcher NAME and reported five protected rules as unprotected. The ledger is a committed artefact shared by both platforms, so its keys are POSIX by definition and are now built that way. What the defect actually costs is the reasoning around it. The campaign's own note said one Linux layer sufficed because every mutant edits declarative text and every catcher reads that text, so the verdict "is identical on either platform". That argument was wrong in the only way that matters — not about a mutant, about the harness underneath every mutant — and no Linux campaign could have reported it. So the verdict is now MEASURED on both platforms by a `stage2-windows-mutations` gate rather than argued. The gate records nothing. One campaign has one recorded provenance, and it stays the Linux run; a second file claiming the same campaign name would make "which tree was measured" ambiguous, which is the failure the Stage-1 evidence work already fixed once. It only fails if Windows disagrees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
… 11/11 p022-stage2-1 @ 30a4918, dirty:false — 11/11 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error, every expected catcher hit, M00 surviving the unmutated tree. Re-recorded because the previous run measured a census whose ledger key took the host's path separator, and because S11 changed the definition's hash. A result that names a tree nobody would want to check out is not evidence, whatever its counts say. The Windows verdict on the same eleven mutations is a CI gate (stage2-windows-mutations) rather than a second recorded file: one campaign has one recorded provenance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
… agreed
The gate did its job — "Windows agrees: all 11 mutations caught, each by the
catcher its definition names" — and then failed the step after it, on
git checkout -- docs/evidence/p022-stage2-1.result.json
because at that commit the recorded result was not tracked yet, and `git
checkout --` on an untracked pathspec is an error rather than a no-op. A job
whose measurement had already succeeded reported red for a housekeeping line.
Restore-or-remove now, so the step is correct whether or not the file is
tracked at the commit under test, and the cleanup is turned into the assertion
it was always meant to be: the step fails if this gate left a recorded result
behind. That is the invariant worth having — one campaign, one recorded
provenance — and it was previously implied by a command rather than checked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
p022-stage2-1 @ 345cf04, dirty:false — 11/11 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error, every expected catcher hit, M00 surviving the unmutated tree. Re-recorded because the previous run named 30a4918, and the gate-cleanup fix after it touched .github/workflows/ci.yml — which is a mutation TARGET here. Evidence must name the tree that ships, not the one it was convenient to measure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Что и зачем
#262 Stage 2: this repository's own CI and dogfood run on the Rust core, and nothing a user can see changes. Stage 1 proved the core works behind the launcher; Stage 2 proves only that Own.NET can run on it internally. It is not the cutover — all four public surfaces still resolve Python, and Stage 3 stays behind its own owner decision.
Тип изменения
No product behaviour changes.
EngineSelection.Defaultis stillEngine.Python,action.yml'sengineinput still defaults topython, and a bareowen/own-check.sh/own-check.ps1/ Action invocation still resolves Python.The census, because the claim needs a denominator
"Own.NET CI is Rust-default" is unfalsifiable if it means "we found one job that runs Rust".
docs/evidence/p022-stage2-census.jsontherefore names every call site in every workflow that executes the analysis core or a production launcher surface, and the role each one plays.tests/test_stage2_dogfood.pyenumerates the workflows itself and fails in both directions: an unclassified call site, and a ledger entry for a job that no longer invokes anything. A census that can be satisfied by editing prose is not a census.21 call sites across 5 workflows, 10 entry-point patterns. Two negative lookaheads are declared in the ledger and were measured, not guessed:
benchmark.py --selftestandshadow_sweep.py --collect/--resultanalyse nothing, so counting them would inflate the denominator with runs that execute no core.Class D was exactly one job before this change:
own-check-codescan, which calls itself dog-food in its own name. It now builds the productionown-cli, records the candidate's sha256 and byte length, and passesengine: rust. The Action's public default is untouched and still exercised bare byown-check-surfaceand by the marketplace consumer simulation, so switching the dogfood deleted no public-contract evidence.stage2-dogfoodis added because codescan is ubuntu-only and uploads a single code-scanning category, so it cannot carry Windows — and the two launcher surfaces differ in exactly the mechanics that cost Stage 1 six CI rounds. It runs this repository's own tree throughown-check.shon both platforms and throughown-check.ps1on Windows, then breaks the candidate on purpose to show the run fails visibly rather than being rescued by Python.Nine controls, positive and negative
The public-default control is a falsifier, not a grep. It runs a bare invocation with
OWEN_RUST_COREpointing at something that cannot work: if the default had moved, the run would die on the locator; because it has not, the run ignores the candidate and produces a verdict. Written that way after the first version failed for the wrong reason — it assumedown-check.shhonoursOWEN_PYTHON, and it does not. That asymmetry between the launcher surfaces is recorded in the control rather than papered over.The relabel escape is closed: every other rule is satisfied by moving a job out of Class D, so a job that calls itself dog-food is Class D by declaration and the ledger does not get to disagree with the workflow about what the job is for.
Mutation campaign — and the survivor that mattered
p022-stage2-1@345cf04fe9f9,dirty:false: 11/11 caught, 0 survived, 0 compile-error, 0 invalid, 0 runner-error, every expected catcher hit,M00surviving the unmutated tree.The first run was 9/10, and the survivor was worth more than the nine kills. S03 deletes
windows-latestfrom the dogfood matrix — the exact regressionplatform-leg-lostexists to catch — and survived because the control asked whether the stringwindows-latestappeared anywhere in the job. After the deletion it still does, in everyif: matrix.os == 'windows-latest'step guard left behind. A job that could no longer run on Windows still looked like one that did.Same defect class as the Stage-1 shell locator whose bracket matched nothing, and as the agreement control whose stderr assertion was switched off by empty data: a validator reading a proxy instead of the thing. The control now reads each Class-D job's actual runner images and names them. Parsed rather than
yaml.safe_loaded, because PyYAML is not a declared dependency here and a control that only runs where an undeclared package happens to be installed is the same kind of accident. Every other text-scanning control now reads a comment-stripped view, and S11 is the mutant for that: the engine selection commented out rather than deleted, which is the shape a temporary rollback actually takes.The platform claim is measured, not argued
The recorded run is Linux. Every mutation edits declarative text and every catcher reads that text, so the verdicts should not differ by platform — and "should not differ" is the reasoning that failed here, twice over.
A second Windows-only defect, in the harness rather than in a mutant.
enumerate_call_sitesbuilt its ledger key withstr(path.relative_to(ROOT)). On Windows that is.github\workflows\ci.ymlwhile the committed ledger stores forward slashes, so every key missed: the census read all twenty call sites as unclassified and all twenty ledger entries as stale, and two more controls failed behind it. Green on Linux, red on Windows, on the control whose entire job is to be the denominator. This repository has paid for a host separator once already — #260's mutation harness took it into a catcher name and reported five protected rules as unprotected. Keys are POSIX by definition now.No Linux campaign could have reported that, so the campaign verdict is now measured on both platforms by a
stage2-windows-mutationsgate. Windows agrees: all 11 caught, each by the catcher its definition names. The gate records nothing — one campaign has one recorded provenance, and a second file claiming the same campaign name would make "which tree was measured" ambiguous — and it asserts that it left nothing behind rather than merely tidying up.Как проверено
python tests/run_tests.py— rc 0; stage-2 dogfood controls 9/9, 0 skippedruff check .— All checks passed;mypy --strict— Successpython scripts/mutate_campaign.py --campaign docs/evidence/p022-stage2-1.json --run— 11/11python scripts/render_checkpoint_status.py --check— fragments in sync with the evidenceOWN001at exit 1 over this repository's own tree; SARIF 2.1.0 with 165 results at--severity warning; a broken candidate → exit 2, no verdictСвязанные issue
Refs #262 (Stage 2 of four; #262 stays open through Stage 4), #261, #260. Closes nothing.
Чеклист
docs/proposals/README.mdmove together, with the qualifier: repository CI/dogfood only, Python remains the public engine and referenceMeasured on CI at this head
a39b3eab6505b70a…, 1 899 136 bytes11ab5be26143d1a8…, 1 660 416 bytesOK: Rust-default dogfood on ubuntu-latest (own-check.sh)OKthrough bothown-check.shandown-check.ps1OK: a broken candidate is a visible failure (exit 2), never a Python rescueFindings recorded rather than fixed
own-check.shhas noOWEN_PYTHONoverride. The C# launcher does; the shell surface calls a barepython. A real asymmetry between two public surfaces, out of Stage-2 scope, recorded in the control that tripped over it.Owencode-scanning check reportsneutral— "1 configuration not found". It is GitHub's code-scanning tool status, not a CI failure, and it reads the same way onmain. Named here so a reviewer does not have to wonder why the head is not uniformly green.🤖 Generated with Claude Code
https://claude.ai/code/session_01CYLNQy6tLXqsV1CbuNqsSb
Generated by Claude Code
Generated by Claude Code