From 12022f35c3e6aa4ecd13ef5e6eca13f3a1d098ef Mon Sep 17 00:00:00 2001 From: Tyagiquamar Date: Fri, 18 Sep 2026 12:40:44 +0530 Subject: [PATCH 1/3] fix(validate): require marker punctuation after a leading TBD/TODO Closes #1897 --- .changeset/purpose-marker-punctuation.md | 7 +++++++ src/core/validation/purpose-placeholder.ts | 23 ++++++++++++++-------- test/core/purpose-placeholder.test.ts | 17 ++++++++++++++++ 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 .changeset/purpose-marker-punctuation.md diff --git a/.changeset/purpose-marker-punctuation.md b/.changeset/purpose-marker-punctuation.md new file mode 100644 index 0000000000..ce1ed9e7ed --- /dev/null +++ b/.changeset/purpose-marker-punctuation.md @@ -0,0 +1,7 @@ +--- +'@fission-ai/openspec': patch +--- + +Fix `validate --strict` reporting `PURPOSE_IS_PLACEHOLDER` for a Purpose that opens with the ordinary word "Todo" followed by prose, as in Spanish ("Todo el…") and Portuguese ("Todo o…") specs ([#1897](https://github.com/Fission-AI/OpenSpec/issues/1897)). + +- A leading `TBD`/`TODO` now only counts as a placeholder marker when it is followed by the end of the Purpose, a line break, or marker punctuation (`TODO:`, `TODO -`, `TBD.`), never by prose. diff --git a/src/core/validation/purpose-placeholder.ts b/src/core/validation/purpose-placeholder.ts index 5fa3060df0..e74e5e1e1b 100644 --- a/src/core/validation/purpose-placeholder.ts +++ b/src/core/validation/purpose-placeholder.ts @@ -39,15 +39,22 @@ export interface PurposePlaceholderIssue { } /** - * A `TBD` or `TODO` opening the Purpose. The lookahead keeps it off a longer - * word that merely begins with those letters, like "TBDs" or "TODOs", while - * still allowing the punctuation a marker is usually written with: `TODO:`, - * `TBD -`. It rejects any letter, digit or combining mark rather than only the - * ASCII ones `\b` knows about, because a Purpose is prose and prose is not - * always written in Latin script - `TBD` followed by an Arabic-Indic digit is - * as much a longer word as `TBDs` is. + * A `TBD` or `TODO` opening the Purpose. The first lookahead keeps it off a + * longer word that merely begins with those letters, like "TBDs" or "TODOs", + * while still allowing the punctuation a marker is usually written with: + * `TODO:`, `TBD -`. It rejects any letter, digit or combining mark rather + * than only the ASCII ones `\b` knows about, because a Purpose is prose and + * prose is not always written in Latin script - `TBD` followed by an + * Arabic-Indic digit is as much a longer word as `TBDs` is. + * + * The second lookahead requires the marker to be followed by the end of the + * Purpose, a line break, or marker punctuation. A marker is written `TODO:`, + * `TODO -`, or alone on its line - it is never `Todo` followed by prose, and + * in Spanish and Portuguese `todo` is an extremely frequent sentence opener + * ("Todo el…", "Todo o…"). Without this, ordinary prose in those languages is + * reported as an unwritten placeholder. */ -const LEADING_MARKER = /^(?:TBD|TODO)(?![\p{L}\p{N}\p{M}_])/iu; +const LEADING_MARKER = /^(?:TBD|TODO)(?![\p{L}\p{N}\p{M}_])(?=[ \t]*(?:$|\n|[:\-–—.,;()[\]{}]))/iu; const PURPOSE_HEADER = /^ {0,3}##(?!#)[ \t]+Purpose[ \t]*$/i; const TOP_LEVEL_HEADER = /^ {0,3}#{1,2}(?!#)[ \t]+/; diff --git a/test/core/purpose-placeholder.test.ts b/test/core/purpose-placeholder.test.ts index aee7b9cbc8..548432f56f 100644 --- a/test/core/purpose-placeholder.test.ts +++ b/test/core/purpose-placeholder.test.ts @@ -164,6 +164,23 @@ describe('findPurposePlaceholderIssue', () => { expect(findPurposePlaceholderIssue(' \n ', specWith(''))).toBeNull(); }); + it('does not report the Spanish word "Todo" opening authored prose', () => { + // `todo` is an extremely frequent sentence opener in Spanish ("Todo + // el…") and Portuguese ("Todo o…"). A marker is written `TODO:`, + // `TODO -`, or alone on its line - never `Todo` followed by prose. + for (const purpose of [ + 'Todo el conocimiento del producto vive del otro lado, en el repo hermano.', + 'Todo o catálogo é carregado a partir do repositório irmão.', + ]) { + expect(findPurposePlaceholderIssue(purpose, specWith(purpose))).toBeNull(); + } + }); + + it('still reports a marker alone on its line above placeholder prose', () => { + const purpose = 'TODO\nfill this in once the capability settles down.'; + expect(findPurposePlaceholderIssue(purpose, specWith(purpose))).not.toBeNull(); + }); + it('does not report an ordinary short Purpose, which PURPOSE_TOO_BRIEF covers', () => { expect(findPurposePlaceholderIssue('Does stuff.', specWith('Does stuff.'))).toBeNull(); }); From 6a45649126f23c42401bb039512e24a6af831442 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 22 Sep 2026 14:52:31 -0500 Subject: [PATCH 2/3] fix(validate): keep a shouted TODO a placeholder marker Requiring marker punctuation after a leading TBD/TODO fixed the Spanish and Portuguese false positive, but it also stopped reporting the plainest unwritten Purpose there is: `TODO write this once the capability settles down.` Case is what actually separates the marker from the word. In capitals it is the marker whatever follows it. In any other case it is a marker only when punctuation or the end of the line says so, which is how the lowercase forms an agent leaves behind are written (`todo - `, `tbd.`) and is not how a Spanish sentence opens. Covers `todo el ...` in lowercase too, which the capitals-only reading of the original fix would have reported. Co-Authored-By: Claude Opus 5 --- .changeset/purpose-marker-punctuation.md | 3 +- src/core/validation/purpose-placeholder.ts | 44 ++++++++++++++-------- test/core/purpose-placeholder.test.ts | 20 ++++++++++ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/.changeset/purpose-marker-punctuation.md b/.changeset/purpose-marker-punctuation.md index ce1ed9e7ed..9f99e5da07 100644 --- a/.changeset/purpose-marker-punctuation.md +++ b/.changeset/purpose-marker-punctuation.md @@ -4,4 +4,5 @@ Fix `validate --strict` reporting `PURPOSE_IS_PLACEHOLDER` for a Purpose that opens with the ordinary word "Todo" followed by prose, as in Spanish ("Todo el…") and Portuguese ("Todo o…") specs ([#1897](https://github.com/Fission-AI/OpenSpec/issues/1897)). -- A leading `TBD`/`TODO` now only counts as a placeholder marker when it is followed by the end of the Purpose, a line break, or marker punctuation (`TODO:`, `TODO -`, `TBD.`), never by prose. +- Case now separates the marker from the word. `TBD`/`TODO` in capitals is still a placeholder marker whatever follows it, so `TODO write this later` is still reported. +- In any other case it counts as a marker only when followed by the end of the Purpose, a line break, or marker punctuation (`todo - `, `tbd.`), so an authored Spanish or Portuguese sentence is not reported. diff --git a/src/core/validation/purpose-placeholder.ts b/src/core/validation/purpose-placeholder.ts index e74e5e1e1b..2f411a3dfc 100644 --- a/src/core/validation/purpose-placeholder.ts +++ b/src/core/validation/purpose-placeholder.ts @@ -39,22 +39,35 @@ export interface PurposePlaceholderIssue { } /** - * A `TBD` or `TODO` opening the Purpose. The first lookahead keeps it off a - * longer word that merely begins with those letters, like "TBDs" or "TODOs", - * while still allowing the punctuation a marker is usually written with: - * `TODO:`, `TBD -`. It rejects any letter, digit or combining mark rather - * than only the ASCII ones `\b` knows about, because a Purpose is prose and - * prose is not always written in Latin script - `TBD` followed by an - * Arabic-Indic digit is as much a longer word as `TBDs` is. + * A `TBD` or `TODO` opening the Purpose. * - * The second lookahead requires the marker to be followed by the end of the - * Purpose, a line break, or marker punctuation. A marker is written `TODO:`, - * `TODO -`, or alone on its line - it is never `Todo` followed by prose, and - * in Spanish and Portuguese `todo` is an extremely frequent sentence opener - * ("Todo el…", "Todo o…"). Without this, ordinary prose in those languages is - * reported as an unwritten placeholder. + * `WORD_END` keeps the marker off a longer word that merely begins with those + * letters, like "TBDs" or "TODOs", while still allowing the punctuation a + * marker is usually written with: `TODO:`, `TBD -`. It rejects any letter, + * digit or combining mark rather than only the ASCII ones `\b` knows about, + * because a Purpose is prose and prose is not always written in Latin script - + * `TBD` followed by an Arabic-Indic digit is as much a longer word as `TBDs`. + * + * Case is what separates the marker from the word. `TODO` shouted in capitals + * is the marker whatever follows it, so `TODO write this later` is still an + * unwritten Purpose. Written in any other case it is only a marker when + * punctuation or the end of the line says so, because `todo` is an extremely + * frequent sentence opener in Spanish ("Todo el...") and Portuguese ("Todo + * o..."), and ordinary prose in those languages is not a placeholder. That + * keeps the lowercase forms an agent really does leave behind - `todo - write + * this later`, `tbd.` - reported, without reading a Spanish sentence as one. */ -const LEADING_MARKER = /^(?:TBD|TODO)(?![\p{L}\p{N}\p{M}_])(?=[ \t]*(?:$|\n|[:\-–—.,;()[\]{}]))/iu; +const WORD_END = '(?![\\p{L}\\p{N}\\p{M}_])'; +const MARKER_PUNCTUATION = '(?=[ \\t]*(?:$|\\n|[:\\-\u2013\u2014.,;()\\[\\]{}]))'; + +/** `TBD`/`TODO` in capitals: the marker, whatever follows it. */ +const LEADING_MARKER_SHOUTED = new RegExp(`^(?:TBD|TODO)${WORD_END}`, 'u'); + +/** Any other case: a marker only when punctuation or the line end says so. */ +const LEADING_MARKER_PUNCTUATED = new RegExp( + `^(?:TBD|TODO)${WORD_END}${MARKER_PUNCTUATION}`, + 'iu' +); const PURPOSE_HEADER = /^ {0,3}##(?!#)[ \t]+Purpose[ \t]*$/i; const TOP_LEVEL_HEADER = /^ {0,3}#{1,2}(?!#)[ \t]+/; @@ -111,7 +124,8 @@ export function findPurposePlaceholderIssue( // that is nothing but a fenced block reduces to the same empty text here, and // is left to the brevity and empty-Purpose rules for the same reason. const prose = unfencedLines(overview).join('\n').trim(); - const leading = LEADING_MARKER.test(prose); + const leading = + LEADING_MARKER_SHOUTED.test(prose) || LEADING_MARKER_PUNCTUATED.test(prose); if (!leading && generatedPlaceholderPrefixIndex(prose) === undefined) return null; // Which rule matched decides where the placeholder is, so the locator is told. // When both match the leading marker wins: it sits at or above the generated diff --git a/test/core/purpose-placeholder.test.ts b/test/core/purpose-placeholder.test.ts index 548432f56f..cb78ce9e57 100644 --- a/test/core/purpose-placeholder.test.ts +++ b/test/core/purpose-placeholder.test.ts @@ -181,6 +181,26 @@ describe('findPurposePlaceholderIssue', () => { expect(findPurposePlaceholderIssue(purpose, specWith(purpose))).not.toBeNull(); }); + it('still reports a shouted TODO opening placeholder prose without punctuation', () => { + // Case is what separates the marker from the Spanish word. In capitals + // it is the marker whatever follows it, so requiring punctuation must + // not let the plainest unwritten Purpose of all through. + for (const purpose of [ + 'TODO write this once the capability settles down.', + 'TBD pending the design review that has not happened yet.', + ]) { + expect(findPurposePlaceholderIssue(purpose, specWith(purpose))).not.toBeNull(); + } + }); + + it('does not report lowercase Spanish prose either', () => { + // The reporter is the capitals, not the position: `todo` uncapitalised + // opens a sentence just as often, and is just as much authored prose. + const purpose = + 'todo el conocimiento del producto vive del otro lado, en el repo hermano.'; + expect(findPurposePlaceholderIssue(purpose, specWith(purpose))).toBeNull(); + }); + it('does not report an ordinary short Purpose, which PURPOSE_TOO_BRIEF covers', () => { expect(findPurposePlaceholderIssue('Does stuff.', specWith('Does stuff.'))).toBeNull(); }); From 05b81ec2c497b832dd4eaab0ef21f0933dffd015 Mon Sep 17 00:00:00 2001 From: Clay Good Date: Tue, 22 Sep 2026 16:10:26 -0500 Subject: [PATCH 3/3] docs(changeset): drop the trailing space inside a code span markdownlint MD038. Changeset text only; no behaviour change. Co-Authored-By: Claude Opus 5 --- .changeset/purpose-marker-punctuation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/purpose-marker-punctuation.md b/.changeset/purpose-marker-punctuation.md index 9f99e5da07..ae4102edd4 100644 --- a/.changeset/purpose-marker-punctuation.md +++ b/.changeset/purpose-marker-punctuation.md @@ -5,4 +5,4 @@ Fix `validate --strict` reporting `PURPOSE_IS_PLACEHOLDER` for a Purpose that opens with the ordinary word "Todo" followed by prose, as in Spanish ("Todo el…") and Portuguese ("Todo o…") specs ([#1897](https://github.com/Fission-AI/OpenSpec/issues/1897)). - Case now separates the marker from the word. `TBD`/`TODO` in capitals is still a placeholder marker whatever follows it, so `TODO write this later` is still reported. -- In any other case it counts as a marker only when followed by the end of the Purpose, a line break, or marker punctuation (`todo - `, `tbd.`), so an authored Spanish or Portuguese sentence is not reported. +- In any other case it counts as a marker only when followed by the end of the Purpose, a line break, or marker punctuation (`todo -`, `tbd.`), so an authored Spanish or Portuguese sentence is not reported.