docs(pipeline): #528 define Stage 5 checkpoint boundary + Stage 6 terminal semantics, with defrift lock (#535)

* docs(pipeline): #528 define Stage 5 checkpoint boundary + Stage 6 terminal semantics, with defrift lock

Closes the two under-specified boundaries the Mode-A replay surfaced
(items 3-4 of #528; items 1-2 shipped in #529):

Stage 5 (item 3): "Before finalization: always MANDATORY" now names
exactly one checkpoint — the entry gate between Stage 4.5 PASS and the
Stage 5 dispatch, carrying the format decisions. The in-stage content
confirmation before the final PDF is Stage 5 execution, not a pipeline
checkpoint; the Stage 5 completion checkpoint (before Stage 6) is FULL —
never SLIM — but not MANDATORY. Authority section in
pipeline_state_machine.md, mirrored in SKILL.md + orchestrator.

Stage 6 (item 4): the state machine previously ended at Stage 5 -> END
with no Stage 6 at all. It now defines the Stage 5 -> 6 transition, the
decline path (Stage 6 is non-mandatory: declining marks it skipped and
the pipeline still terminates completed), the terminal checkpoint, and
the canonical acknowledgement vocabulary (finish / end / done / confirm,
or an unambiguous natural-language equivalent) whose acceptance sets the
pipeline global state to completed. process_summary_protocol.md gains
the terminal step; the orchestrator gains the state_tracker wiring.

All derived from existing text — no architecture change, no checkpoint
relaxed (both #528 non-goals hold).

New scripts/check_pipeline_boundary_semantics.py pins all four #528
resolutions (the two #529 fixes had no lint either) with 21 mutation
tests, wired into spec-consistency.yml + the unified pytest manifest.
Also backfills the #529 CHANGELOG entry the changelog-covers-merges gate
required before the next tag, and the CHANGELOG entry for this PR.

Full pytest: 3244 passed, 3 skipped, 1 xfailed. Closes #528.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-1 closures — entry-gate scope + full-row pins + adverse-value witnesses

P1-1: the entry gate now records only the finalization-format decision
(citation style, matching the reset machinery's pending_decision); the
LaTeX question and content confirmation are explicitly in-stage Stage 5
execution — removes the double-prompt contradiction with the Stage 5
output process on SKILL.md / orchestrator.

P1-2: transition rows are pinned as complete rows (all four cells),
scoped to the ## Legal State Transitions span; authority section gains
the "not on the MANDATORY list" literal; the protocol pins the
natural-language-equivalent clause. Four adverse-value mutation tests
added (FULL→MANDATORY flip, clause deletion, keyword-only narrowing,
completed→skipped flip); 25 tests total.

P2: decline branch added to the ASCII diagram; dependency-matrix END
terminal renamed to User (delivery); row pins section-scoped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-2 closures — pin gate scope, non-ack classification, wiring pairs

Three lock-coverage gaps (prose was already consistent; the lint could
not see it drift): (1) citation-style-as-sole-gate-decision + the
"Need LaTeX?" in-stage clause pinned in the authority section and the
rule-5 mirrors; (2) the change-requests-are-not-acknowledgements rule
pinned on authority + SKILL.md + protocol; (3) the orchestrator
state_tracker wiring pinned as complete acknowledgement/decline action
pairs instead of a bare update_pipeline_state("completed"). Five new
adverse-value witnesses; 30 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-3 closures — Stage 6 tracker contract + FULL/MANDATORY collision + gate-decision consumption + decline pins

(1) state_tracker_agent.md: stage_id enum extends to "6", stages example
gains the Stage 6 entry, and the terminal/decline call shapes are
documented as contract-valid 3-arg calls; the orchestrator wiring now
uses update_stage("6", ...) with an explicit decline details argument.
(2) The FULL checkpoint-type row claimed "before finalization" while the
MANDATORY row named the same boundary (pre-existing collision this PR's
entry-gate definition exposed) — FULL now owns the Stage 5 COMPLETION
checkpoint on both mirrors. (3) SKILL Stage 5 execution Step 1 consumes
the entry-gate citation-style decision, asking only when no gate
decision exists (direct format-convert / mid-entry). (4) Decline
semantics pinned per mirror (SKILL Stage 6 section scoped via
check_section_literals, orchestrator handoff row, protocol trigger) +
rule-10 terminal pin, closing the two-copies drift gap. Five new
adverse-value witnesses; 35 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-4 closures — observer ordering + MANDATORY-type pin + outcome pins

(1) The whole-pipeline collaboration_depth observer pass is now
explicitly dispatched DURING Stage 6 record compilation, before the
Process Record is generated and delivered — the previous "at Stage 6
completion" wording could not coexist with the #528 terminal semantics
(Stage 6 completes only after the delivered record is acknowledged);
protocol Workflow gains step 2b. (2) The entry gate's MANDATORY
classification is pinned as the complete checkpoint-type row and the
type-bearing rule-5 clause, so neither declaration can flip to FULL
independently. (3) The acknowledgement outcome sentence is pinned on
every operative copy (state machine authority, SKILL Stage 6 block,
protocol step 5 reflowed to one line) and the tracker's terminal /
decline action pairs are pinned. Five new adverse-value witnesses;
38 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-5 closures — observer timing sweep + routing/rule-9 pins

(1) The remaining "at pipeline completion / after Stage 6" observer
timing surfaces (SKILL agent table + dispatch list + observer section +
overview bullet, collaboration_depth_agent persona + range table,
state_tracker dialogue_log_ref + observer_reports + append signature,
orchestrator sampling note) all now say the whole-pipeline pass runs
during Stage 6 record compilation, before the Process Record is
delivered — consistent with the #528 terminal semantics. (2) The Stage
3' Accept|Minor -> Stage 4.5 routing declarations are pinned on SKILL
rule 6 and the state-machine transition row (invariant 2 previously
pinned only the coaching exclusions). (3) SKILL rule 9's completion
type + decline outcome is pinned. Three new adverse-value witnesses;
41 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-6 closures — affirmative coaching predicates + mirror type pins + sequencing pins

(1) The affirmative Stage 3' Major-only coaching predicates are pinned
(the operative trigger clause + the handoff table's (if Major)
qualifier) — the negative exclusions alone let the trigger widen back
to Minor/Major while staying green. (2) The type-bearing Stage 5
completion trigger is pinned on both mirrors (orchestrator Stage 5->6
handoff row, protocol trigger line) so a (FULL)->(MANDATORY) flip
fires. (3) Delivery-before-acknowledgement sequencing is pinned on the
orchestrator wiring line, the protocol step 5, and the state-machine
authority. Three new adverse-value witness groups; 44 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-7 closures — authority full-sentence, diagram-edge, handoff-line, step-5 pins

Pins the last operative restatements of the #528 resolutions: (1) the
authority section's no-auto-advance bullet, non-mandatory declaration,
and completion-is-the-terminal-checkpoint sentence as complete
sentences; (2) the ASCII diagram's Stage 3' Accept/Minor branch and the
terminal-acknowledgement / decline edges (diagram-section scoped);
(3) SKILL Step 4's Stage 5->6 handoff line with its decline option;
(4) the protocol's step-5 terminal-checkpoint header and the
no-next-stage sentence. Seven new adverse-value witnesses; 51 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-8 closures — tracker prerequisites + Stage 6 skippable declaration

(1) check_prerequisites' Stage 2 row now recommends the Methodology
Blueprint like every other Stage 1->2 surface (the material-gap warning
consumer silently accepted its absence), and the table gains a Stage 6
target row. (2) Stage 6 joins the orchestrator's explicitly-skippable
list with its decline scope — the skip-command validator only honors
listed stages, so the pinned decline path was unexecutable. Both pinned
with adverse-value witnesses; 54 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-9 closures — engagement FULL exception + paired skip pins + full SSOT block pin

(1) The engagement-tracking SLIM downgrade (continue-count >= 2 / >= 4)
now carries an explicit FULL-checkpoint exception so it cannot swallow
the Stage 5 completion checkpoint. (2) Both sides of the skip
classification are pinned as complete lines — adding Stage 6 to the
non-skippable side now fires. (3) The tracker's Stage 6 SSOT block is
pinned verbatim (16 lines) so a load-bearing field flip
(approval_gate) fires. Three new adverse-value witnesses; 57 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): line-anchor the list-line pins (append-exploit closure)

The round-9 non-skippable witness exposed that substring pins on list
lines are append-exploitable: adding ", Stage 6" after "Stage 5
(finalize)" keeps the pinned text as a prefix and stays green. New
_line_pinned helper (whitespace-stripped full-line equality) applied to
the three list-line pins (non-skippable list, SKILL rule 6 routing,
SKILL Step 4 handoff). 57/57 witnesses green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-10 closure — pin the tracker's accepted-value declarations

The terminal wiring calls were pinned but the enum values they depend
on were not: a `skipped`→`declined` rename in update_stage's status
enum, or `completed`→`finished` in update_pipeline_state's legal
values, would make the named consumer reject the pinned Stage 6
decline / terminal transition while staying green. Both declarations
pinned (line-anchored) with witnesses; 59 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-11 closures + whole-file content locks (endgame)

Round-11 P1 closures: the reset-boundary iron rule scopes its MANDATORY
list to the Stage 5 entry gate; the decline pins now include the
type-bearing "Stage 6 is non-mandatory" declaration on SKILL + protocol;
the skippable line is pinned complete with its label (line-anchored);
the Stage 5 -> 6 handoff row is pinned as the complete line, first cell
included.

Endgame mechanism: eleven review rounds showed sentence-level pins on a
state-machine doc cannot converge — every round found another single
green mutation. The two #528-central reference docs
(pipeline_state_machine.md, process_summary_protocol.md) are entirely
operative, so they now carry bibliography_agent-style whole-file sha256
content locks: any byte change fails CI until the pinned hash is
updated in the same commit, making every future single-edit mutation of
those docs visible at review time by construction. Multi-concern files
(SKILL/orchestrator/tracker) keep sentence pins. 66 tests total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* fix(review): codex round-12 closures + content locks extended to all five surfaces

Round-12 pins: the Step 1 consume clause now includes its
ask-only-when-absent fallback; the overview summary's
delivery-before-acknowledgement ordering is pinned; the reset-boundary
iron rule's entry-gate scoping is pinned; the decline wiring pins now
include their if-predicates on both orchestrator and tracker.

Endgame completed: CONTENT_LOCKS now covers all five #528 surfaces
(SKILL.md, orchestrator, state_tracker, state machine, protocol) with
whole-file sha256 — twelve review rounds showed the single-edit
mutation class is inexhaustible under sentence pins alone; the hash
lock terminates it by construction (same F2-lock convention as
bibliography_agent: any edit updates the pinned hash in the same
commit, making the change a deliberate, reviewable act). Sentence pins
remain as targeted diagnostics. 66 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

* docs(changelog): #528 reflect the final closure scope in the Unreleased entry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013y1bcrbtdSmARbBRmA8LHq

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Edward Cheng-I Wu
2026-07-16 12:05:52 +08:00
committed by GitHub
parent 22034f7497
commit d8c0f43304
11 changed files with 1669 additions and 35 deletions
+12
View File
@@ -367,6 +367,18 @@ jobs:
PYTHONPATH: .
run: python scripts/check_439_format_profile.py
- name: Run #528 pipeline boundary-semantics defrift lock
# Pins the four Mode-A-replay resolutions (#529 items 1-2 + #528
# items 3-4) across academic-pipeline SKILL.md / orchestrator /
# state machine / process_summary_protocol: Methodology Blueprint
# in the Stage 1->2 handoff, Stage 3' Minor never triggers
# coaching, the Stage 5 MANDATORY boundary is the entry gate
# (completion checkpoint FULL — never SLIM), and the Stage 6
# terminal-acknowledgement semantics. The pytest companion
# `test_check_pipeline_boundary_semantics.py` runs via the
# unified manifest (#156).
run: python3 scripts/check_pipeline_boundary_semantics.py
- name: Run v3.9.2 Phase Boundary coverage lint (#133)
# Enforces 22 Bucket A agents have ## Phase Boundary (v3.9.2)
# block, 16 Bucket B/C/D agents DON'T, and each Bucket A block
+3
View File
File diff suppressed because one or more lines are too long
+14 -11
View File
@@ -30,7 +30,7 @@ A lightweight orchestrator that manages the complete academic pipeline from rese
3. **Two-stage review** — First full review + post-revision focused verification review
4. **Final integrity check** — After revision completion, re-verify all citations and data are 100% correct
5. **Reproducible** — Standardized workflow producing consistent quality assurance each time
6. **Process documentation**After pipeline completion, automatically generates a "Paper Creation Process Record" PDF documenting the human-AI collaboration history
6. **Process documentation**Stage 6 generates a "Paper Creation Process Record" PDF documenting the human-AI collaboration history (delivered before the terminal acknowledgement that completes the pipeline)
## Quick Start
@@ -132,8 +132,8 @@ This mirrors PaperOrchestra's parallel execution of Plot Generation (Step 2) and
6. **Stage 3' RE-REVIEW** -> Accept|Minor -> Stage 4.5 / Major -> Stage 4'
7. **Stage 4' RE-REVISE** -> user confirmation -> Stage 4.5 (no return to review)
8. **Stage 4.5 FINAL INTEGRITY** -> PASS (zero issues) -> Stage 5 (FAIL -> fix and re-verify)
9. **Stage 5 FINALIZE** -> MD -> DOCX via Pandoc when available (otherwise instructions) -> ask about LaTeX -> confirm -> PDF -> Stage 6
10. **Stage 6 PROCESS SUMMARY** -> ask language version -> generate process record MD -> LaTeX -> PDF -> end
9. **Stage 5 FINALIZE** -> MD -> DOCX via Pandoc when available (otherwise instructions) -> ask about LaTeX -> confirm -> PDF -> completion checkpoint (FULL) -> Stage 6 (user may decline Stage 6: marked `skipped`, pipeline goes directly to `completed`)
10. **Stage 6 PROCESS SUMMARY** -> ask language version -> generate process record MD -> LaTeX -> PDF -> terminal acknowledgement (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) -> pipeline global state `completed`
See `references/pipeline_state_machine.md` for complete state transition definitions.
@@ -147,9 +147,9 @@ See `references/pipeline_state_machine.md` for complete state transition definit
| Type | When Used | Content |
|------|-----------|---------|
| FULL | First checkpoint; after integrity boundaries; before finalization | Full deliverables list + decision dashboard + all options |
| FULL | First checkpoint; after integrity boundaries; Stage 5 completion (final-deliverable acceptance) | Full deliverables list + decision dashboard + all options |
| SLIM | After 2+ consecutive "continue" responses on non-critical stages | One-line status + explicit continue/pause prompt |
| MANDATORY | Integrity FAIL; Review decision; Stage 5 | Cannot be skipped; requires explicit user input |
| MANDATORY | Integrity FAIL; Review decision; Stage 5 entry gate (before finalization) | Cannot be skipped; requires explicit user input |
### Decision Dashboard (shown at FULL checkpoints)
@@ -181,7 +181,7 @@ Ready to proceed to Stage [Y]? You can also:
2. **After 2+ consecutive "continue" without review**: prompt user awareness ("You've continued [N] times in a row. Want to review progress?")
3. **Integrity boundaries (Stage 2.5, 4.5)**: always MANDATORY
4. **Review decisions (Stage 3, 3')**: always MANDATORY
5. **Before finalization (Stage 5)**: always MANDATORY
5. **Before finalization (Stage 5 entry gate)**: always MANDATORY — this is the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch, where the user explicitly confirms proceeding and makes the finalization-format decision (citation style); the in-stage LaTeX question and content confirmation stay inside Stage 5 execution. The Stage 5 completion checkpoint (Final Paper delivered, before Stage 6) is FULL — never SLIM. See `references/pipeline_state_machine.md` § Stage 5 boundary semantics
6. **All other stages**: start FULL, downgrade to SLIM if user says "just continue"
### Checkpoint Rules
@@ -213,7 +213,7 @@ If ANY answer raises concern, include it in the checkpoint presentation to the u
| 1 | `pipeline_orchestrator_agent` | Main orchestrator: detects stage, recommends mode, triggers skill, manages transitions | `agents/pipeline_orchestrator_agent.md` |
| 2 | `state_tracker_agent` | State tracker: records completed stages, produced materials, revision loop count | `agents/state_tracker_agent.md` |
| 3 | `integrity_verification_agent` | Integrity verifier: 100% reference/citation/data verification (blocking) | `agents/integrity_verification_agent.md` |
| 4 | `collaboration_depth_agent` | **Observer (advisory only — never blocks).** Reads dialogue log and scores user-AI collaboration pattern against `shared/collaboration_depth_rubric.md`. Invoked at FULL/SLIM checkpoints and at pipeline completion. Based on Wang & Zhang (2026). | `agents/collaboration_depth_agent.md` |
| 4 | `collaboration_depth_agent` | **Observer (advisory only — never blocks).** Reads dialogue log and scores user-AI collaboration pattern against `shared/collaboration_depth_rubric.md`. Invoked at FULL/SLIM checkpoints and during Stage 6 record compilation (whole-pipeline pass, before the Process Record is delivered). Based on Wang & Zhang (2026). | `agents/collaboration_depth_agent.md` |
| 5 | `claim_ref_alignment_audit_agent` | **Opt-in claim faithfulness auditor (v3.8 #103).** Audits sampled citations for claim ↔ reference alignment + negative-constraint compliance; emits per-claim `claim_audit_results[]`, `claim_drift[]`, `uncited_assertions[]`, `constraint_violations[]`. Dispatched via orchestrator §3.6 when claim_audit mode is requested. | `agents/claim_ref_alignment_audit_agent.md` |
---
@@ -285,6 +285,7 @@ After user confirmation:
- Stage 3' --> 4': Pass new Revision Roadmap + R&R Traceability Matrix (Schema 11) to academic-paper revision mode
- Stage 4/4' --> 4.5: Pass revision-completed paper to integrity_verification_agent (final verification)
- Stage 4.5 --> 5: Pass verified final draft to format-convert mode
- Stage 5 --> 6: Pass final deliverables list + pipeline state history to Process Summary (user may decline Stage 6 at the Stage 5 completion checkpoint)
3. Begin next stage
```
@@ -323,7 +324,7 @@ In Mode B, **single-phase agents (Bucket A per `docs/design/2026-05-18-ars-v3.9.
- `pipeline_orchestrator_agent` (D — orchestrator, full pipeline visibility)
- `state_tracker_agent` (D — meta state, all phases)
- `integrity_verification_agent` (C — Stage 2.5 / 4.5 cross-skill gate)
- `collaboration_depth_agent` (C — FULL/SLIM checkpoints + pipeline completion, advisory-only)
- `collaboration_depth_agent` (C — FULL/SLIM checkpoints + Stage 6 record compilation, advisory-only)
- `claim_ref_alignment_audit_agent` (C — opt-in claim audit, phase-orthogonal)
Routing into Mode B requires explicit user signal — `/ars-<mode>` slash command or `[direct-mode]` prefix. Ambiguous cross-phase input defaults to clarification per `.claude/CLAUDE.md` Routing Discipline + `shared/references/intent_clarification_protocol.md`. **Critically:** if `pipeline_orchestrator_agent` is dispatched on ambiguous cross-phase materials, the orchestrator itself currently cannot reconcile (this is the v3.10 conductor #134 work) — v3.9.2 routes such cases to clarification BEFORE the orchestrator runs.
@@ -418,6 +419,8 @@ Every pipeline artifact is versioned, hashed, and auditable.
Produces the final process record: paper creation journey, collaboration quality evaluation (6 dimensions, 1-100), and AI self-reflection report.
**Terminal semantics (#528)**: Stage 6 is non-mandatory — the user may decline it at the Stage 5 completion checkpoint (Stage 6 marked `skipped`; the pipeline still terminates `completed`). When it runs, after the process record is delivered the orchestrator prompts for a terminal acknowledgement — `finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent that accepts the deliverables. On acknowledgement, Stage 6 is marked `completed` and the pipeline global state is set to `completed`; change requests (the other language version, content corrections) keep Stage 6 `in_progress` and are not acknowledgements. See `references/pipeline_state_machine.md` § Stage 6 terminal semantics.
> See `references/process_summary_protocol.md` for full workflow, required content structure, scoring dimensions, and output specifications.
---
@@ -426,9 +429,9 @@ Produces the final process record: paper creation journey, collaboration quality
The `collaboration_depth_agent` observes the user's collaboration pattern with the pipeline. It is **advisory only** and **never blocks** progression at any checkpoint. It is `non-blocking` by design and carries `blocking: false` in its frontmatter as a structural guarantee.
**When invoked**: every FULL checkpoint, every SLIM checkpoint, and after Stage 6 (pipeline completion). MANDATORY checkpoints (Stages 2.5 / 4.5 integrity gates) **do not** invoke the observer — those are integrity concerns and must not be diluted.
**When invoked**: every FULL checkpoint, every SLIM checkpoint, and during Stage 6 record compilation (the whole-pipeline pass runs before the Process Record is generated and delivered, so its output can be a chapter of the record the user acknowledges). MANDATORY checkpoints (Stages 2.5 / 4.5 integrity gates) **do not** invoke the observer — those are integrity concerns and must not be diluted.
**What it does**: reads the dialogue range for the just-completed stage (at checkpoints) or the whole pipeline (at completion), scores the pattern against the canonical rubric at `shared/collaboration_depth_rubric.md`, and emits an advisory block/chapter. Dimensions: Delegation Intensity, Cognitive Vigilance, Cognitive Reallocation, Zone Classification (Zone 1 / Zone 2 / Zone 3). Rubric is based on Wang & Zhang (2026) IJETHE 23:11 (DOI 10.1186/s41239-026-00585-x).
**What it does**: reads the dialogue range for the just-completed stage (at checkpoints) or the whole pipeline (during Stage 6 record compilation), scores the pattern against the canonical rubric at `shared/collaboration_depth_rubric.md`, and emits an advisory block/chapter. Dimensions: Delegation Intensity, Cognitive Vigilance, Cognitive Reallocation, Zone Classification (Zone 1 / Zone 2 / Zone 3). Rubric is based on Wang & Zhang (2026) IJETHE 23:11 (DOI 10.1186/s41239-026-00585-x).
**Distinction from existing mechanisms**:
@@ -588,7 +591,7 @@ Stage 3': academic-paper-reviewer
Stage 4/4': academic-paper (revision mode)
Stage 5: academic-paper (format-convert mode)
- Step 1: Ask user which academic formatting style (APA 7.0 / Chicago / IEEE, etc.)
- Step 1: Consume the citation-style decision recorded at the Stage 5 entry gate; ask which academic formatting style (APA 7.0 / Chicago / IEEE, etc.) only when no gate decision exists (direct format-convert / mid-entry invocation)
- Step 2: Produce MD, then generate DOCX via Pandoc when available (otherwise provide conversion instructions)
- Step 3: Produce LaTeX (using corresponding document class, e.g., apa7 class for APA 7.0)
- Step 4: After user confirms content is correct, tectonic compiles PDF (final version)
@@ -20,7 +20,7 @@ version: "1.0.0"
## Role Definition
You are a post-hoc **observer** of the user's collaboration pattern with the ARS pipeline. You do not participate in research, writing, review, or orchestration. You read the dialogue log for a just-completed stage (or the whole pipeline at completion) and produce a **short, descriptive, advisory-only** report scoring the user's collaboration depth against the canonical rubric at `shared/collaboration_depth_rubric.md`.
You are a post-hoc **observer** of the user's collaboration pattern with the ARS pipeline. You do not participate in research, writing, review, or orchestration. You read the dialogue log for a just-completed stage (or the whole pipeline during Stage 6 record compilation) and produce a **short, descriptive, advisory-only** report scoring the user's collaboration depth against the canonical rubric at `shared/collaboration_depth_rubric.md`.
**You never block progression.** Your output is a separate section in the checkpoint presentation and a chapter in the Process Record. The orchestrator's `Ready to proceed?` prompt ignores your report. If a user wants to ignore this report entirely, that is a valid choice and your output must not hint otherwise.
@@ -47,7 +47,7 @@ You are invoked by `pipeline_orchestrator_agent` at three moments:
|---|---|---|
| FULL checkpoint (after each stage) | Turns within the just-completed stage | Named section in checkpoint presentation |
| SLIM checkpoint (after each stage) | Turns within the just-completed stage | Named section in checkpoint presentation (brief) |
| Pipeline completion (after Stage 6) | All turns, whole pipeline | New chapter in Process Record: "Collaboration Depth Trajectory" |
| Stage 6 record compilation (whole-pipeline pass, before the Process Record is delivered) | All turns, whole pipeline | New chapter in Process Record: "Collaboration Depth Trajectory" |
The orchestrator passes you a `dialogue_log_ref` (turn range, e.g. `turns #47..#91`). Read those turns from the live conversation history. Do not accept summaries — read raw turns.
@@ -120,9 +120,9 @@ You can adjust any stage's mode at any time. Ready to begin?
| Type | When Used | Content |
|------|-----------|---------|
| FULL | First checkpoint; after integrity boundaries; before finalization | Full deliverables list + decision dashboard + all options |
| FULL | First checkpoint; after integrity boundaries; Stage 5 completion (final-deliverable acceptance) | Full deliverables list + decision dashboard + all options |
| SLIM | After 2+ consecutive "continue" responses on non-critical stages | One-line status + explicit continue/pause prompt |
| MANDATORY | Integrity FAIL; Review decision; Stage 5 | Cannot be skipped; requires explicit user input |
| MANDATORY | Integrity FAIL; Review decision; Stage 5 entry gate (before finalization) | Cannot be skipped; requires explicit user input |
#### Checkpoint Type Rules
@@ -130,7 +130,7 @@ You can adjust any stage's mode at any time. Ready to begin?
2. After 2+ consecutive "continue" without reviewing deliverables: switch to SLIM and prompt user awareness ("You've continued 3 times in a row. Want to review progress?")
3. Integrity boundaries (Stage 2.5, 4.5): always MANDATORY
4. Review decisions (Stage 3, 3'): always MANDATORY
5. Before finalization (Stage 5): always MANDATORY
5. Before finalization (Stage 5 entry gate): always MANDATORY — this is the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch, where the user explicitly confirms proceeding and makes the finalization-format decision (citation style); the in-stage LaTeX question and content confirmation stay inside Stage 5 execution. The Stage 5 completion checkpoint (Final Paper delivered, before Stage 6) is FULL — never SLIM. See `../references/pipeline_state_machine.md` § Stage 5 boundary semantics
6. All other stages: start FULL, downgrade to SLIM if user says "just continue"
#### User Engagement Tracking
@@ -142,8 +142,8 @@ consecutive_continue_count: integer (reset to 0 when user chooses any action oth
```
- `consecutive_continue_count < 2` -> FULL checkpoint (unless rules above override)
- `consecutive_continue_count >= 2` -> SLIM checkpoint (unless rules above override to MANDATORY)
- `consecutive_continue_count >= 4` -> SLIM + awareness prompt ("You've continued [N] times in a row...")
- `consecutive_continue_count >= 2` -> SLIM checkpoint (unless rules above override to MANDATORY, or the checkpoint is one the rules pin to FULL — the Stage 5 completion checkpoint is FULL — never SLIM, regardless of the continue count)
- `consecutive_continue_count >= 4` -> SLIM + awareness prompt ("You've continued [N] times in a row..."); the FULL-pinned checkpoints above still render FULL
#### Steps
@@ -203,7 +203,7 @@ SLIM checkpoints never reset. MANDATORY checkpoints co-occur with reset when app
4. The `[PASSPORT-RESET: ...]` tag is the sole machine-stable handoff anchor. The `### Resume Instruction` subsection is for user ergonomics.
5. Hash mismatch on `resume_from_passport=<hash>` is a hard error; orchestrator refuses to proceed.
6. A `boundary` is consumed only by appending a `kind: resume` entry with matching `consumes_hash`. Double-resume (second resume of an already-consumed boundary) is a hard error.
7. MANDATORY checkpoints (Stage 2.5 / 4.5, review decisions, Stage 5) remain MANDATORY even when reset co-occurs. Integrity gates are never diluted. If the boundary carries `pending_decision`, resume must re-prompt the user; `next` is advisory. Actual routing comes from the matched option's `next_stage`/`next_mode`, not from the boundary `next` field.
7. MANDATORY checkpoints (Stage 2.5 / 4.5, review decisions, the Stage 5 entry gate) remain MANDATORY even when reset co-occurs. Integrity gates are never diluted. If the boundary carries `pending_decision`, resume must re-prompt the user; `next` is advisory. Actual routing comes from the matched option's `next_stage`/`next_mode`, not from the boundary `next` field.
8. `collaboration_depth_agent` observer fires on FULL checkpoints as before; its output is included in the checkpoint notification regardless of reset state. Observer state does NOT cross reset boundaries.
9. Resume consumption MUST hold an exclusive advisory lock on the passport file for the entire read-check-append sequence (acquire the lock on the "Acquire passport lock" obligation, hold across the read-ledger, no-prior-resume check, and resume-entry append steps, release only after the append is durable). Releasing the lock between the no-prior-resume check and the resume-entry append reopens the double-resume race this rule exists to prevent. Non-POSIX implementations that cannot provide OS-level exclusion MUST refuse to resume rather than degrade silently (fail with an explicit error surfaced to the user). See §"Concurrency model" in the protocol doc.
@@ -320,7 +320,7 @@ Users respond to checkpoint prompts with one of these commands. The orchestrator
| `abort` / `terminate` | Terminate pipeline entirely | `pipeline_state` = `aborted`; save all materials with current versions |
**Skippable vs Non-Skippable Stages**:
- Skippable: Stage 1 (deep-research, if user provides own bibliography), Stage 3' (re-review, if only minor revisions), Stage 4' (re-revise, if accepted)
- Skippable: Stage 1 (deep-research, if user provides own bibliography), Stage 3' (re-review, if only minor revisions), Stage 4' (re-revise, if accepted), Stage 6 (process summary — declined at the Stage 5 completion checkpoint; marked `skipped`, pipeline still terminates `completed`)
- Non-Skippable: Stage 2 (writing), Stage 2.5 (pre-review integrity), Stage 3 (initial review), Stage 4.5 (final integrity), Stage 5 (finalize)
### Mode Switching Rules
@@ -357,18 +357,18 @@ When a sub-skill stage fails or produces unacceptable output:
### Collaboration Depth Observer (advisory, never blocks)
**When.** At every FULL checkpoint, every SLIM checkpoint, and after Stage 6 (pipeline completion). This is an **observer** agent — it reads the just-completed dialogue range (per-stage) or the whole pipeline log (at completion), scores the user-AI collaboration pattern against `shared/collaboration_depth_rubric.md`, and emits a short advisory report. It is **not** in the blocking path; the orchestrator's progression decision ignores its output.
**When.** At every FULL checkpoint, every SLIM checkpoint, and during Stage 6 record compilation (the whole-pipeline pass, before the Process Record is delivered). This is an **observer** agent — it reads the just-completed dialogue range (per-stage) or the whole pipeline log (at completion), scores the user-AI collaboration pattern against `shared/collaboration_depth_rubric.md`, and emits a short advisory report. It is **not** in the blocking path; the orchestrator's progression decision ignores its output.
**How the orchestrator invokes it.**
1. At checkpoint step 3 (above), after updating `state_tracker` with the new checkpoint, derive the stage's `dialogue_log_ref` (turn range covering only the just-completed stage; see `state_tracker_agent.md`).
2. **Short-stage guard**: if the stage's user-turn count is less than 5, skip the dispatch and inject a static `Collaboration Depth: insufficient_evidence (stage had N user turns; rubric needs ≥5)` block. This avoids a full-model call just to receive the agent's own `insufficient_evidence` answer.
3. Otherwise, dispatch `collaboration_depth_agent` with the range pointer. It reads live conversation turns — **do not** pass a summary.
4. Receive its Markdown block and inject it as a named section into the checkpoint template (FULL: full block; SLIM: one-line compact; MANDATORY: omit — MANDATORY checkpoints are integrity gates and must not be diluted).
5. At Stage 6 completion, dispatch the observer a second time in **whole-pipeline mode** (range = all stages). Its output becomes a new chapter, "Collaboration Depth Trajectory", in the Process Record, **separate from** the existing 6-dimension Collaboration Quality Evaluation (which is AI self-reflection; the observer is about the user's collaboration pattern).
5. During Stage 6 compilation — after the dialogue review (Process Summary Workflow step 2) and BEFORE the Process Record is generated and delivered (steps 3-5), so its output can be a chapter of the record the user acknowledges — dispatch the observer a second time in **whole-pipeline mode** (range = all stages). Its output becomes a new chapter, "Collaboration Depth Trajectory", in the Process Record, **separate from** the existing 6-dimension Collaboration Quality Evaluation (which is AI self-reflection; the observer is about the user's collaboration pattern).
**Cross-model cost and behaviour.** When `ARS_CROSS_MODEL` is set, do not re-dispatch automatically. The secondary-model invocation reads raw dialogue turns that may contain the user's private reasoning and unpublished material, so apply the consent gate first: ask for explicit user consent (if not already granted in this session) and identify the external provider, model, and content class (raw dialogue turns) that would be sent. The environment variable alone is not consent to upload that material. If consent is not granted, log `[CROSS-MODEL-SKIPPED]`, run only the primary-model observer, and append no `cross_model_divergence` block. If consent is granted, re-dispatch `collaboration_depth_agent` on the secondary model; if any dimension score diverges by > 2 points between primary and secondary, append a `cross_model_divergence` block to the checkpoint section. **Never silently average cross-model scores.** The gate gates only the upload — the observer's advisory-only, non-blocking role is unchanged. See `shared/cross_model_verification.md` for the consent boundary.
The cost is multiplicative: a 10-stage pipeline with cross-model enabled produces up to ~20 observer invocations (10 primary + 10 secondary) on top of primary pipeline work. Users willing to trade coverage for cost may set `ARS_CROSS_MODEL_SAMPLE_INTERVAL=N` (default `1` = every checkpoint; `3` = every third, plus always at pipeline completion). The short-stage guard above also applies per-model, so empty stages incur no cross-model cost.
The cost is multiplicative: a 10-stage pipeline with cross-model enabled produces up to ~20 observer invocations (10 primary + 10 secondary) on top of primary pipeline work. Users willing to trade coverage for cost may set `ARS_CROSS_MODEL_SAMPLE_INTERVAL=N` (default `1` = every checkpoint; `3` = every third, plus always at the Stage 6 whole-pipeline pass). The short-stage guard above also applies per-model, so empty stages incur no cross-model cost.
**Non-blocking guarantees** (orchestrator-level discipline):
- The observer's output never appears in the "Flagged" line (that line is reserved for integrity and metric issues).
@@ -508,6 +508,7 @@ Reference helper: `scripts/slr_lineage.py` `emit(stages, incoming_slr_lineage)`.
| Stage 3' -> **coaching** -> 4' | New Revision Roadmap (if Major) | Schema 7 (Revision Roadmap) | **First Socratic dialogue** -> academic-paper revision mode input |
| Stage 4/4' -> 4.5 | Revised/Re-Revised Draft | Schema 4 (revised) | Pass to integrity_verification_agent (final verification) |
| Stage 4.5 -> 5 | Final Verified Draft + Final Integrity Report | Schema 4 + Schema 5 (Integrity Report) | Produce MD -> DOCX via Pandoc when available (otherwise instructions) -> ask about LaTeX -> confirm -> PDF. Carry forward `experiment_alignment_results[]` + `experiment_intake_declaration` (#260) to formatter surface + Stage 6 histogram |
| Stage 5 -> 6 | Final deliverables list + pipeline state history (state_tracker JSON, agent logs) | — (Process Record; no numbered schema) | Dispatched only after the user confirms the Stage 5 completion checkpoint (FULL). User may decline Stage 6 there: mark it `skipped`, set pipeline state `completed`. Protocol: `../references/process_summary_protocol.md`; terminal semantics: `../references/pipeline_state_machine.md` § Stage 6 terminal semantics |
**All artifacts must carry a Material Passport (Schema 9)** with `origin_skill`, `origin_mode`, `origin_date`, `verification_status`, and `version_label`. From v3.7.4+, the passport also carries the run-level `slr_lineage` boolean computed per the emission step above.
@@ -568,6 +569,7 @@ Notify state_tracker_agent to update state whenever a stage begins or completes:
- Checkpoint passed: `update_pipeline_state("running")`
- Material produced: `update_material(material_name, true)`
- Integrity check result: `update_integrity(stage_id, verdict, details)`
- Pipeline terminal transition: on the Stage 6 terminal acknowledgement (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) — `update_stage("6", "completed", outputs)` + `update_pipeline_state("completed")`; if the user declined Stage 6 at the Stage 5 completion checkpoint — `update_stage("6", "skipped", {reason: "user declined Stage 6"})` + `update_pipeline_state("completed")`
Request state_tracker_agent to produce the Progress Dashboard when needed.
@@ -25,11 +25,11 @@ The State Tracker is the **single source of truth** for pipeline state. No other
### Dialogue log references (v3.3.0)
For every stage transition, the tracker records a `dialogue_log_ref` containing the turn range covering that stage (e.g. `turns #47..#91`). This is a lightweight pointer — the full dialogue lives in the live conversation, not in state. The pointer is passed to `collaboration_depth_agent` when the orchestrator invokes it at checkpoints and at pipeline completion. Turn-range entries are immutable once a stage closes.
For every stage transition, the tracker records a `dialogue_log_ref` containing the turn range covering that stage (e.g. `turns #47..#91`). This is a lightweight pointer — the full dialogue lives in the live conversation, not in state. The pointer is passed to `collaboration_depth_agent` when the orchestrator invokes it at checkpoints and during Stage 6 record compilation (the whole-pipeline pass). Turn-range entries are immutable once a stage closes.
### `collaboration_depth_history[]`
Append-only list. Each entry is an observer report produced at a FULL/SLIM checkpoint or at pipeline completion. Entries never gate state transitions — they are stored for the final Process Record's "Collaboration Depth Trajectory" chapter only. The tracker must reject any write request that attempts to turn observer output into a blocking condition.
Append-only list. Each entry is an observer report produced at a FULL/SLIM checkpoint or during Stage 6 record compilation (the whole-pipeline pass). Entries never gate state transitions — they are stored for the final Process Record's "Collaboration Depth Trajectory" chapter only. The tracker must reject any write request that attempts to turn observer output into a blocking condition.
### State Update Protocol
@@ -196,6 +196,21 @@ Every material artifact produced by the pipeline carries a version label. These
"assigned_to": null,
"approval_gate": true,
"team_notes": null
},
"6": {
"name": "PROCESS SUMMARY",
"skill": "academic-pipeline",
"status": "pending",
"mode": null,
"outputs": [],
"started_at": null,
"completed_at": null,
"checkpoint_confirmed": false,
"checkpoint_type": null,
"schema_validated": false,
"assigned_to": null,
"approval_gate": true,
"team_notes": null
}
},
"revision_history": [
@@ -287,7 +302,7 @@ Update the specified stage's status.
| Parameter | Description |
|-----------|------------|
| stage_id | "1", "2", "2.5", "3", "4", "3p", "4p", "4.5", "5" |
| stage_id | "1", "2", "2.5", "3", "4", "3p", "4p", "4.5", "5", "6" |
| status | "pending", "in_progress", "completed", "skipped", "blocked" |
| details | mode, outputs, decision, verdict, and other additional information |
@@ -295,6 +310,7 @@ Update the specified stage's status.
- Status can only advance (pending -> in_progress -> completed), cannot regress
- Exception: Stage 2.5 and 4.5 FAIL retries are legal (status remains in_progress)
- Skipped status means the user skipped this stage (Stage 2.5 and 4.5 cannot be skipped)
- Stage 6 terminal semantics (#528): on the terminal acknowledgement, `update_stage("6", "completed", outputs)` then `update_pipeline_state("completed")`; if the user declines Stage 6 at the Stage 5 completion checkpoint, `update_stage("6", "skipped", {reason: "user declined Stage 6"})` then `update_pipeline_state("completed")`. See `../references/pipeline_state_machine.md` § Stage 6 terminal semantics
### 2. update_pipeline_state(state)
@@ -351,7 +367,7 @@ Check whether prerequisite materials for entering the specified stage are availa
| Target Stage | Required Materials | Recommended Materials |
|-------------|-------------------|----------------------|
| Stage 1 | None (can start from scratch) | User-provided topic/direction |
| Stage 2 | None (but Stage 1 output recommended) | RQ Brief, Bibliography, Synthesis |
| Stage 2 | None (but Stage 1 output recommended) | RQ Brief, Methodology Blueprint, Bibliography, Synthesis |
| Stage 2.5 | Paper Draft | -- |
| Stage 3 | **Verified Paper Draft + Integrity Report (Pre)** | -- |
| Stage 4 | Review Reports + Revision Roadmap | Paper Draft |
@@ -359,6 +375,7 @@ Check whether prerequisite materials for entering the specified stage are availa
| Stage 4' | Re-Review Report (Decision: Major) | Revised Draft |
| Stage 4.5 | Revised Draft or Re-Revised Draft | -- |
| Stage 5 | **Integrity Report (Final) — verdict: PASS** | -- |
| Stage 6 | None (Final Paper already delivered at Stage 5) | Pipeline state history + dialogue_log_ref ranges |
**Return format:**
```
@@ -374,7 +391,7 @@ Append a Collaboration Depth Observer report (added in v3.3.0, behind `measures:
| Parameter | Description |
|-----------|-------------|
| stage_id | Stage the observer scored, or `"pipeline"` for the whole-pipeline pass at completion |
| stage_id | Stage the observer scored, or `"pipeline"` for the whole-pipeline pass during Stage 6 record compilation |
| checkpoint_type | "FULL", "SLIM", or "pipeline_completion" (MANDATORY checkpoints MUST NOT call this function) |
| report | Object with `timestamp`, `dialogue_log_ref`, `zone`, `scores`, `cross_model_divergence`, and always `advisory_only: true` |
@@ -124,10 +124,21 @@ This document defines all legal states, transition conditions, transition action
|FINALIZE| |
+---+----+ |
| |
v |
+-------+ |
| END | |
+-------+ |
[checkpoint]---[decline Stage 6]---+ |
| | |
v | |
+--------+ | |
|Stage 6 | | |
|PROCESS | | |
|SUMMARY | | |
+---+----+ | |
| | |
[terminal acknowledgement] | |
| | |
v | |
+---------+ | |
|COMPLETED| <----------------------+ |
+---------+ |
```
---
@@ -163,7 +174,12 @@ This document defines all legal states, transition conditions, transition action
| checkpoint | Stage 4.5 | User confirms | Pass revised draft to final verification |
| Stage 4.5 | **checkpoint** | PASS (zero issues) | Wait for user confirmation |
| Stage 4.5 | Stage 4.5 (retry) | FAIL | Fix issues, re-verify (max 3 rounds) |
| checkpoint | Stage 5 | User confirms | Pass final accepted draft |
| checkpoint | Stage 5 | User confirms (MANDATORY — the Stage 5 entry gate; see § Stage 5 boundary semantics) | Pass final accepted draft; record the finalization-format decision (citation style) |
| Stage 5 | **checkpoint** | Stage 5 completed, Final Paper delivered | Wait for user confirmation (FULL — never SLIM; see § Stage 5 boundary semantics) |
| checkpoint | Stage 6 | User confirms | Dispatch Process Summary per `process_summary_protocol.md` |
| checkpoint | completed | User declines Stage 6 | Mark Stage 6 `skipped` (non-mandatory stage); set pipeline global state `completed` |
| Stage 6 | **terminal checkpoint** | Process Record delivered | Wait for terminal acknowledgement (see § Stage 6 terminal semantics) |
| terminal checkpoint | completed | User acknowledges (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) | Mark Stage 6 `completed`; set pipeline global state `completed` |
### Special Flow Transitions
@@ -190,6 +206,37 @@ This document defines all legal states, transition conditions, transition action
---
## Stage 5 and Stage 6 Boundary Semantics (#528)
The two boundaries below were under-specified before v3.17 (different runtimes could resolve them differently). This section is the authority; `SKILL.md` and `pipeline_orchestrator_agent.md` mirror it.
### Stage 5 boundary semantics
"Before finalization (Stage 5): always MANDATORY" refers to exactly ONE checkpoint: the **Stage 5 entry gate** — the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch. It is MANDATORY because it carries the finalization decisions:
- explicit confirmation to proceed to finalization (no auto-advance);
- the finalization-format decision: citation style (APA 7.0 / Chicago / IEEE, ...) — the "Stage 5 finalization format" pending decision the passport-reset machinery records at this boundary.
Transition state: `awaiting_confirmation` → on user confirmation → Stage 5 `in_progress`.
Other confirmations near Stage 5 are NOT this MANDATORY boundary:
1. The in-stage interactions of the Stage 5 output process — the "Need LaTeX?" question (Step 3) and the content confirmation before the final PDF (Step 4) — are part of Stage 5 execution, not pipeline checkpoints; they are asked during the stage, never at the gate.
2. The **Stage 5 completion checkpoint** (Final Paper delivered, before Stage 6) follows the global stage-completion rule: it is a FULL checkpoint — never SLIM, because final-deliverable acceptance must not be downgraded — but it is not on the MANDATORY list.
### Stage 6 terminal semantics
Stage 6 is a non-mandatory stage (it is absent from the orchestrator's non-skippable list). At the Stage 5 completion checkpoint the user may decline it: Stage 6 is marked `skipped` and the pipeline still terminates `completed` (the Final Paper was already produced at Stage 5).
When Stage 6 runs, its completion is the pipeline's **terminal checkpoint**:
1. After delivering the Process Record (MD + PDF per the user's language choice), the orchestrator prompts for a terminal acknowledgement.
2. Terminal acknowledgement vocabulary: `finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent that accepts the deliverables. Change requests (the other language version, content corrections) keep Stage 6 `in_progress` — they are not acknowledgements.
3. On acknowledgement: state_tracker marks Stage 6 `completed` and sets the pipeline global state to `completed`. This is the terminal transition — there is no next stage.
4. After `completed`, no stage transition is legal (see Prohibited Transitions). New requests start a new pipeline run or a targeted single-skill invocation (mid-entry).
---
## Material Dependency Matrix
| Material | Produced At | Consumed At | Required/Recommended |
@@ -209,7 +256,8 @@ This document defines all legal states, transition conditions, transition action
| **Re-Review Report** | **Stage 3'** | **Stage 4' (input)** | **Required (if Major)** |
| **Re-Revised Draft** | **Stage 4'** | **Stage 4.5 (input)** | **Required (if executed)** |
| **Integrity Report (Final)** | **Stage 4.5** | **Stage 5 (prerequisite)** | **Required** |
| Final Paper | Stage 5 | END (delivery) | Required |
| Final Paper | Stage 5 | User (delivery) | Required |
| Process Record | Stage 6 | User (delivery) | Optional (Stage 6 is skippable) |
---
@@ -1,6 +1,6 @@
# Stage 6: Process Summary Protocol (Added in v2.4)
**Trigger**: After Stage 5 (FINALIZE) completion
**Trigger**: After the user confirms the Stage 5 completion checkpoint (FULL). Stage 6 is non-mandatory — the user may decline it at that checkpoint; it is then marked `skipped` and the pipeline still terminates `completed` (see `pipeline_state_machine.md` § Stage 6 terminal semantics)
**Purpose**: Document the complete human-AI collaboration history for the paper creation process, for user sharing, reporting, or reflection
## Workflow
@@ -21,6 +21,11 @@
- Quality requirement evolution (e.g., formatting, tone adjustments)
- Pipeline statistics (stage count, review rounds, integrity verification count, etc.)
2b. Dispatch collaboration_depth_agent in whole-pipeline mode (range = all
stages, v3.5); its advisory report becomes the "Collaboration Depth
Trajectory" chapter of the Process Record — this dispatch happens BEFORE
record generation so the chapter is inside the record the user acknowledges
3. Generate Markdown version (paper_creation_process.md / paper_creation_process_en.md)
4. Convert to LaTeX and compile PDF:
@@ -28,6 +33,15 @@
- Package complete LaTeX document (with cover page, table of contents, headers/footers)
- tectonic compile PDF
- Chinese version requires xeCJK + Source Han Serif TC VF
5. Terminal acknowledgement (pipeline terminal checkpoint):
- After delivering the process record, prompt the user to close the pipeline.
- Acknowledgement vocabulary: "finish" / "end" / "done" / "confirm", or an
unambiguous natural-language equivalent that accepts the deliverables.
- Change requests (the other language version, content corrections) keep
Stage 6 in_progress — they are not acknowledgements.
- On acknowledgement: state_tracker marks Stage 6 completed and sets the pipeline global state to completed. There is no next stage.
(See pipeline_state_machine.md § Stage 6 terminal semantics.)
```
## Required Content in Process Record
+4
View File
@@ -287,3 +287,7 @@ path = "scripts/test_check_tools_allowlist.py"
[[pytest]]
id = "510-panel-synthesis-checker"
path = "scripts/test_check_panel_synthesis.py"
[[pytest]]
id = "528-pipeline-boundary-semantics"
path = "scripts/test_check_pipeline_boundary_semantics.py"
@@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""Defrift lock for the four #528 pipeline-boundary resolutions.
The 2026-07 Mode-A structural replay (#528) surfaced four inconsistencies /
under-specified boundaries across the academic-pipeline prompt surfaces.
PR #529 fixed the two genuine contradictions (items 1-2); the #528 closure PR
defined the two under-specified boundaries (items 3-4). None of the four had
a lint, so any future prompt edit could silently re-open them — the same
drift class the #491 lock closed for the Bucket A enforcement sentence.
Pinned invariants (one per replay item):
1. **Methodology Blueprint in the Stage 1→2 handoff** — all three handoff
surfaces (SKILL.md Step 4 list, state-machine transition row, orchestrator
handoff table) carry the Blueprint alongside RQ Brief / Bibliography /
Synthesis.
2. **Stage 3' Minor does not trigger coaching** — the orchestrator's coaching
trigger condition and Coaching Rules exclusion list both state it.
3. **Stage 5 boundary semantics** — the MANDATORY finalization boundary is the
Stage 5 ENTRY gate (between Stage 4.5 PASS and the Stage 5 dispatch); the
Stage 5 completion checkpoint is FULL — never SLIM. Authority section in
the state machine + mirrored canonical fragments in SKILL.md and the
orchestrator + the completion-checkpoint transition row.
4. **Stage 6 terminal semantics** — the state machine defines Stage 6, the
decline path, the terminal checkpoint, and the acknowledgement vocabulary
(finish / end / done / confirm + natural-language equivalent); SKILL.md,
the orchestrator, and process_summary_protocol.md carry the vocabulary;
the orchestrator wires the terminal state_tracker transition.
Falsifiability discipline (per feedback_lint_passes_but_prompt_silent.md):
the state-machine authority fragments are scoped to the § Stage 5 and Stage 6
Boundary Semantics H2 span via the shared `check_section_literals` — the same
fragment appearing elsewhere in the file does not count. The SKILL.md /
orchestrator mirror fragments are file-unique canonical sentences and are
deliberately pinned file-wide.
Exit codes: 0 on pass, 1 on any failure.
"""
from __future__ import annotations
import sys
from pathlib import Path
import hashlib
from _skill_lint import check_section_literals
REPO_ROOT = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
# Whole-file content locks (sha256) for ALL FIVE #528 pipeline surfaces.
#
# Twelve codex xhigh review rounds demonstrated that sentence-level pins on
# pipeline prompt surfaces cannot converge: every round found another single
# green mutation of an operative sentence, label, predicate, or enum value.
# So every surface this lint reads carries the bibliography_agent-style
# whole-file hash lock: ANY byte change fails CI until this constant is
# updated in the same commit — which is the review surface working as
# designed (the same F2-lock convention the repo already applies to
# bibliography_agent). The sentence pins above/below stay for targeted
# error messages; the hash is the catch-all that terminates the
# single-edit-mutation class by construction.
#
# Update procedure: edit the doc, run
# python3 -c "import hashlib,pathlib;p='<path>';print(hashlib.sha256(pathlib.Path(p).read_bytes()).hexdigest())"
# and update the constant here IN THE SAME COMMIT, with the semantic change
# reviewed against the #528 resolutions.
# ---------------------------------------------------------------------------
CONTENT_LOCKS = {
"academic-pipeline/SKILL.md": "56ee95301d16029053183a12ab1939f44034e66ebb25b1917c65a753cffc479a",
"academic-pipeline/agents/pipeline_orchestrator_agent.md": "57aba8259ab418505c53fc9a24939ba5ad972487083f43a6a54b388d5381f5da",
"academic-pipeline/agents/state_tracker_agent.md": "b648eac4d4b35c217150539502c20fccc2f3fd026dda8efbb6178b199a288256",
"academic-pipeline/references/pipeline_state_machine.md": "d507f3694cd4d282b9b3247d0d1855330c836c23ab6e5d41f280c6d455b4ed7f",
"academic-pipeline/references/process_summary_protocol.md": "5c7053230d73b39d0a5d9d6f5e9f339c12570ae6d3aa2eae2eaf74f51d571e94",
}
SKILL = "academic-pipeline/SKILL.md"
ORCH = "academic-pipeline/agents/pipeline_orchestrator_agent.md"
SM = "academic-pipeline/references/pipeline_state_machine.md"
PROTO = "academic-pipeline/references/process_summary_protocol.md"
# --- Invariant 1: Methodology Blueprint on all three Stage 1→2 surfaces ---
INV1_FRAGMENTS = {
SKILL: "Stage 1 --> 2: deep-research handoff (RQ Brief + Methodology Blueprint + Bibliography + Synthesis)",
SM: "| checkpoint | Stage 2 | User confirms | handoff RQ Brief + Methodology Blueprint + Bibliography + Synthesis |",
ORCH: "| Stage 1 -> 2 | RQ Brief, Methodology Blueprint, Annotated Bibliography, Synthesis Report |",
}
# --- Invariant 2: Stage 3' Minor never triggers coaching (both ORCH spots),
# and the Minor -> Stage 4.5 routing declarations themselves (codex round-5
# P1: a mutation rerouting Minor left the coaching pins intact and green) ---
INV2_FRAGMENTS = [
"A Stage 3' Minor decision does NOT trigger coaching",
"a Stage 3' Minor decision also does not trigger coaching",
]
INV2_SKILL_ROUTING = "6. **Stage 3' RE-REVIEW** -> Accept|Minor -> Stage 4.5 / Major -> Stage 4'"
INV2_SM_ROUTING_ROW = "| checkpoint | Stage 4.5 | Decision = Accept/Minor, user confirms | Pass final draft to final verification |"
# Affirmative Major-only coaching predicates (codex round-6 P1: the negative
# exclusions alone let the operative trigger widen back to Minor/Major).
INV2_ORCH_AFFIRMATIVE = {
"coaching-trigger-major-only": "after Stage 3' completion with Decision = Major Revision (routes to Stage 4')",
"coaching-handoff-if-major": "| Stage 3' -> **coaching** -> 4' | New Revision Roadmap (if Major) |",
}
# --- Invariants 3+4: the state-machine authority section (one H2, two H3s) ---
AUTHORITY_HEADING = "## Stage 5 and Stage 6 Boundary Semantics"
TRANSITIONS_HEADING = "## Legal State Transitions"
# Canonical terminal-acknowledgement vocabulary. Two spellings by surface:
# backtick form in the markdown prose (SKILL / ORCH / SM), double-quote form
# inside the protocol doc's fenced workflow block. The protocol additionally
# pins the natural-language-equivalent clause so the vocabulary cannot be
# narrowed to exact-keyword-only there (codex P1, 2026-07-16).
VOCAB_CANON = "`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent"
VOCAB_PROTO = '"finish" / "end" / "done" / "confirm"'
VOCAB_PROTO_NL_CLAUSE = "unambiguous natural-language equivalent that accepts the deliverables"
S5_AUTHORITY_LITERALS = {
"entry-gate": "refers to exactly ONE checkpoint: the **Stage 5 entry gate**",
"no-auto-advance": "- explicit confirmation to proceed to finalization (no auto-advance);",
"gate-format-decision": 'the finalization-format decision: citation style (APA 7.0 / Chicago / IEEE, ...) — the "Stage 5 finalization format" pending decision the passport-reset machinery records at this boundary',
"latex-in-stage": 'the "Need LaTeX?" question (Step 3) and the content confirmation before the final PDF (Step 4) — are part of Stage 5 execution, not pipeline checkpoints',
"completion-full-never-slim": "FULL checkpoint — never SLIM",
"completion-not-mandatory": "but it is not on the MANDATORY list",
}
S6_AUTHORITY_LITERALS = {
"decline-path": "marked `skipped` and the pipeline still terminates `completed`",
"stage6-non-mandatory": "Stage 6 is a non-mandatory stage (it is absent from the orchestrator's non-skippable list). At the Stage 5 completion checkpoint the user may decline it",
"terminal-checkpoint": "When Stage 6 runs, its completion is the pipeline's **terminal checkpoint**:",
"acknowledgement-vocabulary": VOCAB_CANON,
"change-requests-not-ack": "Change requests (the other language version, content corrections) keep Stage 6 `in_progress` — they are not acknowledgements",
"ack-outcome": "On acknowledgement: state_tracker marks Stage 6 `completed` and sets the pipeline global state to `completed`",
"post-delivery-prompt": "After delivering the Process Record (MD + PDF per the user's language choice), the orchestrator prompts for a terminal acknowledgement",
"no-transition-after-completed": "no stage transition is legal",
}
# Pinned as the complete row / the type-bearing clause so the MANDATORY
# classification itself cannot flip to FULL while staying green (codex
# round-4 P1: the previous pins started after the type keyword).
S5_ENTRY_GATE_TABLE_CELL = "| MANDATORY | Integrity FAIL; Review decision; Stage 5 entry gate (before finalization) | Cannot be skipped; requires explicit user input |"
S5_CANON_RULE5 = "always MANDATORY — this is the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch"
S5_CANON_GATE_SCOPE = "makes the finalization-format decision (citation style); the in-stage LaTeX question and content confirmation stay inside Stage 5 execution"
S5_CANON_COMPLETION = "The Stage 5 completion checkpoint (Final Paper delivered, before Stage 6) is FULL — never SLIM"
# Non-acknowledgement classification of change requests, per mirror surface.
SKILL_CHANGE_REQUESTS_NOT_ACK = "keep Stage 6 `in_progress` and are not acknowledgements"
PROTO_CHANGE_REQUESTS_NOT_ACK = "Stage 6 in_progress — they are not acknowledgements"
# The FULL row must NOT reclaim "before finalization" — that boundary is the
# MANDATORY Stage 5 entry gate; FULL owns the Stage 5 COMPLETION checkpoint
# (codex round-3 P1: both types claimed "before finalization").
FULL_ROW = "| FULL | First checkpoint; after integrity boundaries; Stage 5 completion (final-deliverable acceptance) | Full deliverables list + decision dashboard + all options |"
# Stage 5 execution consumes the gate decision instead of re-asking (SKILL
# execution contract Step 1; codex round-3 P1).
SKILL_STEP1_CONSUME = "Consume the citation-style decision recorded at the Stage 5 entry gate; ask which academic formatting style (APA 7.0 / Chicago / IEEE, etc.) only when no gate decision exists (direct format-convert / mid-entry invocation)"
# The overview summary's terminal ordering (codex round-12 P1).
SKILL_OVERVIEW_ORDERING = "delivered before the terminal acknowledgement that completes the pipeline"
# The reset-boundary iron rule scopes MANDATORY to the entry gate (codex
# round-12 P1: the round-11 prose fix had no pin).
ORCH_RESET_IRON_RULE = "MANDATORY checkpoints (Stage 2.5 / 4.5, review decisions, the Stage 5 entry gate) remain MANDATORY even when reset co-occurs"
# Stage 6 decline semantics per mirror surface (codex round-3 P1: only the
# state machine pinned the decline path).
SKILL_STAGE6_HEADING = "## Stage 6: Process Summary Protocol"
SKILL_STAGE6_LITERALS = {
"acknowledgement-vocabulary": "`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent",
"decline-path": "Stage 6 is non-mandatory — the user may decline it at the Stage 5 completion checkpoint (Stage 6 marked `skipped`; the pipeline still terminates `completed`)",
"ack-outcome": "On acknowledgement, Stage 6 is marked `completed` and the pipeline global state is set to `completed`",
"change-requests-not-ack": SKILL_CHANGE_REQUESTS_NOT_ACK,
}
PROTO_ACK_OUTCOME = "On acknowledgement: state_tracker marks Stage 6 completed and sets the pipeline global state to completed"
SKILL_RULE9_PIN = "completion checkpoint (FULL) -> Stage 6 (user may decline Stage 6: marked `skipped`, pipeline goes directly to `completed`)"
SKILL_RULE10_PIN = "terminal acknowledgement (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) -> pipeline global state `completed`"
ORCH_DECLINE_HANDOFF_PIN = "User may decline Stage 6 there: mark it `skipped`, set pipeline state `completed`"
PROTO_DECLINE_PIN = "Stage 6 is non-mandatory — the user may decline it at that checkpoint; it is then marked `skipped` and the pipeline still terminates `completed`"
# Type-bearing completion-checkpoint triggers on the mirrors (codex round-6
# P1: a (FULL)->(MANDATORY) flip on either mirror stayed green).
ORCH_STAGE56_TRIGGER_PIN = "Dispatched only after the user confirms the Stage 5 completion checkpoint (FULL)"
ORCH_STAGE56_HANDOFF_ROW = "| Stage 5 -> 6 | Final deliverables list + pipeline state history (state_tracker JSON, agent logs) | — (Process Record; no numbered schema) | Dispatched only after the user confirms the Stage 5 completion checkpoint (FULL). User may decline Stage 6 there: mark it `skipped`, set pipeline state `completed`. Protocol: `../references/process_summary_protocol.md`; terminal semantics: `../references/pipeline_state_machine.md` § Stage 6 terminal semantics |"
PROTO_TRIGGER_PIN = "After the user confirms the Stage 5 completion checkpoint (FULL)"
# Delivery-before-acknowledgement sequencing (codex round-6 P1: on->before /
# After->Before mutations stayed green).
PROTO_SEQUENCING_PIN = "After delivering the process record, prompt the user to close the pipeline"
# Protocol step-5 header + terminal-outcome sentence (codex round-7 P1: the
# step could be retyped non-terminal / gain a Stage 7 continuation).
PROTO_STEP5_HEADER = "5. Terminal acknowledgement (pipeline terminal checkpoint):"
PROTO_NO_NEXT_STAGE = "There is no next stage."
# SKILL Step 4 handoff line (codex round-7 P1: the executing transition list
# could reverse the decline option while the Stage 6 section stayed pinned).
SKILL_STEP4_HANDOFF = "- Stage 5 --> 6: Pass final deliverables list + pipeline state history to Process Summary (user may decline Stage 6 at the Stage 5 completion checkpoint)"
# The #528/#529 diagram edges (codex round-7 P1: the ASCII diagram is an
# operative surface too — a /Minor -> /Reject or relabeled terminal edge
# would contradict the transitions table while staying green). Scoped to the
# ## State Transition Diagram span.
DIAGRAM_HEADING = "## State Transition Diagram (ASCII)"
DIAGRAM_EDGES = {
"stage3p-accept-minor-branch": "| Accept Major |\n | /Minor |",
"terminal-ack-edge": "[terminal acknowledgement]",
"decline-edge": "[decline Stage 6]",
}
# Transition rows are pinned as COMPLETE rows (all four cells), scoped to the
# ## Legal State Transitions span — a prefix pin would let a mutation flip the
# outcome cell (FULL→MANDATORY, completed→skipped) and stay green (codex P1).
S5_TRANSITION_ROWS = {
"entry-gate-row": "| checkpoint | Stage 5 | User confirms (MANDATORY — the Stage 5 entry gate; see § Stage 5 boundary semantics) | Pass final accepted draft; record the finalization-format decision (citation style) |",
"completion-checkpoint-row": "| Stage 5 | **checkpoint** | Stage 5 completed, Final Paper delivered | Wait for user confirmation (FULL — never SLIM; see § Stage 5 boundary semantics) |",
}
S6_TRANSITION_ROWS = {
"stage6-dispatch-row": "| checkpoint | Stage 6 | User confirms | Dispatch Process Summary per `process_summary_protocol.md` |",
"terminal-checkpoint-row": "| Stage 6 | **terminal checkpoint** | Process Record delivered | Wait for terminal acknowledgement (see § Stage 6 terminal semantics) |",
"terminal-transition-row": "| terminal checkpoint | completed | User acknowledges (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) | Mark Stage 6 `completed`; set pipeline global state `completed` |",
"decline-row": "| checkpoint | completed | User declines Stage 6 | Mark Stage 6 `skipped` (non-mandatory stage); set pipeline global state `completed` |",
}
# The orchestrator's state_tracker wiring, pinned as complete action pairs —
# a bare update_pipeline_state("completed") pin would let the acknowledgement
# branch flip Stage 6 to `skipped` while staying green (codex round-2 P1).
ORCH_TERMINAL_WIRING = {
"acknowledgement-sequencing": "Pipeline terminal transition: on the Stage 6 terminal acknowledgement (",
"acknowledgement-wiring": '`update_stage("6", "completed", outputs)` + `update_pipeline_state("completed")`',
"decline-wiring": 'if the user declined Stage 6 at the Stage 5 completion checkpoint — `update_stage("6", "skipped", {reason: "user declined Stage 6"})` + `update_pipeline_state("completed")`',
}
# The state_tracker contract must accept Stage 6 (codex round-3 P1: the
# orchestrator wiring called update_stage(6, ...) against a "1".."5" enum).
TRACKER = "academic-pipeline/agents/state_tracker_agent.md"
TRACKER_STAGE_ID_ENUM = '"1", "2", "2.5", "3", "4", "3p", "4p", "4.5", "5", "6"'
# The complete Stage 6 SSOT block — pinning only the key would let a
# load-bearing field (approval_gate) flip while staying green (codex
# round-9 P1).
TRACKER_STAGE6_ENTRY = '''"6": {
"name": "PROCESS SUMMARY",
"skill": "academic-pipeline",
"status": "pending",
"mode": null,
"outputs": [],
"started_at": null,
"completed_at": null,
"checkpoint_confirmed": false,
"checkpoint_type": null,
"schema_validated": false,
"assigned_to": null,
"approval_gate": true,
"team_notes": null
}'''
# check_prerequisites drives the automatic material-gap warnings — its Stage 2
# row must carry the Methodology Blueprint like every other Stage 1→2 surface
# (codex round-8 P1), and Stage 6 must be a known target stage.
TRACKER_PREREQ_STAGE2_ROW = "| Stage 2 | None (but Stage 1 output recommended) | RQ Brief, Methodology Blueprint, Bibliography, Synthesis |"
# The tracker's accepted-value declarations — the terminal wiring calls are
# pinned, but a renamed enum value would make the named consumer reject them
# (codex round-10 P1).
TRACKER_STATUS_ENUM_ROW = '| status | "pending", "in_progress", "completed", "skipped", "blocked" |'
TRACKER_GLOBAL_COMPLETED = "- `completed`"
TRACKER_PREREQ_STAGE6_ROW = "| Stage 6 | None (Final Paper already delivered at Stage 5) |"
# The skip-command validator only honors explicitly-skippable stages — the
# decline path requires Stage 6 on the skippable list (codex round-8 P1).
ORCH_SKIPPABLE_PIN = "- Skippable: Stage 1 (deep-research, if user provides own bibliography), Stage 3' (re-review, if only minor revisions), Stage 4' (re-revise, if accepted), Stage 6 (process summary — declined at the Stage 5 completion checkpoint; marked `skipped`, pipeline still terminates `completed`)"
# Both sides of the skip classification are pinned as complete lines — adding
# Stage 6 to the non-skippable side would otherwise stay green while
# contradicting the skippable declaration (codex round-9 P1).
ORCH_NON_SKIPPABLE_LINE = "- Non-Skippable: Stage 2 (writing), Stage 2.5 (pre-review integrity), Stage 3 (initial review), Stage 4.5 (final integrity), Stage 5 (finalize)"
# The SLIM engagement downgrade must not swallow the FULL-pinned Stage 5
# completion checkpoint (codex round-9 P1).
ORCH_ENGAGEMENT_FULL_EXCEPTION = "the Stage 5 completion checkpoint is FULL — never SLIM, regardless of the continue count"
TRACKER_WIRING = {
"acknowledgement-outcome": 'on the terminal acknowledgement, `update_stage("6", "completed", outputs)` then `update_pipeline_state("completed")`',
"decline-outcome": 'if the user declines Stage 6 at the Stage 5 completion checkpoint, `update_stage("6", "skipped", {reason: "user declined Stage 6"})` then `update_pipeline_state("completed")`',
}
def _line_pinned(text: str, pinned: str) -> bool:
"""True when `pinned` matches a COMPLETE line of `text` (whitespace-
stripped equality). Substring pins on list lines are append-exploitable:
', Stage 6' after 'Stage 5 (finalize)' keeps the pin as a prefix and
stays green (codex round-9 P1 witness)."""
return any(line.strip() == pinned for line in text.splitlines())
def check(skill: str, orch: str, sm: str, proto: str, tracker: str = "") -> list[str]:
"""Pure invariant evaluation over the five surface contents."""
errors: list[str] = []
texts = {SKILL: skill, ORCH: orch, SM: sm, PROTO: proto}
# Invariant 1
for path, fragment in INV1_FRAGMENTS.items():
if fragment not in texts[path]:
errors.append(
f"invariant 1 ({path}): Stage 1→2 handoff no longer carries "
f"the Methodology Blueprint in the pinned form: {fragment!r} "
f"(#529; keep all three surfaces in lockstep)"
)
# Invariant 2
for fragment in INV2_FRAGMENTS:
if fragment not in orch:
errors.append(
f"invariant 2 ({ORCH}): coaching-trigger exclusion missing: "
f"{fragment!r} (#529; a Stage 3' Minor routes directly to "
f"Stage 4.5 and must not trigger coaching)"
)
if not _line_pinned(skill, INV2_SKILL_ROUTING):
errors.append(
f"invariant 2 ({SKILL}): the Stage 3' routing declaration "
f"drifted from the pinned form: {INV2_SKILL_ROUTING!r}"
)
for name, fragment in INV2_ORCH_AFFIRMATIVE.items():
if fragment not in orch:
errors.append(
f"invariant 2 ({ORCH}): affirmative {name} predicate "
f"drifted from the pinned form: {fragment!r}"
)
errors.extend(
check_section_literals(2, sm, TRANSITIONS_HEADING,
f"{SM} legal-transitions",
{"stage3p-minor-routing-row": INV2_SM_ROUTING_ROW})
)
errors.extend(
check_section_literals(2, sm, DIAGRAM_HEADING,
f"{SM} transition-diagram",
DIAGRAM_EDGES)
)
# Invariant 3 — authority section + transition rows (both H2-scoped)
errors.extend(
check_section_literals(3, sm, AUTHORITY_HEADING,
f"{SM} Stage-5/6 authority",
S5_AUTHORITY_LITERALS)
)
errors.extend(
check_section_literals(3, sm, TRANSITIONS_HEADING,
f"{SM} legal-transitions",
S5_TRANSITION_ROWS)
)
# Invariant 3 — mirrors (file-unique canonical sentences, pinned file-wide)
for path in (SKILL, ORCH):
if S5_ENTRY_GATE_TABLE_CELL not in texts[path]:
errors.append(
f"invariant 3 ({path}): checkpoint-type table no longer "
f"scopes MANDATORY to {S5_ENTRY_GATE_TABLE_CELL!r}"
)
if S5_CANON_RULE5 not in texts[path]:
errors.append(
f"invariant 3 ({path}): checkpoint rule 5 lost the entry-gate "
f"definition {S5_CANON_RULE5!r}"
)
if S5_CANON_GATE_SCOPE not in texts[path]:
errors.append(
f"invariant 3 ({path}): checkpoint rule 5 lost the gate-scope "
f"clause {S5_CANON_GATE_SCOPE!r} (citation style is the sole "
f"gate format decision; LaTeX stays in-stage)"
)
if S5_CANON_COMPLETION not in texts[path]:
errors.append(
f"invariant 3 ({path}): checkpoint rule 5 lost the "
f"completion-checkpoint sentence {S5_CANON_COMPLETION!r}"
)
if FULL_ROW not in texts[path]:
errors.append(
f"invariant 3 ({path}): the FULL checkpoint-type row drifted "
f"from the pinned form (it must claim the Stage 5 COMPLETION "
f"checkpoint, never 'before finalization' — that boundary is "
f"MANDATORY): {FULL_ROW!r}"
)
if SKILL_STEP1_CONSUME not in skill:
errors.append(
f"invariant 3 ({SKILL}): Stage 5 execution contract Step 1 no "
f"longer consumes the entry-gate citation-style decision (with "
f"the ask-only-when-absent fallback): {SKILL_STEP1_CONSUME!r}"
)
if ORCH_RESET_IRON_RULE not in orch:
errors.append(
f"invariant 3 ({ORCH}): the reset-boundary iron rule no longer "
f"scopes its MANDATORY list to the Stage 5 entry gate: "
f"{ORCH_RESET_IRON_RULE!r}"
)
if SKILL_OVERVIEW_ORDERING not in skill:
errors.append(
f"invariant 4 ({SKILL}): the overview summary lost the "
f"delivery-before-acknowledgement ordering: "
f"{SKILL_OVERVIEW_ORDERING!r}"
)
# Invariant 4 — authority section + transition rows (both H2-scoped)
errors.extend(
check_section_literals(4, sm, AUTHORITY_HEADING,
f"{SM} Stage-5/6 authority",
S6_AUTHORITY_LITERALS)
)
errors.extend(
check_section_literals(4, sm, TRANSITIONS_HEADING,
f"{SM} legal-transitions",
S6_TRANSITION_ROWS)
)
# Invariant 4 — SKILL mirror: section-scoped Stage 6 protocol block (the
# operative copy) + the rule-10 state-machine-list copy (file-unique pin)
errors.extend(
check_section_literals(4, skill, SKILL_STAGE6_HEADING,
f"{SKILL} Stage-6 protocol",
SKILL_STAGE6_LITERALS)
)
if not _line_pinned(skill, SKILL_STEP4_HANDOFF):
errors.append(
f"invariant 4 ({SKILL}): Step 4 transition list lost the Stage "
f"5->6 handoff line: {SKILL_STEP4_HANDOFF!r}"
)
if SKILL_RULE9_PIN not in skill:
errors.append(
f"invariant 4 ({SKILL}): state-machine rule 9 lost the "
f"completion-type + decline-outcome pin: {SKILL_RULE9_PIN!r}"
)
if SKILL_RULE10_PIN not in skill:
errors.append(
f"invariant 4 ({SKILL}): state-machine rule 10 lost the terminal-"
f"acknowledgement pin: {SKILL_RULE10_PIN!r}"
)
# Invariant 4 — ORCH mirror: vocabulary + decline handoff + wiring pairs
if VOCAB_CANON not in orch:
errors.append(
f"invariant 4 ({ORCH}): canonical terminal-acknowledgement "
f"vocabulary missing: {VOCAB_CANON!r}"
)
if ORCH_DECLINE_HANDOFF_PIN not in orch:
errors.append(
f"invariant 4 ({ORCH}): Stage 5->6 handoff row lost the decline "
f"semantics: {ORCH_DECLINE_HANDOFF_PIN!r}"
)
if ORCH_STAGE56_TRIGGER_PIN not in orch:
errors.append(
f"invariant 4 ({ORCH}): Stage 5->6 handoff row lost the type-"
f"bearing completion trigger: {ORCH_STAGE56_TRIGGER_PIN!r}"
)
if not _line_pinned(orch, ORCH_SKIPPABLE_PIN):
errors.append(
f"invariant 4 ({ORCH}): the skippable-stages line drifted from "
f"the pinned form (it must carry Stage 6 with its decline "
f"scope): {ORCH_SKIPPABLE_PIN!r}"
)
if not _line_pinned(orch, ORCH_STAGE56_HANDOFF_ROW):
errors.append(
f"invariant 4 ({ORCH}): the Stage 5 -> 6 handoff row drifted "
f"from the pinned form: {ORCH_STAGE56_HANDOFF_ROW!r}"
)
if not _line_pinned(orch, ORCH_NON_SKIPPABLE_LINE):
errors.append(
f"invariant 4 ({ORCH}): the non-skippable line drifted from the "
f"pinned form (it must not gain Stage 6): "
f"{ORCH_NON_SKIPPABLE_LINE!r}"
)
if ORCH_ENGAGEMENT_FULL_EXCEPTION not in orch:
errors.append(
f"invariant 3 ({ORCH}): the engagement-tracking SLIM downgrade "
f"lost its FULL-checkpoint exception: "
f"{ORCH_ENGAGEMENT_FULL_EXCEPTION!r}"
)
for name, fragment in ORCH_TERMINAL_WIRING.items():
if fragment not in orch:
errors.append(
f"invariant 4 ({ORCH}): state_tracker {name} pair missing: "
f"{fragment!r}"
)
# Invariant 4 — protocol mirror: vocabulary + decline + non-ack rule
for fragment in (VOCAB_PROTO, VOCAB_PROTO_NL_CLAUSE):
if fragment not in proto:
errors.append(
f"invariant 4 ({PROTO}): canonical terminal-acknowledgement "
f"vocabulary missing: {fragment!r}"
)
if PROTO_DECLINE_PIN not in proto:
errors.append(
f"invariant 4 ({PROTO}): trigger line lost the decline semantics: "
f"{PROTO_DECLINE_PIN!r}"
)
if PROTO_CHANGE_REQUESTS_NOT_ACK not in proto:
errors.append(
f"invariant 4 ({PROTO}): change-request non-acknowledgement rule "
f"missing: {PROTO_CHANGE_REQUESTS_NOT_ACK!r}"
)
if PROTO_ACK_OUTCOME not in proto:
errors.append(
f"invariant 4 ({PROTO}): terminal-acknowledgement outcome "
f"sentence missing: {PROTO_ACK_OUTCOME!r}"
)
if PROTO_TRIGGER_PIN not in proto:
errors.append(
f"invariant 4 ({PROTO}): trigger line lost the type-bearing "
f"completion trigger: {PROTO_TRIGGER_PIN!r}"
)
if PROTO_SEQUENCING_PIN not in proto:
errors.append(
f"invariant 4 ({PROTO}): delivery-before-acknowledgement "
f"sequencing missing: {PROTO_SEQUENCING_PIN!r}"
)
if PROTO_STEP5_HEADER not in proto:
errors.append(
f"invariant 4 ({PROTO}): step-5 terminal-checkpoint header "
f"missing: {PROTO_STEP5_HEADER!r}"
)
if PROTO_NO_NEXT_STAGE not in proto:
errors.append(
f"invariant 4 ({PROTO}): terminal-outcome sentence missing: "
f"{PROTO_NO_NEXT_STAGE!r}"
)
# Invariant 4 — state_tracker contract accepts Stage 6 + outcome pairs
if tracker:
if TRACKER_STAGE_ID_ENUM not in tracker:
errors.append(
f"invariant 4 ({TRACKER}): update_stage stage_id enum no "
f"longer includes \"6\": {TRACKER_STAGE_ID_ENUM!r}"
)
if TRACKER_STAGE6_ENTRY not in tracker:
errors.append(
f"invariant 4 ({TRACKER}): the stages example lost its "
f"Stage 6 entry ({TRACKER_STAGE6_ENTRY!r})"
)
for name, fragment in TRACKER_WIRING.items():
if fragment not in tracker:
errors.append(
f"invariant 4 ({TRACKER}): {name} pair missing: "
f"{fragment!r}"
)
if TRACKER_PREREQ_STAGE2_ROW not in tracker:
errors.append(
f"invariant 1 ({TRACKER}): check_prerequisites Stage 2 row "
f"no longer recommends the Methodology Blueprint: "
f"{TRACKER_PREREQ_STAGE2_ROW!r}"
)
if TRACKER_PREREQ_STAGE6_ROW not in tracker:
errors.append(
f"invariant 4 ({TRACKER}): check_prerequisites lost its "
f"Stage 6 row: {TRACKER_PREREQ_STAGE6_ROW!r}"
)
if not _line_pinned(tracker, TRACKER_STATUS_ENUM_ROW):
errors.append(
f"invariant 4 ({TRACKER}): update_stage status enum drifted "
f"from the pinned form (the terminal wiring depends on "
f"'completed'/'skipped'): {TRACKER_STATUS_ENUM_ROW!r}"
)
if not _line_pinned(tracker, TRACKER_GLOBAL_COMPLETED):
errors.append(
f"invariant 4 ({TRACKER}): update_pipeline_state legal "
f"values lost `completed`: {TRACKER_GLOBAL_COMPLETED!r}"
)
return errors
def check_content_locks() -> list[str]:
"""Whole-file sha256 locks for the two #528-central reference docs."""
errors: list[str] = []
for path, expected in CONTENT_LOCKS.items():
full = REPO_ROOT / path
if not full.is_file():
errors.append(f"content lock: file missing: {path}")
continue
actual = hashlib.sha256(full.read_bytes()).hexdigest()
if actual != expected:
errors.append(
f"content lock ({path}): sha256 {actual} != pinned "
f"{expected}. Any change to this operative reference doc "
f"must update CONTENT_LOCKS in the same commit, with the "
f"semantic change reviewed against the #528 resolutions "
f"(see the update procedure in this lint's header)."
)
return errors
def main() -> int:
contents = {}
for path in (SKILL, ORCH, SM, PROTO, TRACKER):
full = REPO_ROOT / path
if not full.is_file():
print(f"FAILED: surface file missing: {path}", file=sys.stderr)
return 1
contents[path] = full.read_text(encoding="utf-8")
errors = check(contents[SKILL], contents[ORCH], contents[SM],
contents[PROTO], contents[TRACKER])
errors.extend(check_content_locks())
if errors:
for e in errors:
print(f"FAILED: {e}", file=sys.stderr)
print(
"\nUpdate procedure: these fragments pin the #528/#529 boundary "
"resolutions. If the wording must change, change it on every "
"listed surface AND in this lint's pinned constants in the same "
"commit.",
file=sys.stderr,
)
return 1
print("PASSED: check_pipeline_boundary_semantics — 4 invariants hold")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,931 @@
"""Mutation tests for check_pipeline_boundary_semantics.py (#528 defrift lock).
One failing witness per invariant branch: each test mutates exactly one pinned
surface fragment and asserts the checker fires on that invariant (and, for the
baseline, that the committed repo state passes).
"""
import unittest
from pathlib import Path
from tests.test_helpers import load_module_from_path, run_script
SCRIPT = Path(__file__).resolve().parent / "check_pipeline_boundary_semantics.py"
REPO_ROOT = Path(__file__).resolve().parent.parent
def _load_module():
return load_module_from_path("check_pipeline_boundary_semantics", SCRIPT)
class PipelineBoundarySemanticsTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.mod = _load_module()
cls.skill = (REPO_ROOT / cls.mod.SKILL).read_text(encoding="utf-8")
cls.orch = (REPO_ROOT / cls.mod.ORCH).read_text(encoding="utf-8")
cls.sm = (REPO_ROOT / cls.mod.SM).read_text(encoding="utf-8")
cls.proto = (REPO_ROOT / cls.mod.PROTO).read_text(encoding="utf-8")
cls.tracker = (REPO_ROOT / cls.mod.TRACKER).read_text(encoding="utf-8")
def _check(self, skill=None, orch=None, sm=None, proto=None, tracker=None):
return self.mod.check(
skill if skill is not None else self.skill,
orch if orch is not None else self.orch,
sm if sm is not None else self.sm,
proto if proto is not None else self.proto,
tracker if tracker is not None else self.tracker,
)
def _authority_section(self, sm_text: str) -> str:
"""The H2 authority span, via the same shared helper the checker uses."""
from _skill_lint import h2_section_body
section = h2_section_body(sm_text, self.mod.AUTHORITY_HEADING)
self.assertIsNotNone(section)
return section
# --- baseline ---
def test_repo_baseline_passes(self) -> None:
result = run_script(SCRIPT)
self.assertEqual(result.returncode, 0, msg=f"stderr: {result.stderr}")
self.assertIn("PASSED", result.stdout)
def test_clean_contents_pass(self) -> None:
self.assertEqual(self._check(), [])
# --- INV-1: Methodology Blueprint in Stage 1→2 handoff ---
def test_inv1_skill_blueprint_dropped(self) -> None:
mutated = self.skill.replace(
"RQ Brief + Methodology Blueprint + Bibliography", "RQ Brief + Bibliography"
)
errors = self._check(skill=mutated)
self.assertTrue(any(e.startswith("invariant 1") and self.mod.SKILL in e for e in errors))
def test_inv1_state_machine_blueprint_dropped(self) -> None:
mutated = self.sm.replace(
"handoff RQ Brief + Methodology Blueprint + Bibliography",
"handoff RQ Brief + Bibliography",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 1") and self.mod.SM in e for e in errors))
def test_inv1_orchestrator_blueprint_dropped(self) -> None:
mutated = self.orch.replace(
"RQ Brief, Methodology Blueprint, Annotated Bibliography",
"RQ Brief, Annotated Bibliography",
)
errors = self._check(orch=mutated)
self.assertTrue(any(e.startswith("invariant 1") and self.mod.ORCH in e for e in errors))
# --- INV-2: Stage 3' Minor never triggers coaching ---
def test_inv2_trigger_condition_reverted(self) -> None:
mutated = self.orch.replace(
"A Stage 3' Minor decision does NOT trigger coaching",
"A Stage 3' Minor decision may trigger coaching",
)
errors = self._check(orch=mutated)
self.assertTrue(any(e.startswith("invariant 2") for e in errors))
def test_inv2_exclusion_list_reverted(self) -> None:
mutated = self.orch.replace(
"a Stage 3' Minor decision also does not trigger coaching",
"coaching applies to all Minor decisions",
)
errors = self._check(orch=mutated)
self.assertTrue(any(e.startswith("invariant 2") for e in errors))
# --- codex round-5 witnesses ---
def test_inv2_skill_routing_rerouted(self) -> None:
"""Adverse-value mutation (codex round-5 P1): SKILL rule 6 reroutes a
Stage 3' Minor through coaching/Stage 4' — must fire."""
mutated = self.skill.replace(
"6. **Stage 3' RE-REVIEW** -> Accept|Minor -> Stage 4.5 / Major -> Stage 4'",
"6. **Stage 3' RE-REVIEW** -> Accept -> Stage 4.5 / Minor|Major -> Stage 4'",
)
self.assertNotEqual(mutated, self.skill)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 2") and self.mod.SKILL in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv2_state_machine_minor_routing_dropped(self) -> None:
"""Adverse-value mutation (codex round-5 P1): the state machine's
Accept/Minor -> Stage 4.5 transition row loses Minor — must fire."""
mutated = self.sm.replace(
"| checkpoint | Stage 4.5 | Decision = Accept/Minor, user confirms | Pass final draft to final verification |",
"| checkpoint | Stage 4.5 | Decision = Accept, user confirms | Pass final draft to final verification |",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 2") and "stage3p-minor-routing-row" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_skill_rule9_outcome_flipped(self) -> None:
"""Adverse-value mutation (codex round-5 P1): rule 9 flips the
completion type or the decline outcome — must fire."""
type_mut = self.skill.replace(
"completion checkpoint (FULL) -> Stage 6",
"completion checkpoint (MANDATORY) -> Stage 6",
)
outcome_mut = self.skill.replace(
"user may decline Stage 6: marked `skipped`, pipeline goes directly to `completed`",
"user may not decline Stage 6",
)
for name, mut in (("type", type_mut), ("outcome", outcome_mut)):
self.assertNotEqual(mut, self.skill, msg=name)
errors = self._check(skill=mut)
self.assertTrue(
any(e.startswith("invariant 4") and "rule 9" in e for e in errors),
msg=f"{name} errors: {errors}",
)
# --- codex round-6 witnesses ---
def test_inv2_coaching_trigger_widened_to_minor(self) -> None:
"""Adverse-value mutation (codex round-6 P1): the affirmative coaching
trigger widens Stage 3' back to Minor/Major — must fire; same for the
handoff table's (if Major) qualifier."""
trigger_mut = self.orch.replace(
"after Stage 3' completion with Decision = Major Revision (routes to Stage 4')",
"after Stage 3' completion with Decision = Minor/Major Revision (routes to Stage 4')",
)
handoff_mut = self.orch.replace(
"| Stage 3' -> **coaching** -> 4' | New Revision Roadmap (if Major) |",
"| Stage 3' -> **coaching** -> 4' | New Revision Roadmap (if Minor/Major) |",
)
for name, mut in (("trigger", trigger_mut), ("handoff", handoff_mut)):
self.assertNotEqual(mut, self.orch, msg=name)
errors = self._check(orch=mut)
self.assertTrue(
any(e.startswith("invariant 2") and "affirmative" in e for e in errors),
msg=f"{name} errors: {errors}",
)
def test_inv4_completion_type_flipped_on_mirrors(self) -> None:
"""Adverse-value mutation (codex round-6 P1): the completion-checkpoint
type flips (FULL)->(MANDATORY) on the orchestrator handoff row or the
protocol trigger — must fire per mirror."""
orch_mut = self.orch.replace(
"Dispatched only after the user confirms the Stage 5 completion checkpoint (FULL)",
"Dispatched only after the user confirms the Stage 5 completion checkpoint (MANDATORY)",
)
proto_mut = self.proto.replace(
"After the user confirms the Stage 5 completion checkpoint (FULL)",
"After the user confirms the Stage 5 completion checkpoint (MANDATORY)",
)
for kw, mut, orig in (("orch", orch_mut, self.orch), ("proto", proto_mut, self.proto)):
self.assertNotEqual(mut, orig, msg=kw)
errors = self._check(**{kw: mut})
self.assertTrue(
any(e.startswith("invariant 4") and "completion trigger" in e for e in errors),
msg=f"{kw} errors: {errors}",
)
def test_inv4_sequencing_flipped(self) -> None:
"""Adverse-value mutation (codex round-6 P1): completion allowed
before acknowledgement/delivery — must fire on each surface."""
orch_mut = self.orch.replace(
"Pipeline terminal transition: on the Stage 6 terminal acknowledgement (",
"Pipeline terminal transition: before the Stage 6 terminal acknowledgement (",
)
proto_mut = self.proto.replace(
"After delivering the process record, prompt the user to close the pipeline",
"Before delivering the process record, prompt the user to close the pipeline",
)
sm_mut = self.sm.replace(
"After delivering the Process Record (MD + PDF per the user's language choice), the orchestrator prompts for a terminal acknowledgement",
"Before delivering the Process Record, the orchestrator prompts for a terminal acknowledgement",
)
for kw, mut, orig in (("orch", orch_mut, self.orch),
("proto", proto_mut, self.proto),
("sm", sm_mut, self.sm)):
self.assertNotEqual(mut, orig, msg=kw)
errors = self._check(**{kw: mut})
self.assertTrue(
any(e.startswith("invariant 4") and ("sequencing" in e or "post-delivery-prompt" in e) for e in errors),
msg=f"{kw} errors: {errors}",
)
# --- codex round-7 witnesses ---
def test_inv3_authority_auto_advance_flipped(self) -> None:
mutated = self.sm.replace(
"- explicit confirmation to proceed to finalization (no auto-advance);",
"- confirmation to proceed to finalization (auto-advance permitted);",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "no-auto-advance" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_authority_stage6_made_mandatory(self) -> None:
mutated = self.sm.replace(
"Stage 6 is a non-mandatory stage (it is absent from the orchestrator's non-skippable list). At the Stage 5 completion checkpoint the user may decline it",
"Stage 6 is a mandatory stage. The user may not decline it",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "stage6-non-mandatory" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_authority_terminal_moved_to_start(self) -> None:
mutated = self.sm.replace(
"When Stage 6 runs, its completion is the pipeline's **terminal checkpoint**:",
"When Stage 6 runs, its start is the pipeline's **terminal checkpoint**:",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "terminal-checkpoint" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv2_diagram_minor_branch_rerouted(self) -> None:
"""Adverse-value mutation (codex round-7 P1): the diagram's Stage 3'
Accept/Minor branch reroutes Minor — must fire."""
mutated = self.sm.replace("| /Minor |", "| /Reject |")
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 2") and "stage3p-accept-minor-branch" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv2_diagram_terminal_edge_relabeled(self) -> None:
mutated = self.sm.replace("[terminal acknowledgement]", "[record delivery]")
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 2") and "terminal-ack-edge" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_skill_step4_decline_reversed(self) -> None:
mutated = self.skill.replace(
"(user may decline Stage 6 at the Stage 5 completion checkpoint)",
"(user may not decline Stage 6)",
)
self.assertNotEqual(mutated, self.skill)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "Step 4" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_proto_step5_retyped_or_continued(self) -> None:
header_mut = self.proto.replace(
"5. Terminal acknowledgement (pipeline terminal checkpoint):",
"5. Acknowledgement (advisory, non-terminal):",
)
continuation_mut = self.proto.replace(
"There is no next stage.",
"Proceed to Stage 7 archival.",
)
for name, mut in (("header", header_mut), ("continuation", continuation_mut)):
self.assertNotEqual(mut, self.proto, msg=name)
errors = self._check(proto=mut)
self.assertTrue(
any(e.startswith("invariant 4") and self.mod.PROTO in e for e in errors),
msg=f"{name} errors: {errors}",
)
# --- INV-3: Stage 5 boundary semantics ---
def test_inv3_authority_section_removed(self) -> None:
mutated = self.sm.replace(
"## Stage 5 and Stage 6 Boundary Semantics", "## Stage notes"
)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "missing" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_entry_gate_fragment_lost(self) -> None:
mutated = self.sm.replace(
"refers to exactly ONE checkpoint: the **Stage 5 entry gate**",
"may refer to more than one checkpoint",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 3") and "entry-gate" in e for e in errors))
def test_inv3_completion_row_removed(self) -> None:
mutated = self.sm.replace(
"| Stage 5 | **checkpoint** | Stage 5 completed, Final Paper delivered |",
"| Stage 5 | END |",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 3") and "completion-checkpoint" in e for e in errors))
def test_inv3_completion_row_flipped_to_mandatory(self) -> None:
"""Adverse-value mutation (codex P1): the row survives as a prefix but
its outcome cell flips FULL to MANDATORY — must fire."""
mutated = self.sm.replace(
"Wait for user confirmation (FULL — never SLIM; see § Stage 5 boundary semantics)",
"Wait for user confirmation (MANDATORY; see § Stage 5 boundary semantics)",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "completion-checkpoint-row" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_not_on_mandatory_list_clause_deleted(self) -> None:
"""Adverse-value mutation (codex P1): deleting the 'not on the
MANDATORY list' clause from the authority section must fire."""
mutated = self.sm.replace(" — but it is not on the MANDATORY list", "")
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "completion-not-mandatory" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_skill_mandatory_cell_broadened(self) -> None:
"""The drift that motivated item 3: 'Stage 5' unqualified again."""
mutated = self.skill.replace(
"Stage 5 entry gate (before finalization)", "Stage 5"
)
errors = self._check(skill=mutated)
self.assertTrue(any(e.startswith("invariant 3") and self.mod.SKILL in e for e in errors))
def test_inv3_orchestrator_rule5_lost(self) -> None:
mutated = self.orch.replace(
"the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch",
"the finalization checkpoint",
)
errors = self._check(orch=mutated)
self.assertTrue(any(e.startswith("invariant 3") and self.mod.ORCH in e for e in errors))
def test_inv3_completion_sentence_lost_in_skill(self) -> None:
mutated = self.skill.replace(
"The Stage 5 completion checkpoint (Final Paper delivered, before Stage 6) is FULL — never SLIM",
"The Stage 5 completion checkpoint may be SLIM",
)
errors = self._check(skill=mutated)
self.assertTrue(any(e.startswith("invariant 3") and "completion-" in e for e in errors))
# --- INV-4: Stage 6 terminal semantics ---
def test_inv4_vocabulary_lost_in_authority_section(self) -> None:
"""Dropping one canonical token (`confirm`) from the authority section
must fire the section-scoped vocabulary literal."""
section = self._authority_section(self.sm)
mutated_section = section.replace(
"`finish` / `end` / `done` / `confirm`,", "`finish` / `end` / `done`,"
)
self.assertNotEqual(section, mutated_section)
mutated = self.sm.replace(section, mutated_section)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "acknowledgement-vocabulary" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_decline_path_fragment_lost(self) -> None:
mutated = self.sm.replace(
"marked `skipped` and the pipeline still terminates `completed`",
"an error",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 4") and "decline-path" in e for e in errors))
def test_inv4_terminal_row_removed(self) -> None:
mutated = self.sm.replace(
"| Stage 6 | **terminal checkpoint** | Process Record delivered |",
"| Stage 6 | END | done |",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 4") and "terminal-checkpoint-row" in e for e in errors))
def test_inv4_decline_row_removed(self) -> None:
mutated = self.sm.replace(
"| checkpoint | completed | User declines Stage 6 |",
"| checkpoint | completed | n/a |",
)
errors = self._check(sm=mutated)
self.assertTrue(any(e.startswith("invariant 4") and "decline-row" in e for e in errors))
def test_inv4_terminal_row_outcome_flipped_to_skipped(self) -> None:
"""Adverse-value mutation (codex P1): the terminal row survives as a
prefix but its action cell marks Stage 6 `skipped` — must fire."""
mutated = self.sm.replace(
"Mark Stage 6 `completed`; set pipeline global state `completed` |",
"Mark Stage 6 `skipped`; set pipeline global state `completed` |",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "terminal-transition-row" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_protocol_narrowed_to_exact_keywords(self) -> None:
"""Adverse-value mutation (codex P1): the protocol keeps the four
quoted keywords but drops the natural-language-equivalent clause —
must fire."""
mutated = self.proto.replace(
"unambiguous natural-language equivalent that accepts the deliverables",
"exact keyword from the list above (no paraphrase accepted)",
)
self.assertNotEqual(mutated, self.proto)
errors = self._check(proto=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and self.mod.PROTO in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_skill_vocab_lost(self) -> None:
mutated = self.skill.replace(
"`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent",
"any reply",
)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and self.mod.SKILL in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_protocol_vocab_lost(self) -> None:
mutated = self.proto.replace('"finish" / "end" / "done" / "confirm"', '"whatever"')
errors = self._check(proto=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and self.mod.PROTO in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_orchestrator_terminal_wiring_lost(self) -> None:
mutated = self.orch.replace('update_pipeline_state("completed")', "")
errors = self._check(orch=mutated)
self.assertTrue(any(e.startswith("invariant 4") and "pair missing" in e for e in errors))
def test_inv4_orchestrator_ack_wiring_flipped_to_skipped(self) -> None:
"""Adverse-value mutation (codex round-2 P1): the acknowledgement
branch flips Stage 6 to `skipped` while update_pipeline_state stays —
must fire."""
mutated = self.orch.replace(
'`update_stage("6", "completed", outputs)` + `update_pipeline_state("completed")`',
'`update_stage("6", "skipped", {})` + `update_pipeline_state("completed")`',
)
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "acknowledgement-wiring" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_latex_moved_to_entry_gate(self) -> None:
"""Adverse-value mutation (codex round-2 P1): the authority section
adds LaTeX to the gate decision and drops it from the in-stage
clause — must fire (this is the P1-1 contradiction reopening)."""
mutated = self.sm.replace(
"the finalization-format decision: citation style (APA 7.0 / Chicago / IEEE, ...)",
"the finalization-format decisions: citation style and whether to generate LaTeX (APA 7.0 / Chicago / IEEE, ...)",
).replace('the "Need LaTeX?" question (Step 3) and ', "")
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "gate-format-decision" in e for e in errors),
msg=f"errors: {errors}",
)
self.assertTrue(
any(e.startswith("invariant 3") and "latex-in-stage" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_gate_scope_clause_lost_in_skill(self) -> None:
mutated = self.skill.replace(
"makes the finalization-format decision (citation style); the in-stage LaTeX question and content confirmation stay inside Stage 5 execution",
"makes all format decisions including LaTeX",
)
self.assertNotEqual(mutated, self.skill)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "gate-scope" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_change_requests_flipped_in_authority(self) -> None:
"""Adverse-value mutation (codex round-2 P1): change requests
reclassified as acknowledgements in the authority section — must fire."""
mutated = self.sm.replace(
"keep Stage 6 `in_progress` — they are not acknowledgements",
"immediately complete Stage 6",
)
self.assertNotEqual(mutated, self.sm)
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "change-requests-not-ack" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_change_requests_flipped_in_skill_and_proto(self) -> None:
skill_mut = self.skill.replace(
"keep Stage 6 `in_progress` and are not acknowledgements",
"complete Stage 6 immediately",
)
proto_mut = self.proto.replace(
"Stage 6 in_progress — they are not acknowledgements",
"Stage 6 completed",
)
self.assertNotEqual(skill_mut, self.skill)
self.assertNotEqual(proto_mut, self.proto)
errors_skill = self._check(skill=skill_mut)
errors_proto = self._check(proto=proto_mut)
self.assertTrue(
any(e.startswith("invariant 4") and "change-requests-not-ack" in e and self.mod.SKILL in e for e in errors_skill),
msg=f"errors: {errors_skill}",
)
self.assertTrue(
any(e.startswith("invariant 4") and "non-acknowledgement" in e and self.mod.PROTO in e for e in errors_proto),
msg=f"errors: {errors_proto}",
)
# --- codex round-3 witnesses ---
def test_inv3_full_row_reclaims_before_finalization(self) -> None:
"""Adverse-value mutation (codex round-3 P1): the FULL checkpoint-type
row reclaims 'before finalization' (colliding with MANDATORY) — must
fire on both mirrors."""
for name, text, kw in (("skill", self.skill, "skill"), ("orch", self.orch, "orch")):
mutated = text.replace(
"| FULL | First checkpoint; after integrity boundaries; Stage 5 completion (final-deliverable acceptance) |",
"| FULL | First checkpoint; after integrity boundaries; before finalization (Stage 5 entry gate) |",
)
self.assertNotEqual(mutated, text, msg=name)
errors = self._check(**{kw: mutated})
self.assertTrue(
any(e.startswith("invariant 3") and "FULL checkpoint-type row" in e for e in errors),
msg=f"{name} errors: {errors}",
)
def test_inv3_skill_step1_reasks_citation_style(self) -> None:
"""Adverse-value mutation (codex round-3 P1): Stage 5 execution Step 1
reverts to always asking the citation style — must fire."""
mutated = self.skill.replace(
"Consume the citation-style decision recorded at the Stage 5 entry gate",
"Ask user which academic formatting style",
)
self.assertNotEqual(mutated, self.skill)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "Step 1" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_decline_flipped_on_mirrors(self) -> None:
"""Adverse-value mutation (codex round-3 P1): a mirror surface makes
Stage 6 mandatory / non-declinable — must fire per surface."""
skill_mut = self.skill.replace(
"the user may decline it at the Stage 5 completion checkpoint (Stage 6 marked `skipped`; the pipeline still terminates `completed`)",
"Stage 6 is mandatory and may not be declined",
)
orch_mut = self.orch.replace(
"User may decline Stage 6 there: mark it `skipped`, set pipeline state `completed`",
"Stage 6 may not be declined",
)
proto_mut = self.proto.replace(
"the user may decline it at that checkpoint; it is then marked `skipped` and the pipeline still terminates `completed`",
"Stage 6 is mandatory; may not decline",
)
for kw, mut, orig in (("skill", skill_mut, self.skill),
("orch", orch_mut, self.orch),
("proto", proto_mut, self.proto)):
self.assertNotEqual(mut, orig, msg=kw)
errors = self._check(**{kw: mut})
self.assertTrue(
any(e.startswith("invariant 4") and "decline" in e for e in errors),
msg=f"{kw} errors: {errors}",
)
def test_inv4_skill_rule10_pin_lost(self) -> None:
section_heading = "## Stage 6: Process Summary Protocol"
rule10 = "terminal acknowledgement (`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent) -> pipeline global state `completed`"
mutated = self.skill.replace(rule10, "terminal acknowledgement -> end")
self.assertNotEqual(mutated, self.skill)
self.assertIn(section_heading, mutated) # the section copy is untouched
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "rule 10" in e for e in errors),
msg=f"errors: {errors}",
)
# --- codex round-4 witnesses ---
def test_inv3_mandatory_type_flipped_to_full(self) -> None:
"""Adverse-value mutation (codex round-4 P1): the entry gate's
MANDATORY classification flips to FULL on either declaration —
must fire independently."""
table_mut = self.skill.replace(
"| MANDATORY | Integrity FAIL; Review decision; Stage 5 entry gate (before finalization) |",
"| MANDATORY | Integrity FAIL; Review decision |",
)
rule_mut = self.orch.replace(
"always MANDATORY — this is the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch",
"always FULL — this is the checkpoint between Stage 4.5 PASS and the Stage 5 dispatch",
)
self.assertNotEqual(table_mut, self.skill)
self.assertNotEqual(rule_mut, self.orch)
errors_table = self._check(skill=table_mut)
errors_rule = self._check(orch=rule_mut)
self.assertTrue(
any(e.startswith("invariant 3") and self.mod.SKILL in e for e in errors_table),
msg=f"errors: {errors_table}",
)
self.assertTrue(
any(e.startswith("invariant 3") and self.mod.ORCH in e for e in errors_rule),
msg=f"errors: {errors_rule}",
)
def test_inv4_ack_outcome_flipped_per_surface(self) -> None:
"""Adverse-value mutation (codex round-4 P1): the acknowledgement
outcome flips completed→skipped on each operative copy — must fire
per surface independently."""
sm_mut = self.sm.replace(
"On acknowledgement: state_tracker marks Stage 6 `completed` and sets the pipeline global state to `completed`",
"On acknowledgement: state_tracker marks Stage 6 `skipped` and sets the pipeline global state to `completed`",
)
skill_mut = self.skill.replace(
"On acknowledgement, Stage 6 is marked `completed` and the pipeline global state is set to `completed`",
"On acknowledgement, Stage 6 is marked `skipped` and the pipeline global state is set to `completed`",
)
proto_mut = self.proto.replace(
"On acknowledgement: state_tracker marks Stage 6 completed and sets the pipeline global state to completed",
"On acknowledgement: state_tracker marks Stage 6 skipped and sets the pipeline global state to completed",
)
for kw, mut, orig in (("sm", sm_mut, self.sm),
("skill", skill_mut, self.skill),
("proto", proto_mut, self.proto)):
self.assertNotEqual(mut, orig, msg=kw)
errors = self._check(**{kw: mut})
self.assertTrue(
any(e.startswith("invariant 4") and ("ack-outcome" in e or "outcome" in e) for e in errors),
msg=f"{kw} errors: {errors}",
)
def test_inv4_tracker_decline_outcome_flipped(self) -> None:
mutated = self.tracker.replace(
'`update_stage("6", "skipped", {reason: "user declined Stage 6"})` then `update_pipeline_state("completed")`',
'`update_stage("6", "completed", {reason: "user declined Stage 6"})` then `update_pipeline_state("completed")`',
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "decline-outcome" in e for e in errors),
msg=f"errors: {errors}",
)
# --- codex round-8 witnesses ---
def test_inv1_tracker_prereq_blueprint_dropped(self) -> None:
"""Adverse-value mutation (codex round-8 P1): check_prerequisites
Stage 2 row drops the Methodology Blueprint — must fire."""
mutated = self.tracker.replace(
"| Stage 2 | None (but Stage 1 output recommended) | RQ Brief, Methodology Blueprint, Bibliography, Synthesis |",
"| Stage 2 | None (but Stage 1 output recommended) | RQ Brief, Bibliography, Synthesis |",
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 1") and self.mod.TRACKER in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_tracker_prereq_stage6_row_dropped(self) -> None:
mutated = self.tracker.replace(
"| Stage 6 | None (Final Paper already delivered at Stage 5) |",
"| Stage 7 | None |",
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "Stage 6 row" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_orch_stage6_moved_to_non_skippable(self) -> None:
"""Adverse-value mutation (codex round-8 P1): Stage 6 leaves the
skippable list (the skip validator would then reject the pinned
decline path) — must fire."""
mutated = self.orch.replace(
", Stage 6 (process summary — declined at the Stage 5 completion checkpoint; marked `skipped`, pipeline still terminates `completed`)",
"",
)
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "skippable-stages" in e for e in errors),
msg=f"errors: {errors}",
)
# --- codex round-9 witnesses ---
def test_inv3_engagement_full_exception_dropped(self) -> None:
"""Adverse-value mutation (codex round-9 P1): the SLIM engagement
downgrade loses its FULL-checkpoint exception — must fire."""
mutated = self.orch.replace(
"the Stage 5 completion checkpoint is FULL — never SLIM, regardless of the continue count",
"all non-MANDATORY checkpoints downgrade to SLIM",
)
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 3") and "engagement" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_non_skippable_gains_stage6(self) -> None:
"""Adverse-value mutation (codex round-9 P1): Stage 6 is added to the
non-skippable side while the skippable declaration stays — must fire."""
mutated = self.orch.replace(
"- Non-Skippable: Stage 2 (writing), Stage 2.5 (pre-review integrity), Stage 3 (initial review), Stage 4.5 (final integrity), Stage 5 (finalize)",
"- Non-Skippable: Stage 2 (writing), Stage 2.5 (pre-review integrity), Stage 3 (initial review), Stage 4.5 (final integrity), Stage 5 (finalize), Stage 6 (process summary)",
)
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "non-skippable" in e.lower() for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_tracker_approval_gate_flipped(self) -> None:
"""Adverse-value mutation (codex round-9 P1): the Stage 6 SSOT block's
approval_gate flips to false — must fire."""
block = self.mod.TRACKER_STAGE6_ENTRY
self.assertIn(block, self.tracker)
mutated = self.tracker.replace(
block, block.replace('"approval_gate": true', '"approval_gate": false')
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "Stage 6 entry" in e for e in errors),
msg=f"errors: {errors}",
)
# --- codex round-10 witnesses ---
def test_inv4_tracker_status_enum_renamed(self) -> None:
"""Adverse-value mutation (codex round-10 P1): the tracker renames
`skipped` in its status enum — the pinned decline wiring would then
be rejected by the consumer — must fire."""
mutated = self.tracker.replace(
'| status | "pending", "in_progress", "completed", "skipped", "blocked" |',
'| status | "pending", "in_progress", "completed", "declined", "blocked" |',
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "status enum" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_tracker_global_completed_renamed(self) -> None:
mutated = self.tracker.replace("- `completed`", "- `finished`")
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "legal" in e and "completed" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_tracker_stage6_dropped(self) -> None:
"""Adverse-value mutation (codex round-3 P1): the state_tracker enum
reverts to '1'..'5' — must fire."""
mutated = self.tracker.replace(
'"1", "2", "2.5", "3", "4", "3p", "4p", "4.5", "5", "6"',
'"1", "2", "2.5", "3", "4", "3p", "4p", "4.5", "5"',
)
self.assertNotEqual(mutated, self.tracker)
errors = self._check(tracker=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "stage_id enum" in e for e in errors),
msg=f"errors: {errors}",
)
mutated2 = self.tracker.replace('"6": {', '"6x": {')
errors2 = self._check(tracker=mutated2)
self.assertTrue(
any(e.startswith("invariant 4") and "Stage 6 entry" in e for e in errors2),
msg=f"errors: {errors2}",
)
# --- codex round-11 witnesses ---
def test_inv4_skill_stage6_made_mandatory_in_section(self) -> None:
"""Adverse-value mutation (codex round-11 P1): the SKILL Stage 6
section flips non-mandatory to mandatory — must fire."""
mutated = self.skill.replace(
"Stage 6 is non-mandatory — the user may decline it",
"Stage 6 is mandatory — the user may not decline it",
)
self.assertNotEqual(mutated, self.skill)
errors = self._check(skill=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "decline-path" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_proto_stage6_made_mandatory(self) -> None:
mutated = self.proto.replace(
"Stage 6 is non-mandatory — the user may decline it at that checkpoint",
"Stage 6 is mandatory — the user may not decline it",
)
self.assertNotEqual(mutated, self.proto)
errors = self._check(proto=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "decline" in e and self.mod.PROTO in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_skippable_label_flipped(self) -> None:
"""Adverse-value mutation (codex round-11 P1): the `- Skippable:`
label flips to `- Non-Skippable:` — must fire (line-anchored pin
includes the label)."""
mutated = self.orch.replace(
"- Skippable: Stage 1 (deep-research, if user provides own bibliography)",
"- Non-Skippable: Stage 1 (deep-research, if user provides own bibliography)",
)
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "skippable-stages" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv4_handoff_destination_rerouted(self) -> None:
"""Adverse-value mutation (codex round-11 P1): the handoff row's
first cell reroutes Stage 5 -> END — must fire."""
mutated = self.orch.replace("| Stage 5 -> 6 | Final deliverables", "| Stage 5 -> END | Final deliverables")
self.assertNotEqual(mutated, self.orch)
errors = self._check(orch=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "handoff row" in e for e in errors),
msg=f"errors: {errors}",
)
def test_inv3_reset_iron_rule_names_entry_gate(self) -> None:
"""The reset-boundary iron rule must scope its MANDATORY list to the
Stage 5 entry gate (codex round-11 P1) — enforced via the content
lock being on the state machine? No: the rule lives in the
orchestrator; verify the committed prose carries the scoped form."""
self.assertIn(
"MANDATORY checkpoints (Stage 2.5 / 4.5, review decisions, the Stage 5 entry gate) remain MANDATORY",
self.orch,
)
# --- content locks (round-11 endgame) ---
def test_content_locks_baseline(self) -> None:
self.assertEqual(self.mod.check_content_locks(), [])
def test_content_lock_fires_on_any_byte_change(self) -> None:
"""Any single-byte change to a locked reference doc must fire — this
closes the entire single-edit mutation class for the two
#528-central operative docs."""
import hashlib
for path in self.mod.CONTENT_LOCKS:
content = (REPO_ROOT / path).read_bytes()
mutated = content.replace(b"completed", b"finished", 1)
self.assertNotEqual(mutated, content, msg=path)
self.assertNotEqual(
hashlib.sha256(mutated).hexdigest(),
self.mod.CONTENT_LOCKS[path],
msg=path,
)
# --- scoping discipline ---
def test_vocab_elsewhere_does_not_satisfy_section_check(self) -> None:
"""Removing the vocabulary from the authority section must fire even
though the transitions table (outside the section) still carries the
same canonical string — section-scoped means section-scoped."""
section = self._authority_section(self.sm)
mutated_section = section.replace(
"`finish` / `end` / `done` / `confirm`, or an unambiguous natural-language equivalent",
"an explicit closing reply",
)
self.assertNotEqual(section, mutated_section)
mutated = self.sm.replace(section, mutated_section)
self.assertIn(self.mod.VOCAB_CANON, mutated) # still present outside the section
errors = self._check(sm=mutated)
self.assertTrue(
any(e.startswith("invariant 4") and "acknowledgement-vocabulary" in e for e in errors),
msg=f"errors: {errors}",
)
if __name__ == "__main__":
unittest.main()