* test(#4011): TDD gate keys on TDD_MODE alone, not the MVP intersection Contract updates: no shipped line may conjoin MVP_MODE with TDD_MODE as a gate condition, the end-of-phase escalation must not require MVP, the executor agent's gate section triggers on TDD_MODE alone, and the gate semantics reference loads without MVP_MODE. * fix(#4011): key the TDD runtime gate on TDD_MODE alone The RED-commit gate shipped as #76's MVP slice kept the paired invocation's conjunct, so workflow.tdd_mode=true was silently inert on every non-MVP phase, contradicting references/tdd.md's own contract. Drops the MVP conjunct from the per-task gate and the end-of-phase review escalation; rescopes execute-mvp-tdd.md's load condition, gsd-executor's gate section, and mvp-concepts' intersection claim. MVP remains free to imply TDD; the file is not renamed (stated assumption in the PR body). * test(#4011): scope no-conjunct detector to shell conditions; clean stale MVP+TDD phrasing Review follow-ups: the detector now only inspects if/[ condition lines so explanatory prose mentioning both flags cannot trip it; remaining 'under/outside MVP+TDD' phrases in execute-phase.md, the gate reference, and docs/INVENTORY.md now describe TDD-mode semantics. Emitted-Drift-Ack-Growth: execute-phase.md — TDD-gate decoupling comment + escalation rescoping (#4011) Emitted-Drift-Ack-Growth: gsd-executor.md — gate section trigger rescoped to TDD_MODE alone (#4011) * chore(#4011): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -34,14 +34,21 @@ function parseGateContract(content) {
|
||||
return {
|
||||
hasMvpModeVariable: lowerLines.some(line => line.includes('mvp_mode')),
|
||||
hasRoadmapModeResolution: lowerLines.some(line => line.includes('phase.mvp-mode') || line.includes('roadmap') && line.includes('mode')),
|
||||
hasDualGateCondition: lowerLines.some(line => line.includes('mvp_mode') && line.includes('tdd_mode')),
|
||||
hasGateLabel: lowerLines.some(line => line.includes('mvp+tdd gate') || line.includes('mvp-tdd gate')),
|
||||
// #4011: the gate keys on TDD_MODE ALONE. Any line conjoining MVP_MODE and
|
||||
// TDD_MODE re-couples a discipline gate to a product-scope flag and makes
|
||||
// workflow.tdd_mode=true silently inert on non-MVP phases.
|
||||
hasTddOnlyGateCondition: lines.some(line => /\$TDD_MODE.{0,40}=.{0,20}"true"/.test(line) && !/MVP_MODE/.test(line)),
|
||||
// Scoped to shell-condition lines only — explanatory prose legitimately
|
||||
// mentions both flags on one line.
|
||||
hasNoMvpTddConjunct: !lines.some(line =>
|
||||
(/if \[|\] &&|&& \[/.test(line)) && /MVP_MODE/.test(line) && /TDD_MODE/.test(line)),
|
||||
hasTddOnlyEscalation: !/mvp_mode\s*(=|&&|and)/i.test((content.split('tdd review escalation')[1] || '') ),
|
||||
hasGateLabel: lowerLines.some(line => line.includes('mvp+tdd gate') || line.includes('mvp-tdd gate') || line.includes('tdd gate')),
|
||||
hasRedCommitRule: lowerLines.some(line => line.includes('failing-test commit') || line.includes('missing red commit') || line.includes('test(')),
|
||||
// Must assert the REAL refusal semantics, not merely the words "blocking" + "mvp+tdd"
|
||||
// (which "advisory (blocking: false) ... under MVP+TDD" would satisfy as a false green).
|
||||
hasBlockingEscalation:
|
||||
content.toLowerCase().includes('mvp+tdd')
|
||||
&& (content.toLowerCase().includes('refuse to mark the phase complete')
|
||||
(content.toLowerCase().includes('refuse to mark the phase complete')
|
||||
|| content.toLowerCase().includes('phase blocked')),
|
||||
hasReferenceDoc: lowerLines.some(line => line.includes('execute-mvp-tdd.md')),
|
||||
// Must NOT have an unconditional "proceed regardless of gate results" that overrides the block.
|
||||
@@ -58,8 +65,10 @@ describe('execute-phase — MVP+TDD gate', () => {
|
||||
assert.ok(contract.hasRoadmapModeResolution, 'must consult phase mode from roadmap');
|
||||
});
|
||||
|
||||
test('gate fires when both MVP_MODE and TDD_MODE are true', () => {
|
||||
assert.ok(contract.hasDualGateCondition, 'workflow must combine MVP_MODE and TDD_MODE for the gate');
|
||||
test('gate fires on TDD_MODE alone — no MVP conjunct (#4011)', () => {
|
||||
assert.ok(contract.hasTddOnlyGateCondition, 'the per-task gate must key on TDD_MODE alone');
|
||||
assert.ok(contract.hasNoMvpTddConjunct,
|
||||
'no shipped line may conjoin MVP_MODE with TDD_MODE as a gate condition — workflow.tdd_mode=true must enforce TDD on non-MVP phases too (#4011)');
|
||||
});
|
||||
|
||||
test('per-task gate is documented before behavior-adding task execution', () => {
|
||||
@@ -67,8 +76,9 @@ describe('execute-phase — MVP+TDD gate', () => {
|
||||
assert.ok(contract.hasRedCommitRule, 'must reference failing-test commit check');
|
||||
});
|
||||
|
||||
test('end-of-phase TDD review escalates to blocking under MVP+TDD', () => {
|
||||
test('end-of-phase TDD review escalates to blocking under TDD (#4011)', () => {
|
||||
assert.ok(contract.hasBlockingEscalation, 'must escalate end-of-phase review to blocking');
|
||||
assert.ok(contract.hasTddOnlyEscalation, 'the escalation must not require MVP_MODE (#4011)');
|
||||
});
|
||||
|
||||
test('proceed past TDD escalation is conditional — not an unconditional override', () => {
|
||||
@@ -115,3 +125,17 @@ describe('execute-phase MVP+TDD — resolution chain integration', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #4011: the gate semantics reference loads on TDD_MODE alone ─────────────
|
||||
|
||||
describe('execute-mvp-tdd.md — load condition decoupled from MVP (#4011)', () => {
|
||||
const REF = path.join(__dirname, '..', 'gsd-core', 'references', 'execute-mvp-tdd.md');
|
||||
const content = fs.readFileSync(REF, 'utf-8');
|
||||
|
||||
test('reference no longer requires MVP_MODE to load', () => {
|
||||
const head = content.slice(0, 1200);
|
||||
assert.ok(!/MVP_MODE\s*=\s*true\s*(AND|&&|and)\s*TDD_MODE|both.{0,20}MVP_MODE\s*=\s*true\s*AND/i.test(head),
|
||||
'execute-mvp-tdd.md must load on TDD_MODE alone (#4011)');
|
||||
assert.ok(/TDD_MODE\s*=\s*true/i.test(head), 'load condition must still name TDD_MODE');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,16 @@ const REF = path.join(__dirname, '..', 'gsd-core', 'references', 'execute-mvp-td
|
||||
describe('gsd-executor — MVP+TDD gate section', () => {
|
||||
const content = fs.readFileSync(AGENT, 'utf-8');
|
||||
|
||||
test('agent defines an MVP+TDD Gate section', () => {
|
||||
assert.match(content, /MVP\+TDD\s*Gate|MVP[\s-]?TDD[\s-]?gate/i, 'must label the gate');
|
||||
test('agent defines a TDD Gate section keyed on TDD_MODE alone (#4011)', () => {
|
||||
assert.match(content, /MVP\+TDD\s*Gate|MVP[\s-]?TDD[\s-]?gate|TDD\s*Gate/i, 'must label the gate');
|
||||
// The gate's trigger must not require MVP_MODE (#4011): a discipline gate
|
||||
// keyed to a product-scope flag is silently inert on non-MVP phases.
|
||||
const gateSection = content.slice(
|
||||
content.search(/## (?:MVP\+TDD )?TDD Gate/i),
|
||||
content.indexOf('##', content.search(/## (?:MVP\+TDD )?TDD Gate/i) + 3),
|
||||
);
|
||||
assert.ok(!/MVP_MODE\s*=\s*"?"?true"?.{0,80}TDD_MODE|both .MVP_MODE.= true and .TDD_MODE.= true/i.test(gateSection),
|
||||
'the executor gate section must trigger on TDD_MODE alone, not the MVP intersection (#4011)');
|
||||
});
|
||||
|
||||
test('agent instructs halt-and-report when gate trips', () => {
|
||||
|
||||
Reference in New Issue
Block a user