> Superseded in part (operator ruling 2026-10-05). No new contract version: info.version stays 1.0.0-SNAPSHOT, no 1.1.0 and no release tag. The 1.1.0 and tag passages below (R-1, R-7 and the spike rows at 1.1.0-SNAPSHOT and 1.1.0) are the planning-time record; the spec (CL-2, FR-017, FR-018) and plan prevail. The R-7 finding that a same-version bundle change fails with BUNDLE_CHANGED_VERSION_SAME is why the scratch baseline carries a lower version.
Research: Mission Status contract 1.1
Mission mission-status-contract-1-1-01M42XJC (issue #5625, part of epic #5528). Phase 0 output of the plan. Every entry gives the question, the evidence that was produced on this checkout, the decision and what it costs. Everything marked run was executed during planning against the branch as merged with main (the branch head is the merge of origin/main at its tip when the plan started; git merge-base HEAD origin/main equals that tip). Scratch copies of the contract tree, a git archive copy of the repository and a scratch virtual environment lived outside the repository; nothing under kitty-specs/ other than this directory was written, and the corpus reality check was run once (R-5).
Command notation: <scratch> is a scratch directory outside the repository. Commands are shown as run, with the checkout's own interpreter (.venv/bin/python), never a bare uv run.
R-1 OQ-1: the structure check and a fifth CHANGELOG heading
1. structure_check is file-wide, not entry-wide: the four section names already exist in the 1.0.0 entry, so a 1.1.0 entry that dropped Removed would still pass. A test in the contract-proof module reads the ## 1.1.0* entry itself and requires all five headings in it. 2. provisional_check.provisional_sections returns the text of every Provisional section of the file, joined. A word that the 1.0.0 entry already uses (code, staleness, ...) therefore satisfies a 1.1.0 element of that name even when the 1.1.0 entry omits it. The same test requires every AD-14 element name inside the 1.1.0 entry's own Provisional section.
- Question. Does
contracts/tools/structure_check.pyaccept a fifth heading for the deferred-gaps record, or must it sit under an existing heading (FR-018)? - Read.
check()collects every heading ofCHANGELOG.mdat any level (code fences ignored) and fails only when one of the four names inCHANGELOG_SECTIONS(Added,Changed,Removed,Provisional) is absent from that file, or when no heading equals the module's currentinfo.version(has_version_heading: the version text optionally followed by a space and a note, so1.1.0-SNAPSHOTneeds a heading of that text). It forbids no extra heading. - Run (accept). A scratch copy of
contracts/withinfo.versionrewritten to1.1.0-SNAPSHOTand a new top entry## 1.1.0-SNAPSHOTholding### Added,### Changed,### Removed,### Provisionaland a fifth### Deferred to the next major version:python contracts/tools/structure_check.py --root contractsexits 0, last linecounts: readme_headings=20 changelog_headings=12. - Run (control). The same copy with the entry heading renamed to
## Nextexits 1 withCHANGELOG_VERSION_HEADING_MISSING ... no heading for the current info.version '1.1.0-SNAPSHOT'. - Decision. The deferred-gaps record is a fifth level-3 heading
### Deferred to the next major version, a sibling of the four standard headings inside the## 1.1.0-SNAPSHOTentry, placed after### Provisional. It needs no tool change. The two behaviours of the new reads that FR-018 asks for (invalid UTF-8 is a 415; root status records are no artifacts) sit under### Addedas reader notes, and### Changedand### Removedsay "Nothing.". - Two weaknesses found, both closed by a committed test of this Mission (plan D-P11).
R-2 OQ-4: the caps, measured before they are kept
- Run. A read-only script (standard library plus PyYAML, no write under the repository) walked every Mission directory that holds a
meta.jsonand applied the eligible-file rule of the spec (no symlink component, regular file, rootstatus.events.jsonlandstatus.jsonexcluded, path not malformed). - Measurement (548 Missions, 0 directories without
meta.json).
| Quantity | Value |
|---|---|
| Eligible files, all Missions | 15,561 (plus 1,061 root status records excluded; 16,622 files in all) |
| Largest Mission tree | 126 files (the Mission of the v1 contract itself); p99 100; median 24; six Missions have more than 100 |
| Entry cap 1000 against the largest tree | 7.9 times headroom; no Mission is truncated |
| File size: median, p99, p99.9, maximum | 4,721 B; 45,324 B; 116,053 B; 619,090 B |
| Files over 100 KiB / over 200 KiB / over 262,144 B | 20 / 2 / 2 |
| Not UTF-8 / with a NUL byte | 2 (CSV files) / 3 (markdown files) |
| Holding a credential pattern | 0 |
| Symlinks | 0 |
Names with a space, non-ASCII, an at sign or a home/<x>/ segment | 0 (hidden files: 640) |
| Malformed-path files | 0 |
| Files with a sub-second modification time | all 15,561 (so the AD-22 floor test is meaningful) |
| Files with a human-class host-path pattern / an e-mail pattern | 859 / 193 (redaction is exercised on real data) |
- Decision. Keep 1000 entries and 262,144 bytes. The cap is 7.9 times the largest tree and 0.01 percent of files exceed the size cap; neither moves, so no squad note is raised for a cap change. The over-cap files of the corpus are
charter-contract-cleanup-tranche-1-01KQATS4/research/_artifacts/test-gate-5-ruff.txtandunblock-sync-identity-boundary-canary-01KRZJ07/canary-evidence/full-pytest.txt(413); the not-UTF-8 files are004-modular-code-refactoring/research/evidence-log.csvand.../source-register.csv(415); the NUL files are003-auto-protect-agent/data-model.md,004-modular-code-refactoring/research.mdand005-refactor-mission-system/data-model.md(415). They are the named files of the pinned fixture (R-5).
R-3 The owned-files matcher: call it or reproduce it
- Read.
_matches_any_glob(filepath, patterns)insrc/specify_cli/policy/commit_guard.pyand_mt_matches_owned_file(path, patterns)insrc/specify_cli/cli/commands/agent/tasks_move_task.pyrun the same loop:fnmatch.fnmatch(path, pattern)or, for the pattern with every/*and then every/removed and a trailing/stripped, a non-empty prefixPwithpath.startswith(P + "/"). - The exact difference.
_mt_matches_owned_filefirst normalises the path (replace("\\", "/"), thenremoveprefix("./"))._matches_any_globnormalises nothing. Neither normalises the pattern: the production caller of the first,_mt_owned_file_patterns, does that before the call (same two operations, blank entries dropped), and_matches_any_globhas no such caller step. - Importability. Both are private (leading underscore). Neither is public API. Both import without a
src/change:specify_cli.policy.commit_guardin 0.12 s,specify_cli.cli.commands.agent.tasks_move_taskin 0.46 s (it pulls the CLI layer in); the second is also re-exported byspecify_cli.cli.commands.agent.tasksand its presence is pinned bytests/specify_cli/cli/commands/agent/test_tasks_compat_surface.py. - Run (19 cases). Every FR-022 glob case (15) plus four normalisation cases, through both copies: 17 agree; the two cases with a path written
./src/foo/x.pyorsrc\foo\x.pyagainstsrc/foo/giveFalsefrom_matches_any_globandTruefrom_mt_matches_owned_file; the two cases with a pattern written./src/foo/orsrc\foo\*giveFalsefrom both (the function does not normalise patterns). All 15 FR-022 outcomes equal the spec's statements (.pymatchessrc/x.py;src//x.pydoes not matchsrc/x.py;/x.pydoes not matchx.py;a//b//cdoes not matcha/b/c;[x]/matches[x]/yby the prefix step;a//cmatchesa/c/z). - Private import from a test. No architectural gate forbids it:
grepovertests/architecturalfinds no rule on private names, the ruff families selected inpyproject.tomlareE F W C90 C4 ARG B SIM UP ASYNC S TID251and thePT0xxweak-oracle rules (no private-import rule), andtests/already imports underscore names fromsrc/in many files (for exampletests/acceptance/test_accept_gate_rejection_cycle.py). - Corpus effect of the normalisation. Of 14,638 owned-file entries, 0 start with
./, 1 holds a backslash (an archived Mission, a stray literal) and 6 hold a brace; 1,078 are globs (7.4 percent). Every corpuschangeStateisunknownregardless (host without worktrees), so the choice below cannot change a corpus assertion. - Decision: reproduce, with a parity test. The reader's
matches_owned_file(path, pattern)(in the reader module, a pure function, nosrc/import) normalises the path like_mt_matches_owned_fileand uses the pattern as authored (spec FR-007 rule 3). A parity test (test code only) imports both private functions and requires equal results over every FR-022 glob case and the four normalisation cases, with the two known path-normalisation divergences pinned as such. Why not call one: (1) AD-18, acknowledged by the operator, refused a private seam for review cycles on the projector's public-readers-only rule; calling a private matcher from the reader would reintroduce that exception; (2) the two copies disagree, so "call" would have to pick one and say why; (3) the reader is the executable statement of the rules that a service is built from, and importing the CLI layer into it is the wrong direction. The parity test is the drift alarm (tracer F-2).
R-4 The rename-names seam for FR-007 rule 2
- Finding. A public seam exists, so NFR-007 holds without a
src/change and no operator decision is needed.kernel.git.changed_paths(cwd, *revs, renames=False)(src/kernel/git/listing.py, exported inkernel.git.__all__) lists a rename as a delete plus an add, so both names appear;kernel.git.changed_entries(..., renames=True)returnsNameStatusEntry(status, path, orig_path)(new and old name).merge_base_changed_files_checked(specify_cli.core.vcs.git) cannot serve rule 2: it callschanged_paths(..., renames=True)and returns only the new name. - Run. Scratch repository, branch
lane-aoffmainwith one commit that renamessrc/old_name.pytosrc/new_name.py, deletesgone.txtand leavesuntracked.txtuntracked:merge_base_changed_files_checked(wt, "main")gives('gone.txt', 'src/new_name.py')(old name lost);changed_paths(wt, merge_base, "HEAD", renames=False)gives['gone.txt', 'src/new_name.py', 'src/old_name.py'];changed_entries(..., renames=True)gives[('D','gone.txt',None), ('R100','src/new_name.py','src/old_name.py')]; the untracked file is absent (committed changes only);specify_cli.core.git_ops.get_current_branch(wt)giveslane-aandNoneaftergit checkout --detach(the detached-HEAD test of rule 1). - Decision. The reader composes
git_merge_base(specify_cli.core.vcs.git) withchanged_paths(..., renames=False)and treats aNonemerge base or aGitCommandErrorasunknown. The field catalogue's x-derived inputs forchangeStatetherefore namegit_merge_baseandchanged_paths(plan "Departs from the spec", item 2).
R-5 Corpus floors re-measured on the merged tree (A-5)
- Run, once.
PWHEADLESS=1 .venv/bin/python -m pytest -m "corpus and not windows_ci" tests/contract/test_mission_status_reality.py -p no:cacheprovider -s --durations=15 -q(the selection of the router jobtests-corpus-blocking): 577 passed in 91.18 s (wall clock 2:15 including interpreter start; the first case carries a 19.8 s setup, thetests/conftest.pyisolated-install fixture). Report lines: 548 Missions projected, 3,197 work package payloads validated, 24,458 events validated; own-directory pass 3,222 snapshot work packages and 3,197 work package files; disagreement list 8 Missions, ratchet ceiling 8 (the issue, owner and drain date are in the header oftests/contract/fixtures/mission_status_expected.json); 37 coordination fallbacks (environment-dependent, never floored); 27 redactions; slowest case 2.02 s. Not rerun. - CI baseline (read from the Actions API for the last three
ci-router.ymlruns onmain).tests (corpus-blocking)178 s and 183 s (skipped on the third);tests (contract tools)230 s, 156 s and 177 s; the advisory packs corpus suite on the latest push 15 min 12 s against its 20 minute timeout. The spec quoted 3 min 43 s before #5581; the merged job is shorter. - Run (read-only measurement script, standard library plus PyYAML plus the leak patterns; second script: status-log-only ids through
materialize_snapshot).
| Counter | Measured | Proposed floor |
|---|---|---|
Missions with meta.json | 548 | 500 (existing, unchanged) |
Files selected by wp_task_files | 3,197 | 3,000 (existing) |
Distinct valid work_package_id (the detail universe) | 3,193 | 3,000 |
| Files skipped (unparseable frontmatter or no valid id) | 0 | none in the corpus; planted fixtures only |
| Ids present only in the status log (404 on the detail) | 29 in 7 Missions | 26 |
| Work package ids held by more than one file | 4 ids in one Mission | named, subset check, anomaly floor 1 |
| Work packages with no prompt file | 1 (sync-diagnose-canonical-allowlist-01KS4F8H, tasks/WP01.md) | named, anomaly floor 1 |
| Work packages with an empty roster | 9 | anomaly floor 1 |
| Subtask ids in frontmatter | 15,192 | 14,000 |
tasks.md table rows / checkbox rows | 12,666 / 8,747 | 12,000 / 8,300 |
Missions without tasks.md / without lanes.json | 12 / 80 | anomaly floor 1 / 72 |
| Dependency entries | 3,305 | 3,100 |
| Owned-file entries (globs; braces) | 14,638 (1,078; 6) | 13,000 (1,000; anomaly floor 1) |
| Review-cycle files, Missions holding them | 1,322 across 225 | 1,200 across 210 |
Cycle files the product cannot parse (ReviewCycleArtifact.from_file raises) | 168 | 150 |
Parseable cycles with a date-only reviewed_at / with a non-Z offset | 1 / 36 (of 1,154 parseable) | anomaly floor 1 / 32 |
| Cycles whose pointer equals an event reference of the same work package (verdict non-null) | 306 | 290 |
| Eligible files (listing entries) | 15,561 | 14,000 |
readable: true / readable: false | 15,554 / 7 | 14,000 / the 7 named files |
| Refused 413 / 415 (not UTF-8) / 415 (NUL) / 422 | 2 / 2 / 3 / 0 | named files; 422 by a planted fixture Mission |
| Files where redaction changes the text | 1,023 | 970 |
| Kinds present in the corpus | all twelve | every kind at least once |
- Margin rule. Floor = 95 percent of the measurement rounded down to two significant figures; a count under 100 uses 90 percent rounded down to an integer; an anomaly class (measurement under 20) has floor 1 and is pinned by name with a subset check (every named file must still be present and refused or classified as stated; an unnamed new anomaly is reported, not a failure, because an archived Mission cannot be edited and a new Mission may legitimately add one). The first guard is always examined equals discovered on an independent scan; the numeric floors are the second guard (FR-021). The existing floors (500, 3,000, 2,800) stay. Two numbers differ from the spec's orientation figures and are the re-measured ones: 80 Missions without
lanes.json(spec: 77 of 544) and 168 unparseable cycle files (the spec's "about 1.14 thousand ... 36 ... 3" counts rawreviewed_atstrings by regular expression; counted through the product reader, 1,154 parse, of which 1 is date-only, the others are unparseable and so null for the parse reason). - A finding the spec did not know (plan D-P6). Four work package ids of the Mission
cli-boundary-robustness-01M2NQCB(WP02,WP03,WP04,WP06) are each held by two files (for exampleWP02-canonical-json-contract.mdandWP02-shared-json-contract.md). The 3,197 files hold 3,193 distinct ids. v1's reality check builds oneWorkPackagepayload per file (3,197), so two payloads there carry onewpId.
R-6 CI placement and the cost of the new tests
- Which job selects
tests/contract.tests-corpus-blocking(ci-router.yml, 10 minute timeout, gated on the router groupcorpus, which includescontracts/) runs exactly three modules:test_example_round_trip.py,test_mission_status_payloads.py,test_mission_status_reality.py.tests-contract-tools(15 minute timeout,-n 4 --dist loadfile, environmentuv sync --frozen --no-install-project, gated oncorpusorcontract_tools, the latter includingtests/contract/) runs the whole directory minus those three, so a new corpus-marked module lands there with no workflow edit..github/ci-module-registry.ymlexcludestests/contractfrom the module matrix by design. - Run (environment). In a scratch virtual environment built with
uv sync --frozen --no-install-project(the tool job's form) the helper unit teststests/contract/test_mission_status_payloads.pypass: 157 passed. A new module that imports the payload helper therefore runs in the tool job's environment. - Run (two registrations a new corpus-marked module needs). In a scratch git repository holding a
git archiveof the branch head, a one-test moduletests/contract/test_zz_planted_new_module.pymarkedcorpus: (a)tests/ci/test_contracts_workflows.py"one home" tests pass (the tool job runs it); (b)tests/architectural/test_ci_corpus_trigger_completeness.py::test_corpus_marked_modules_match_the_curated_registryfails ("newly marked pytest.mark.corpus but absent from the curated registry"); (c)tests/architectural/test_same_tier_uniqueness.py::test_no_per_change_overlap_outside_the_allowlistfails (ci-router.yml::tests-contract-tools x packs.yml::built-in-corpus-suite: 1 node). After one added--deselect tests/contract/test_zz_planted_new_module.pyin the packs corpus-suite command (.github/workflows/packs.yml), (c) andtests/ci/test_corpus_blocking_home.pyandtests/architectural/test_no_duplicate_suite_execution.pypass again (200 passed, 1 skipped). Both registrations lie outside the spec's slice file set: operator decision OD-1. - Run (cost model of the listing and content readers over the whole corpus). A throwaway script (not the reader) walked, stat-ed and read in full every returned entry (the listing's work), then re-read each readable file, redacted it, scanned it and validated a closed envelope with
jsonschema(the content read's work): listing 3.4 s plus content 19.8 s, 23.2 s for 15,561 files in 548 Missions; worst Mission 0.31 s (117 entries); 1,023 files with a replacement. - Run (the 1000 times 262,144 byte worst case). A scratch directory of 1000 entries of 262,144 bytes (261,881,858 bytes returned:
meta.jsonsorts first, so 999 task files) listed with the same whole-file checks: 6.2 s, identical warm. That is the one timed synthetic case of NFR-001 (limit 60 s). - Run (synthetic git repositories). One scratch repository with a lane worktree, two commits including a rename and a name-status diff takes 0.017 s on average, so the change-state matrix is far below a second in total.
- Budget. NFR-001 allows the corpus job 8 minutes. Baseline 2 min 58 s and 3 min 03 s (R-5). The modelled extension is 23 s locally; with the real reader and the CI factor (the same module took 91 s of pytest here against 178 s on the runner) the plan expects +40 to +90 s, so the job lands near 4 to 5 minutes. Re-plan threshold: a post-implementation corpus job above 6 minutes (75 percent of the limit) stops the Mission for a squad note before the PR. No new case may exceed 60 s; the planned maximum is the 6.2 s synthetic case.
- Re-read for the plan fix. The tool job's installation is
uv sync --frozen --no-install-projectfollowed byuv run --frozen --no-sync pytest; the project is not installed in that environment andpytest.inisetspythonpath = src. The claim above covers the payload helper's imports; the larger import chain of the matcher parity test is proven separately in R-13. - Decision. No new CI job. The reality-check extension lives in
test_mission_status_reality.py(jobtests-corpus-blocking); the reader and synthetic tests live in new modules that the tool job runs..github/ci-module-registry.ymlanddocs/development/reference/ci-gate-mechanics.mdneed no edit. If OD-1 is decided "new modules", the only workflow edit is one--deselectper new module inpacks.ymland one registry row per module intest_ci_corpus_trigger_completeness.py.
R-7 Release dependency and the --baseline-root recipe (run)
- Setup.
<scratch>/baselineholdsmission-status/and_shared/extracted withgit archive HEAD contracts/mission-status contracts/_shared(the Mission's base: v1 at1.0.0-SNAPSHOT).oasdiff1.32.1 was installed into the scratch directory with the repository's owncontracts/tools/install_tools.py --pins contracts/tools/pins.json --dest <scratch>/tools --only oasdiff(checksum verified by the installer). The candidate is a scratch copy of the repository tree carrying one additive operation (a spike of the plannedlistArtifacts: one path filemissions_missionId_artifacts.yaml, five schemas, one response file, two examples, index entries, the path map entry,info.version: 1.1.0-SNAPSHOTand a## 1.1.0-SNAPSHOTCHANGELOG entry). - Command and results.
PATH=<scratch>/tools/oasdiff-1.32.1:$PATH .venv/bin/python contracts/tools/breaking_check.py --root contracts --baseline-root <scratch>/baseline:
| Candidate | Exit | Last line |
|---|---|---|
1.1.0-SNAPSHOT | 0 | counts: modules=1 baselines=1 breaking=0 provisional_changes=0 no_baseline_initial=0 preview_ref=skipped |
1.1.0 | 0 | same |
1.0.1 | 0 | same (a non-breaking change passes at any higher version: why FR-017 needs the extra observables) |
2.0.0 | 0 | same |
1.0.0-SNAPSHOT (bundle changed, version not moved) | 1 | BUNDLE_CHANGED_VERSION_SAME |
1.1.0-SNAPSHOT plus a property added to the WorkPackage schema | 1 | BREAKING_WITHOUT_MAJOR ... added the optional property ... (closed response schemas ...), breaking=1 |
1.1.0-SNAPSHOT plus a 409 response on GET /missions/{missionId} | 1 | BREAKING_WITHOUT_MAJOR ... added the non-success response with the status 409, breaking=1 |
- Observable 3 (a), byte identity (run).
diff -rq <scratch>/baseline/mission-status contracts/mission-statuslists exactly the new files plusCHANGELOG.md,openapi.yamland the three_index.yamlfiles that gained entries (schemas,responses,examples;parameters/_index.yamljoins when the query parameter is added);diff -rqover_sharedreports no difference. - Observable 3 (b), bundle comparison (run). With the Python resolver (
contract_resolver.resolve(...).tree, the tree the bundle is written from): the candidate tree minus the new path keys and withinfo.versionset to the baseline's equals the baseline tree (True); a planted409added to an existing operation makes the comparison differ. Because the tree compared includestagsandservers, the three new operations reuse the existingMissionstag: a new tag would make the equality false. - Other checks on the same spike (run,
<scratch>git repository for the checks that read git).layout_check,example_check(36 examples validated),enum_pin_check,leak_scan,structure_checkexit 0;citation_checkreports no failure (only the informationalCITATION_REUSElines; the limit of five properties per citation is a report, not a gate);provisional_checkreportedkind is not named in the Provisional section: a property that$refs a provisional schema is itself counted as a provisional element named after the property, so the CHANGELOG must namekindas a whole word (R-1, weakness 2); the bundle (bundle.py --bundle-only) and the vacuum lint (lint_check.pywith the pinned vacuum binary, 9 rules) pass:violations=0. JVM-only steps (Gradle validation, the Java view, the TypeScript client smoke, the resolver parity) andrelease_checkover a real tag could not run here. - When the 1.0.0 tag matters. (1) Never for the contract, tooling and test WPs, which use the pure functions and the
--baseline-rootrecipe. (2) For the live baseline test and the PR-time evidence: the live test takes the baseline fromgit archiveof the tagcontract-mission-status-v1.0.0and fails (never skips) while the tag is missing, with the recipe in its message. (3) The wrap-up rebase: the release commit editsinfo.version(to1.0.0) and the CHANGELOG heading (## 1.0.0) that this branch also touches (it adds1.1.0-SNAPSHOTabove the old heading), so the branch is brought level with that commit during wrap-up before the draft PR (CL-2, A-4); the expected conflict is two lines and resolves by keeping the release's1.0.0heading and this branch's1.1.0-SNAPSHOTversion and entry. The operator is asked for the tag at implement start (plan D-0), because it is a maintainer action with unknown lead time; it is needed from the wrap-up rebase (W-6) and for the final evidence (W-7), which is the only evidence run on the final tree: the compaction (W-5) and the rebase (W-6) both change the tree after the aggregate squad (W-4), so earlier runs are pre-tag or pre-rebase evidence and are labelled so.
R-8 Leak classes, redaction and the planted negatives
- Read.
contracts/tools/leak_patterns.pyalready returns three codes (HOST_PATH,EMAIL,SECRET) and listsFORBIDDEN_PROPERTY_NAMES; no pattern is added or weakened by this Mission. None of the planned property names is a forbidden name (run:is_forbidden_property_nameover all 38 planned names returns nothing). - Run (existing content survives the new strict names). In a scratch copy, adding the ten names of FR-015 (a) (
id,laneId,laneBranch,planningBranch,pattern,feedbackReference,reviewer,kind,mediaType,changeState) toSTRICT_FIELDSinfixture_builder.py:leak_scan.py --root contractsstill exits 0 andvalues_strictrises from 333 to 518 (re-measured at the plan fix on a scratch copy:values_humanreads 7952 instead of 8137 andvalues_all23902; a pinned count is always taken on the final tree), so every existing file passes. Benign collision recorded: the JSON Schema keywordpatternis also a key, so its regular-expression string is now scanned as a strict value (it cannot match the strict host-path patterns, which are anchored on a leading slash, tilde or drive letter or on a home segment; a future regular expression starting with a slash would be flagged, which is the correct side to err on). - Note on the raw-text pass (added at the plan fix). The class edit changes the structured pass only; the raw-text pass is a second scan that still reports an at-sign name as
EMAIL(R-14, operator decision OD-4). - Run (the artifact-path class has no existing victim). Every string under a key named
pathorartifactPathin any YAML or JSON file ofcontracts/, outside anx-sourceorx-derivedancestor: 0 values, 0 malformed. Underx-sourcethe keypathis a repository path (226 occurrences) and must stay on the existing scan, so the class skips anx-sourceorx-derivedancestor. - Run (the artifact-path rule as a schema pattern).
^(?!~/)(?![A-Za-z]:)(?!\.\.?(?:/|$))[^/\\\x00]+(?:/(?!\.\.?(?:/|$))[^/\\\x00]+)*$withminLength: 1,maxLength: 512equals the negation of the malformed-path predicate on 30 cases (empty, 512 and 513 characters,/abs,~/x,C:/x,c:x,a//b,a/b/,.,..,a/./b,a/../b, backslash, NUL,~x/y,~, names with a space, accents, a leading dot, an at sign or ahome/<x>/segment): 0 mismatches. The contract can therefore say the rule in the schema (examples with a malformed path fail validation) and the scanner and the reader implement the same predicate. - Mechanism for the planted negatives.
negative_cases.jsoncases of the form{"tool": "leak_scan.py", "build": "leak:<kind>", ...}callfixture_builder.build(kind, ...), which writes the plant and a clean control at run time from string fragments (no leaking text is committed).run_negative_cases.build_leakneeds no change for a new kind, but a new kind needs a new entry infixture_builder.KINDSand_plant, so the edit offixture_builder.pycoversKINDSand the plants as well asSTRICT_FIELDS(plan "Departs from the spec", item 3). The slice file set forbids new fixture files undercontracts/tools/fixtures/, so every planted negative of this Mission is either a runtime-built leak kind or an in-test case intests/contract/. - Existing tests whose pinned numbers move (edited in this Mission).
tests/contract/test_enum_pin_check.py(counts: enums=3 values=19becomesenums=7 values=43:ChangeState3,ArtifactKind12,ArtifactRefusalCode7,WorkPackageDetailRefusalCode2),tests/contract/test_fixture_builder.py(the exactKINDSset),tests/contract/test_leak_scan.py(strict-name and class cases),tests/contract/test_mission_status_examples.py(EXPECTED_PATH_KEYSand the "five paths" test,MIN_EXAMPLES, and the required-example cases for the new schemas),tests/contract/test_run_negative_cases.pyonly through the manifest length it reads.
R-9 The read-only fingerprint
- Read.
tree_fingerprint(repo_root, subpath)hashes the bytes of every filegit ls-fileslists underkitty-specs/(tracked, untracked and ignored) plusgit status --porcelain --ignored; the module-scoped autouse fixturekitty_specs_is_left_untouchedoftest_mission_status_reality.pycompares it before the first case and after the last. The new readers run inside that module, so they are covered with no further wiring. - Gap found. FR-014 says no file's content and no directory listing differs.
git ls-fileslists files only, so an empty directory created by a reader is invisible. The plan adds the directory names (a sortedos.walkof directories underkitty-specs/) toFingerprintand one planted control (a writing reader that only creates an empty directory) to the existing fingerprint tests. The reader's own opens go through the injectable file-system seam; a counting test asserts that the seam saw no write mode.
R-10 Examples and the checks that consume them
example_check.pyvalidates every example (inline or a lone$refto a file underexamples/) against the schema that lists it, in the resolved tree, with the shared date-time checker; reports an orphan file; and checks a required-example manifest (contracts/tools/fixtures/example_check/required_examples.json, outside the slice file set).tests/contract/test_mission_status_examples.pyalready validates every example through both the resolver path and areferencinglibrary path and holds the in-test required cases (for example "every refusal code has an example"); the plan extends that module instead of the manifest (OD-2).layout_check.pyfixes path-file names (contract_resolver.path_file_name: the three planned names were computed:missions_missionId_work-packages_wpId_detail.yaml,missions_missionId_artifacts.yaml,missions_missionId_artifacts_content.yaml), the_index.yamlfiles:lists and the schematitleequal to the file stem.citation_check.pyrequires onex-sourceorx-derivedper property at any depth (a citation on a referenced schema's root is inherited).provisional_check.pyrequires the marker'sopen_decisionof at least four words and the CHANGELOG naming (R-1).enum_pin_check.pyreads a pin by schematitleand failsENUM_UNREADABLEwhen a pinned title is absent, so the pin entries land in the work package that adds the enum schema.
R-11 Baselines of the gate set (run)
.venv/bin/ruff check .: all checks passed;.venv/bin/ruff format --check .: 3,103 files already formatted.- Targeted gate files (
tests/ci/test_contracts_workflows.py,test_contracts_routing.py,test_corpus_blocking_home.py;tests/architectural/test_ci_corpus_trigger_completeness.py,test_same_tier_uniqueness.py,test_no_duplicate_suite_execution.py,test_module_shard_registry.py,test_workflow_coherence.py,test_no_legacy_terminology.py): 368 passed, 1 skipped in 116 s, 0 failed. - The tool-test job's selection (
-m "corpus and not windows_ci" tests/contractwithout the three router-owned modules,-n 4 --dist loadfile): 1,016 passed, 37 skipped in 39 s, 0 failed (the skips were not itemised; the tool-dependent cases that need a JVM, vacuum or oasdiff skip on a machine without them, so the recorded skip count is the local baseline, not a CI figure). spec-kitty cutover-guard --base-ref origin/main: one Mission touched, 0 un-cut-over.- Re-run at the plan fix on the same tree (the plan commit adds only planning files):
ruff check .clean;ruff format --check .3,103 files formatted;tests/contract/test_mission_status_payloads.py157 passed; the nine named gate files 368 passed, 1 skipped in 80 s; the tool-job selection 1,016 passed, 37 skipped in 38 s; the reality module 577 passed in 45 s. Corpus counts re-taken with a plain walk: 548 Missions, 3,197 work package files, 3,193 distinct ids with 4 duplicated, 1,322 review-cycle files, 80 Missions withoutlanes.json, 0 symlinks. A plain walk of eligible files gives 15,575 against the 15,561 of R-2: the 14 more are files of this Mission's own directory added since that measurement (reviews and notes; the Mission is part of its own corpus, plan Reflexivity item 3); the floors keep their margin. - No pre-existing red was found, so no tracker issue is owed under the Pre-existing Failure Reporting Rule.
R-12 Symbols and paths cited by the plan (checked)
Every (path, symbol) below was resolved with a syntax-tree scan of the file on this checkout (0 missing): wp_task_files, WPMetadata, read_authored_wp_frontmatter, read_wp_frontmatter (src/specify_cli/status/wp_metadata.py, plus display_title as a property); AuthoredGroup, ResolvedGroup, reconstruct_wp_view (wp_view.py); normalize_authored_subtask_roster, iter_wp_section_subtask_rows (src/specify_cli/core/subtask_rows.py); dependency_readiness_for_wp (core/dependency_graph.py); build_review_cycle_pointer, validate_review_cycle_pointer, _REVIEW_CYCLE_FILE_RE (review/cycle.py); ReviewCycleArtifact (review/artifacts.py); read_lanes_json, CorruptLanesError, MissingLanesError (lanes/persistence.py); LanesManifest, lane_for_wp (lanes/models.py); lane_branch_name, PLANNING_LANE_ID (lanes/branch_naming.py); resolve_workspace_for_wp, ResolvedWorkspace with runs_in_checkout_root and exists (workspace/context.py); is_glob_pattern (ownership/validation.py); _matches_any_glob (policy/commit_guard.py); _mt_matches_owned_file, _mt_owned_file_patterns (cli/commands/agent/tasks_move_task.py); git_merge_base, merge_base_changed_files_checked (core/vcs/git.py); changed_paths, changed_entries, NameStatusEntry (src/kernel/git/listing.py); get_current_branch (core/git_ops.py); GitPath with as_posix and parse (src/kernel/git/paths.py), parse_paths_z (src/kernel/git/listing.py); ensure_within_directory (core/utils.py); FrontmatterError (specify_cli/frontmatter.py); is_rfc3339_date_time (contracts/tools/schema_formats.py); SECRET_PATTERNS, FORBIDDEN_PROPERTY_NAMES, leak_codes (contracts/tools/leak_patterns.py). The spec's line numbers for the two matchers (141 to 149 and 838 to 847) are correct on this checkout; the plan cites symbols.
R-13 Architectural job baselines, and the parity import in the tool job's environment (run, plan fix)
- Router jobs that apply to every pull request and were missing from the first gate table (read from
ci-router.yml):architectural-fast(10-minute timeout,tests/architecturalminus a few modules,-n 4, battery partfast),layer-rules(tests/architectural/test_layer_rules.pyandtest_pyproject_shape.py, 8-minute timeout),import-linter(job name "import boundaries (TID251)",ruff check --select TID251 ., 5-minute timeout, a job separate fromruff); all three areneedsofrouter-gatetogether witharchitectural-heavyandtests-contract-tools.architectural-heavyruns when the router groupsarchitectural(tests/architectural/) orci_config(.github/workflows/), among others, see a change; two shards, 30-minute timeout. - Baseline from CI (read with
gh run viewfor the last threeci-router.ymlruns onmain, runs 37189655602, 37187714056, 37186818148, all conclusion success).architectural-fast3 min 24 s, 3 min 55 s, 4 min 10 s.architectural-heavyshards 1/2 and 2/2: 10 min 07 s and 9 min 58 s (first run), skipped (second run), 7 min 02 s and 10 min 18 s (third run); durations are start to completion of the job. A run where it is skipped shows that a diff selecting none of its groups does not run it. - Baseline local (run). The eight architectural files that scan or census all of
tests/(test_no_manual_global_state_mutation.py,test_global_state_allowlist_sealed.py,test_spec_kitty_home_pin_census.py,test_spec_kitty_home_pin_guard.py,test_spec_kitty_home_pin_budget.py,test_home_pin_gate_verdict.py,test_no_dead_symbols.py,test_dead_symbol_allowlist_contract.py): 171 passed in 102 s.test_layer_rules.pyandtest_pyproject_shape.py: 83 passed in 17 s.ruff check --select TID251 .: all checks passed. No red, so no issue is owed. - The parity import under
--no-install-project(run). A scratch checkout fromgit archiveof the branch head,uv sync --frozen --no-install-project:import specify_clifails there (the project is not installed). WithPYTHONPATH=src(whatpytest.ini'spythonpath = srcprovides)specify_cli.policy.commit_guard._matches_any_globandspecify_cli.cli.commands.agent.tasks_move_task._mt_matches_owned_fileboth import (0.6 s) and the 19-case table (the 15 FR-022 glob cases and the four normalisation cases) gives 17 agreeing and exactly the two known path-normalisation divergences (./src/foo/x.pyandsrc\foo\x.pyagainstsrc/foo/**). A pytest module doing the same import, markedcorpus, passed under the tool job's selection expression in that environment (1 passed; 28 s of that is the session's isolated-install fixture). The test file lived only in the scratch checkout.
R-14 OD-4: the raw-text pass of the leak scan and a path with an at sign (run on a scratch copy of contracts/)
- Read.
leak_scan._Scanner.runcallsscan_textandscan_structuredfor each file;scan_textrunsleak_codes(line, HUMAN)over every non-blank raw line (host path, e-mail,SECRET);run_self_testbuilds everyfixture_builderkind into a temporary root and runs on everyleak_scaninvocation (the Contracts workflow, thenegative-testsjob, the router tool job). - Run (the defect). The probe name was a retina-style image path (directory
data, stemicon, an at sign, the scale2x, extension.png), assembled at run time from fragments withchr(64)for the at sign, never typed as a literal.leak_codes(<probe name>, HUMAN)returns('EMAIL',): the first alternative ofEMAIL_PATTERN(a local part, an at sign and a dotted domain) matches the stem, the at sign and2x.png. A YAML file holding that probe name underpathis reportedEMAILby the text pass and by the structured pass today. So a control or example holding such a path would fail every run throughCONTROL_FLAGGED, and a planned edit of_Scanner.walkalone does not change the text pass. A name with ahome/<x>/segment, a space or an accented letter passes both passes (the human host-path patterns need a leading slash). - Run (option X prototype: blank the artifact-path value spans before the text scan). Added to the scratch copy:
artifact_path_spans(text)(yaml.compose, scalar value start and end indexes for a keypathorartifactPathreached without anx-sourceorx-derivedkey),mask_spans, a masked argument toscan_textwithSECRETtaken from the unmasked line, and the structured artifact-path class with a stand-in predicate. Results: (1) the probe name underpath,data/home/ann/notes.mdand a flow mapping with a space and accent inpathgive no finding in text; (2) a flow-mapping line holding the probe name underpathand an address undertitlereports the title in text and in structure, not the path; (3) anx-sourcemapping whosepathholds an address is still reported in text and structure; (4) a path value holding a GitHub-token-shaped string is reportedSECRETin text; (5)path: /abs/xis reportedARTIFACT_PATH_MALFORMEDin structure; (6)leak_scan.py --root contractsover the full copy exits 0 withfiles=1009 values_strict=333 values_human=8137, the same strict and human counts as the real tree (thevalues_allcount differs only because the patched tool file is itself scanned). The real tools were not edited. - Reading of AD-19 under each option. AD-19 says a path with an at sign is listed and checked by the artifact-path rule, and that credentials and e-mail addresses inside a file name are not detected (accepted residual). X delivers the first half in the scanner and, because
SECRETstays scanned, detects more than the residual requires. Y delivers it for the reader andpayload_leaksonly. The choice is operator decision OD-4.
R-15 Which planted kinds the unchanged scanner already detects (run on the planning base)
Each plant was written to a scratch root and scanned with the unmodified leak_scan.py.
| Kind family | Result on the planning base | Consequence |
|---|---|---|
| content with a host path and an e-mail address; content with a token; title with a token | detected (HOST_PATH and EMAIL; SECRET; SECRET) | pass on the base and would pass if the FR-015 edit were reverted: regression controls, not red-first |
a leading-tilde value under id and under kind (the strict names are not yet in STRICT_FIELDS) | not detected | red-first; the tilde is a host path in the strict class only |
the twelve artifact-path forms (empty, 513 characters, absolute, tilde, drive letter, backslash, NUL, empty segment, trailing slash, . segment, .. segment, and the same malformation under the key artifactPath) | none detected | red-first |