test(W7c): ship-reentry-gates keeps stop/consent meaning, drops sentence pins

W7c (t08 F3). One test file.

Removed assertion groups (invariant -> remaining protection):
- "Retain queued fixes and restore coverage" -> a missing reviewer keeps
  queued fixes: gate E2E ship-skipped-queued-finding (gate) and judge
  `ship/SKILL.md workflow` (gate lane); the stop itself stays a free check.
- qa decline print line ("Test bootstrap previously declined — skipping.")
  -> qa skips bootstrap after a saved decline: kept free meaning check
  `/BOOTSTRAP_DECLINED ... skip the rest of bootstrap/i`.
- "with the user's free-text evidence" -> UNVERIFIABLE items are confirmed
  one by one: kept free `/per-item confirmation/i` here plus the
  blanket-confirm check in ship-plan-completion-invariants.
- Docs reentry sentences ("On reentry, reuse only ...", its duplicate, "base/
  input hashes still match", "retain its actual status and scope", "Otherwise
  use Blocked recovery ...") -> reentry reuses only this invocation's
  validated evidence: kept free checks "never reuse an audit across
  invocations" and "reentry never resets the count or authorizes a launch";
  gate E2Es ship-docsync-stale-before, ship-docsync-stale-after,
  ship-docsync-recovery, ship-docsync-late-result (all gate).
- "left uncommitted after Step 15" -> late commits take only remaining
  changes: kept free "never create an empty commit" and "preserve unrelated
  user files"; gate E2E ship-local-workflow (gate).

Loosened to case-insensitive meaning checks (stop/consent/approval lines,
always kept): STOP blocks advancement, answer each AskUserQuestion, routine
authorization never waives gates, dispatched-reviewer-missing -> stop,
CYCLES >= 3 -> stop, Any N -> stop as NOT DONE, resume only after verified,
no second deferral, per-item confirmation, explicit Add-tests choice overrides
the decline for this invocation only, framework approval, first entry
launches the audit, no third attempt, unwaivable redaction/security gates.

Kept unchanged: bootstrap branch order, no `rm` in the decline branch, the
Step 14 -> 14.5 routing line, stale-string bans, `NEW_TITLE` field and
`--title "$NEW_TITLE"`.

Cosmetic pins dropped: STOP capitals, "mandatory".

Mutation log: .context/mutation-ship-reentry-gates.md (17 mutations, 0
unexpected: 1 reorder, 2 marker and 9 safety mutations red; 5 rewordings green).
This commit is contained in:
garrytan committed 2026-10-03 01:05:00 +00:00
1 parent 8b1e3423a7
commit 356f7cb6ce
1 file changed
+23 -31
+23 -31
View File
@@ -8,69 +8,61 @@ const read = (file: string) => readFileSync(new URL(`../${file}`, import.meta.ur
const compact = (text: string) => text.replace(/\s+/g, ' ');
const ctx = { host: 'claude' as const, skillName: 'ship', tmplPath: '', paths: HOST_PATHS.claude };
test('ship STOP blocks advancement while retaining the stated repair route', () => {
test('ship stop lines block advancement while retaining the stated repair route', () => {
const entry = compact(read('ship/SKILL.md.tmpl'));
expect(entry).toContain('STOP blocks advancement until the stated repair/resume route clears; without one, end this attempt');
expect(entry).toContain('Answer each AskUserQuestion before continuing');
expect(entry).toMatch(/stop blocks advancement until the stated repair\/resume route clears; without one, end this attempt/i);
expect(entry).toMatch(/answer each AskUserQuestion before continuing/i);
expect(entry).toMatch(/routine authorization never waives those gates or their required user decisions/i);
const review = compact(read('ship/sections/review-army.md.tmpl'));
expect(review).toContain('**Dispatched reviewer output missing:** STOP');
expect(review).toContain('Retain queued fixes and restore coverage');
expect(review).toContain('**Third fixing cycle reached (`CYCLES >= 3`):** STOP');
expect(entry).toContain('Routine authorization never waives those gates or their required user decisions');
expect(review).toMatch(/\*\*Dispatched reviewer output missing:\*\* stop\b/i);
expect(review).toMatch(/\*\*Third fixing cycle reached \(`CYCLES >= 3`\):\*\* stop\b/i);
});
test('a new ship bootstrap choice overrides only the saved decline, not framework selection', () => {
const ship = generateTestBootstrap(ctx);
const decline = ship.slice(ship.indexOf('**If BOOTSTRAP_DECLINED**'), ship.indexOf('**If NO ecosystem marker matched:**'));
expect(compact(decline)).toContain("Step 5's explicit Add tests choice overrides that marker for this invocation only");
expect(compact(decline)).toContain('continue to runtime detection and B2–B3, including framework approval');
expect(compact(decline)).toMatch(/explicit Add tests choice overrides that marker for this invocation only/i);
expect(compact(decline)).toMatch(/including framework approval/i);
expect(decline).not.toContain('rm ');
expect(ship.indexOf('**If ANY existing-test evidence appears**')).toBeLessThan(ship.indexOf('**If BOOTSTRAP_DECLINED**'));
const qa = generateTestBootstrap({ ...ctx, skillName: 'qa' });
expect(qa).toContain('**If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.**');
expect(compact(qa)).toMatch(/BOOTSTRAP_DECLINED[^\n]{0,120}skip the rest of bootstrap/i);
expect(qa).not.toContain("Step 5's explicit Add tests choice");
});
test('an unverified item answered not done enters the existing decision before continuing', () => {
const gate = generatePlanCompletionGateShip(ctx);
const exits = compact(gate.slice(gate.indexOf('**Exit conditions:**'), gate.indexOf('**Cap.**')));
expect(exits).toContain('Any N: STOP and report that item as NOT DONE');
expect(exits).toContain('Resume only after its required work is verified');
expect(exits).toContain('no second deferral choice');
expect(exits).toMatch(/any N: stop and report that item as NOT DONE/i);
expect(exits).toMatch(/resume only after its required work is verified/i);
expect(exits).toMatch(/no second deferral choice/i);
expect(exits).not.toContain('re-running /ship');
expect(gate).toContain('Per-item confirmation is mandatory');
expect(gate).toContain('with the user\'s free-text evidence');
expect(gate).toMatch(/per-item confirmation/i);
});
test('docs reentry distinguishes the initial audit from same-invocation accepted evidence', () => {
const docs = compact(read('ship/sections/documentation.md.tmpl'));
const entry = docs.slice(0, docs.indexOf('## Prepare the candidate'));
expect(entry).toContain('First entry always launches the initial audit');
expect(entry).toContain('On reentry, reuse only this invocation\'s validated audit or named-risk decision');
expect(entry).toContain('Reentry never resets the count or authorizes a launch');
expect(entry).toContain("this invocation's validated audit or named-risk decision");
expect(entry).toContain('base/input hashes still match');
expect(entry).toContain('retain its actual status and scope');
expect(entry).toContain('Otherwise use Blocked recovery, not an unconditional launch');
expect(docs).toContain('never a third attempt, even after Step 16 changes');
expect(docs).toContain('never reuse an audit across invocations');
expect(docs).toContain('Unconfirmed writers, ownership violations, unauthorized Git mutation and redaction/security gates cannot be waived');
expect(entry).toMatch(/first entry always launches the initial audit/i);
expect(entry).toMatch(/reentry never resets the count or authorizes a launch/i);
expect(docs).toMatch(/never a third attempt/i);
expect(docs).toMatch(/never reuse an audit across invocations/i);
expect(docs).toMatch(/unauthorized Git mutation and redaction\/security gates cannot be waived/i);
});
test('late behavioral repairs rebuild before the docs decision and commit only remaining changes', () => {
const entry = compact(read('ship/SKILL.md.tmpl'));
expect(entry).toContain('A range ending at Step 14 does not enter Step 14.5');
expect(entry).toContain('rebuild and compare again before stage 3 decides documentation freshness');
expect(entry).toMatch(/rebuild and compare again before stage 3/i);
const commit = entry.slice(entry.indexOf('### 5. Report, then push'), entry.indexOf('## Step 17:'));
expect(commit).toContain('left uncommitted after Step 15');
expect(commit).toContain('never create an empty commit');
expect(commit).toContain('Preserve unrelated user files');
expect(commit).toMatch(/never create an empty commit/i);
expect(commit).toMatch(/preserve unrelated user files/i);
});
test('the title is prefixed once before the exact scanned value is published', () => {
const body = compact(read('ship/sections/pr-body.md.tmpl'));
expect(body).toContain("Use Step 18's `NEW_TITLE` unchanged; its version prefix is already present");
expect(body).toContain("Step 18's `NEW_TITLE` unchanged");
expect(body).not.toContain('`NEW_TITLE`, prefixed with');
expect(body).toContain('--title "$NEW_TITLE"');
expect(body).toContain('the same scanned `NEW_TITLE`');
expect(body).toContain('scanned `NEW_TITLE`');
});