fix(plan): a 1.9 plan refuses placeholders and mixed-case bare names as files

Operator decision 22 (13.10); review finding 33f37f53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-13 12:50:20 +02:00
commit 48b57284fe
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 50 additions and 6 deletions

View file

@ -499,17 +499,22 @@ export function fieldPaths(text) {
// opens with `File:` always starts a new entry; a `File:` mid-line names no
// test (review `4b6460e5`).
//
// isPathShape is the test fieldPaths puts on a span in the leading position of
// an entry (a path, a build file, or a capitalised name such as `LICENSE`), so
// a 1.9 entry's leading span is judged exactly as the reader below 1.9 judges
// one.
// isPathShape is the 1.9-only test on a span in the leading position of an
// entry (operator decision 22, 13.10): a placeholder (none, TBD, TODO, TBA,
// N/A, NA, `...`) is no file; a path, a build file or an all-capitals name
// such as `LICENSE` or `README` is; a mixed-case bare name (`Parser`) is not,
// and `./Name` names such a file. The readers below 1.9 keep BARE_NAME.
const LEADING_SPAN = /^`([^`\n]+)`/;
// Operator decision 22: these words are the input a 1.9 plan refuses, not unfinished code.
const PLACEHOLDER = /^(?:none|n\/?a|tbd|tba|todo|\.{3})$/i;
const CAPS_NAME = /^[A-Z][A-Z0-9_-]*$/;
/** A backticked span in the leading position of an entry that names a file. */
export function isPathShape(s) {
const span = String(s ?? '').trim();
return tickedPath(span) || BUILD_FILE.test(span) || BARE_NAME.test(span);
if (PLACEHOLDER.test(span)) return false;
return tickedPath(span) || BUILD_FILE.test(span) || CAPS_NAME.test(span);
}
/**

View file

@ -233,7 +233,8 @@ function pathGrammarIssue(n, code, shown, w) {
if (code === 'PLAN_STEP_PATH_NOT_A_PATH') {
return issue(code, `Step ${n}: ${w.subject} opens with a backticked span that is no path: ${shown}`,
`plan_version 1.9: the leading backticked spans of ${w.spans}; a call (\`parse()\`), an option ` +
'or a URL is description and goes after the path.');
'or a URL is description and goes after the path. A placeholder (`none`, `TBD`, `N/A`) names no ' +
'file, and neither does a mixed-case bare name (`Parser`): write `./Name` for such a file.');
}
return issue('PLAN_STEP_PATH_NOT_FIRST',
shown === '' ? `Step ${n}: ${w.none}` : `Step ${n}: ${w.subject} does not open with a backticked path: ${shown}`,

View file

@ -329,3 +329,41 @@ test('1.9: two unmarked File lines in a row give both tests', () => {
const text = planTestFirst19('1.9', ' File: `tests/mod1.test.mjs`\n File: `tests/mod1b.test.mjs`');
assert.deepEqual(schema.testFirstFiles(step1Fields(text), '1.9'), ['tests/mod1.test.mjs', 'tests/mod1b.test.mjs']);
});
// ---- Step 4 (trial 4): a 1.9 plan refuses a placeholder and a mixed-case bare
// name as a file (operator decision 22, 13.10; review 33f37f53) ----
test('1.9: isPathShape is false for a placeholder and a mixed-case bare name, true for a capitals name and ./Name', () => {
for (const s of ['None', 'none', 'TBD', 'TODO', 'TBA', 'N/A', 'n/a', 'NA', '...', 'Parser']) {
assert.equal(schema.isPathShape(s), false, s);
}
for (const s of ['LICENSE', 'README', 'Makefile', './Parser', 'TBC', 'NULL']) assert.equal(schema.isPathShape(s), true, s);
});
test('1.9: a Files entry that is a backticked None or TBD is PLAN_STEP_PATH_NOT_A_PATH under --strict, a warning under --soft', () => {
for (const files of ['`None`', '`TBD`']) {
const text = plan19(files);
const strict = validatePlanContent(text);
assert.equal(strict.valid, false, `${files}: ${errCodes(strict).join(',')}`);
const e = strict.errors.find((i) => i.code === 'PLAN_STEP_PATH_NOT_A_PATH');
assert.ok(e, `${files}: ${errCodes(strict).join(',')}`);
assert.match(e.message, /Step 1/);
assert.match(e.hint, /placeholder/i);
assert.match(e.hint, /TBD/);
assert.match(e.hint, /\.\/Name/);
const soft = validatePlanContent(text, { strict: false });
assert.ok(warnCodes(soft).includes('PLAN_STEP_PATH_NOT_A_PATH'), `${files}: ${warnCodes(soft).join(',')}`);
assert.deepEqual(pathIssues(soft.errors), []);
}
});
test('1.9: a Test first File line naming TBD gives no test, and K6 does not plan it', () => {
const text = planTestFirst19('1.9', ' - File: `TBD`');
assert.ok(text.includes(' - File: `TBD`\n'), 'the builder writes the raw File line');
assert.deepEqual(schema.testFirstFiles(step1Fields(text), '1.9'), []);
assert.ok(!plannedPaths(text).includes('TBD'), JSON.stringify(plannedPaths(text)));
});
test('below 1.9: fieldPaths of a backticked None still returns it (6.3.1\'s answer)', () => {
assert.deepEqual(schema.fieldPaths('`None`'), ['None']);
});