Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions checks/progress.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,13 @@ export function derive({ scopeItems, specs: allSpecs }) {
let state = 'todo';
if (mine.some((s) => s.status === SPEC_DONE)) state = 'done';
else if (mine.length) state = 'doing';
if (mine.length > 1) {
warnings.push({ kind: 'doubleClaim', title: item.title, specs: mine.map((s) => s.file) });
// Only plans still in flight can be working on it at once. A finished spec has shipped its
// part and a later spec supersedes that claim, which is ordinary history: a scope item worth
// two rounds of work carries two specs forever, and warning about it every run teaches the
// reader to skim the heads-up. Two unfinished plans on one item is the real clash.
const together = mine.filter((s) => s.status !== SPEC_DONE);
if (together.length > 1) {
warnings.push({ kind: 'doubleClaim', title: item.title, specs: together.map((s) => s.file) });
}
return { ...item, state, specs: mine.map((s) => s.file) };
});
Expand Down
30 changes: 27 additions & 3 deletions checks/progress.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,40 @@ test('a spec pointing at an unknown scope item is surfaced, not swallowed', () =
assert.deepEqual(p.warnings, [{ kind: 'unknownItem', spec: 'ghost' }]);
});

test('two specs claiming one scope item: the furthest wins and the clash is reported', () => {
test('two unfinished specs claiming one scope item: the furthest wins and the clash is reported', () => {
const p = derive({
scopeItems: items,
specs: [
{ file: 'a', status: 'building', traces: ['SC-1'] },
{ file: 'b', status: 'approved', traces: ['SC-1'] },
],
});
assert.equal(p.items[0].state, 'doing');
assert.deepEqual(p.warnings, [{ kind: 'doubleClaim', title: items[0].title, specs: ['a', 'b'] }]);
});

test('a later spec supersedes a finished claim: the item still counts done, and nobody is warned', () => {
const p = derive({
scopeItems: items,
specs: [
{ file: 'shipped-it-first', status: 'done', traces: ['SC-1'] },
{ file: 'doing-it-again', status: 'building', traces: ['SC-1'] },
],
});
assert.equal(p.items[0].state, 'done');
assert.deepEqual(p.warnings, []);
});

test('two finished specs on one scope item are history, not a clash', () => {
const p = derive({
scopeItems: items,
specs: [
{ file: 'a', status: 'done', traces: ['SC-1'] },
{ file: 'b', status: 'done', traces: ['SC-1'] },
],
});
assert.equal(p.items[0].state, 'done');
assert.deepEqual(p.warnings, [{ kind: 'doubleClaim', title: items[0].title, specs: ['a', 'b'] }]);
assert.deepEqual(p.warnings, []);
});

test('no scope items means not defined, never a zero count', () => {
Expand Down Expand Up @@ -167,7 +191,7 @@ test('warnings speak the project language and name no internal identifiers', ()

const clash = derive({
scopeItems: items,
specs: [{ file: 'a', status: 'building', traces: ['SC-1'] }, { file: 'b', status: 'done', traces: ['SC-1'] }],
specs: [{ file: 'a', status: 'building', traces: ['SC-1'] }, { file: 'b', status: 'draft', traces: ['SC-1'] }],
});
const en = renderFull(project({ lang: 'en' }), clash);
assert.match(en, /is being worked on from 2 plans at once \(a, b\)/);
Expand Down