* fix(#3840): reject a malformed feature `order` instead of coercing it `scripts/gen-features.cjs` was the one field validated by coercion rather than by shape. `Number('')` is 0, and `0x10`, `0b11`, `0o17`, `1e3`, `1.` and `.5` all coerce to finite numbers, so a fragment declaring a bare `order:` sorted to position 0 -- ahead of every real feature, in both the body and the generated table of contents -- with zero violations, a clean `--check` and `--write` exiting 0. That is a fail-open in a gate whose entire contract is a typed violation rather than a silent guess. `order` is now shape-checked against an optionally-signed decimal literal before coercion, mirroring how ID_RE guards `id`. The finite check stays: the regex alone would admit a literal long enough to overflow to Infinity. Surfaced by re-running the feature-implementation directive's design and QA steps against the code merged in #3845, which shipped without them. Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#3840): backfill changeset PR number Refs #3840 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,20 @@ const GROUP_NOTE_FIELDS = Object.freeze(['group']);
|
||||
*/
|
||||
const ID_RE = /^(?:0|[1-9][0-9]*)(?:\.[0-9]+|[a-z])?$/;
|
||||
|
||||
/**
|
||||
* A legal explicit `order`: an optionally-signed decimal literal.
|
||||
*
|
||||
* `order` was the only field validated by COERCION rather than by shape, and
|
||||
* `Number()` is far more liberal than a docs ordering field has reason to be:
|
||||
* `Number('')` and `Number(' ')` are 0, and `0x10`, `0b11`, `0o17`, `1e3`, `1.`
|
||||
* and `.5` all coerce to finite numbers. A fragment declaring `order:` with
|
||||
* nothing after it therefore sorted to position 0 — ahead of every real
|
||||
* feature — with no violation and exit 0, in a gate whose whole contract is a
|
||||
* typed violation rather than a silent guess. Shape first, then coerce,
|
||||
* mirroring how ID_RE guards `id`.
|
||||
*/
|
||||
const ORDER_RE = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/;
|
||||
|
||||
/** The fragment filename shape: a kebab slug, mirroring `docs/adr/`'s rule. */
|
||||
const FRAGMENT_FILENAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.md$/;
|
||||
|
||||
@@ -407,7 +421,10 @@ function readCorpus() {
|
||||
let order = defaultOrder(data.id);
|
||||
if (data.order !== undefined) {
|
||||
order = Number(data.order);
|
||||
if (!Number.isFinite(order)) {
|
||||
// Both guards are load-bearing: the regex rejects the shapes `Number`
|
||||
// would silently accept, and `isFinite` still catches a well-shaped
|
||||
// literal long enough to overflow to Infinity.
|
||||
if (!ORDER_RE.test(data.order) || !Number.isFinite(order)) {
|
||||
add(REASON.ORDER_INVALID, rel, { order: data.order });
|
||||
continue;
|
||||
}
|
||||
@@ -620,7 +637,7 @@ function describeViolation(v) {
|
||||
case REASON.ID_DUPLICATE:
|
||||
return `${v.file}: id '${v.id}' is already used by ${v.first} — pick another (any unique id is legal)`;
|
||||
case REASON.ORDER_INVALID:
|
||||
return `${v.file}: order '${v.order}' is not a finite number`;
|
||||
return `${v.file}: order '${v.order}' is not a decimal number (an optionally-signed integer or decimal)`;
|
||||
case REASON.ANCHOR_DUPLICATE:
|
||||
return `${v.file}: anchor '#${v.anchor}' collides with ${v.first}`;
|
||||
case REASON.BODY_EMPTY:
|
||||
|
||||
Reference in New Issue
Block a user