190190 * that spells a known head makes the cut land inside the statement, at that
191191 * head — and ONE property, not several, makes what follows unleakable rather
192192 * than exposed: only a template whose value runs to END OF MESSAGE may declare
193- * a `head` (the invariant on {@link ValueBearingTemplate .head}). Whatever the
193+ * a `head` (the invariant on {@link EndOfMessageTemplate .head}). Whatever the
194194 * head-anchored cut leaves — the rest of the statement it cut into included —
195195 * is consumed whole by that template's own `whole` pattern.
196196 *
224224 * values. That is the one way this amendment could leak, and the invariant
225225 * above is what forecloses it.
226226 *
227+ * [#9359] ⭐ And since the ablation recorded just above proves that a claim in
228+ * this very note about this very mechanism can read as true for weeks, the
229+ * invariant is no longer left to the note. It is held in two places that a
230+ * future author cannot write past: the TYPE ({@link AnchoredTemplate.head} is
231+ * `never`, so no row can carry a `head` and a `tail` at once) and
232+ * {@link assertHeadBearingTemplatesAreEndAnchored}, which throws at module load
233+ * on a head-bearing row whose `whole` is not end-anchored.
234+ *
227235 * A driver dump with no separator carries no statement to cut (`UNIQUE
228236 * constraint failed: sys_user.email`, `SQLITE_CONSTRAINT_NOTNULL: …`) and is
229237 * returned untouched — there is nothing there but the diagnostic already.
@@ -444,13 +452,38 @@ const MYSQL_TRUNCATED_INCORRECT_VALUE_HEAD = /truncated incorrect \w+ value:\s+'
444452 * drops everything before the anchor, because an anchor is evidence about the
445453 * value, not licence to assert which template printed it.
446454 */
447- interface ValueBearingTemplate {
455+ interface ValueBearingTemplateBase {
448456 /** Dialect and the server's own error code, as the probe raises it. */
449457 readonly id : string ;
450- /** Head + value + anchor. */
458+ /**
459+ * Head + value + anchor. Group 1 is kept before the value, group 2 after —
460+ * {@link redactDiagnosticValues} reads both by index, so every row needs
461+ * exactly two.
462+ */
451463 readonly whole : RegExp ;
452- /** The head-gone residue, when the template has a right anchor to recover it. */
464+ }
465+
466+ /**
467+ * A family whose diagnostic continues PAST the value, through a right anchor.
468+ *
469+ * ⛔ Such a family may NOT declare a `head`, and `head?: never` is what makes
470+ * that unrepresentable instead of merely forbidden: the head-anchored cut can
471+ * land inside the STATEMENT (a hostile value may spell a head), and a right
472+ * anchor would then keep whatever follows it — which on such a cut is
473+ * statement, which is caller values. It takes {@link tail} instead, which
474+ * recovers the same residue with no cut change at all.
475+ */
476+ interface AnchoredTemplate extends ValueBearingTemplateBase {
477+ /** The head-gone residue, recovered through the family's right anchor. */
453478 readonly tail ?: RegExp ;
479+ readonly head ?: never ;
480+ }
481+
482+ /**
483+ * A family whose value runs to END OF MESSAGE, so it has no right anchor and no
484+ * residue to recover — it takes a {@link head} instead.
485+ */
486+ interface EndOfMessageTemplate extends ValueBearingTemplateBase {
454487 /**
455488 * [#9275] The template's head through the value's OPENING QUOTE — what the
456489 * statement cut looks for so a value containing ` - ` cannot eat it.
@@ -464,21 +497,127 @@ interface ValueBearingTemplate {
464497 * template with a right anchor would instead keep whatever follows that
465498 * anchor, which on such a cut is statement, which is caller values.
466499 *
467- * Families that DO have a right anchor take {@link tail} instead; it recovers
468- * the same residue with no cut change at all. Both are asserted structurally
469- * by `driver-fault-redaction.test.ts` rather than trusted from this note.
500+ * [#9359] That invariant is no longer carried by this note. The `tail` half is
501+ * held by the type ({@link AnchoredTemplate.head} is `never`); the anchoring
502+ * half is held by
503+ * {@link assertHeadBearingTemplatesAreEndAnchored}, which runs at module load
504+ * over the table below. A doc comment is not a guard — a claim this very note
505+ * once made about this very mechanism (that last-match was a second line of
506+ * defence) read as true for weeks and fell only to an ablation.
470507 */
471- readonly head ?: RegExp ;
508+ readonly head : RegExp ;
509+ readonly tail ?: never ;
472510}
473511
474- const VALUE_BEARING_TEMPLATES : readonly ValueBearingTemplate [ ] = [
512+ type ValueBearingTemplate = AnchoredTemplate | EndOfMessageTemplate ;
513+
514+ export const VALUE_BEARING_TEMPLATES : readonly ValueBearingTemplate [ ] = [
475515 { id : 'mysql/1062 ER_DUP_ENTRY' , whole : DUPLICATE_ENTRY , tail : DUPLICATE_ENTRY_TAIL } ,
476516 { id : 'mysql/1366 ER_TRUNCATED_WRONG_VALUE_FOR_FIELD' , whole : MYSQL_INCORRECT_VALUE , tail : MYSQL_INCORRECT_VALUE_TAIL } ,
477517 { id : 'mysql/1292 ER_TRUNCATED_WRONG_VALUE' , whole : MYSQL_TRUNCATED_INCORRECT_VALUE , head : MYSQL_TRUNCATED_INCORRECT_VALUE_HEAD } ,
478518 { id : 'pg/22P02 invalid_text_representation' , whole : PG_INVALID_INPUT_SYNTAX , head : PG_INVALID_INPUT_SYNTAX_HEAD } ,
479519 { id : 'pg/22003 numeric_value_out_of_range' , whole : PG_VALUE_OUT_OF_RANGE , tail : PG_VALUE_OUT_OF_RANGE_TAIL } ,
480520] ;
481521
522+ /**
523+ * [#9359] What a head-bearing `whole` must END with: `$` — end of INPUT, since
524+ * JavaScript's `$` means end-of-line only under `m`, which is why the guard
525+ * rejects that flag — immediately followed by the EMPTY group 2 that
526+ * {@link redactDiagnosticValues} reads as "nothing is kept after the value".
527+ */
528+ const END_OF_MESSAGE_ANCHOR = '$()' ;
529+
530+ /**
531+ * [#9359] The head invariant, ENFORCED at module load rather than described.
532+ *
533+ * ## What it forecloses
534+ *
535+ * Only an end-of-message template may declare a `head`. That single property is
536+ * what bounds a hostile value's influence over the statement cut to
537+ * OVER-REDACTION: a value that spells a known head makes the cut land inside
538+ * the statement, and `whole` then swallows the remainder whole. Give a `head`
539+ * to a family with a RIGHT ANCHOR and the same cut keeps everything after that
540+ * anchor — statement, which is caller values. That is the one way #9275's
541+ * amendment can leak, and this function is what stops it being written.
542+ *
543+ * ## Why a load-time throw is the right shape here
544+ *
545+ * The precedent is `assertMetaUrlSpellingsAgree()` in `packages/spec`, and the
546+ * property that makes a boot-path throw safe is the same in both: this reads
547+ * NOTHING but literals declared in this file. It cannot depend on install
548+ * state, environment or import order, so it is deterministic per build — it
549+ * fires on the author's first import, never at a deployment that imported the
550+ * same bytes successfully yesterday. ⛔ If a future row is ever computed from
551+ * anything outside this module, that reasoning lapses and this belongs in a
552+ * gate script instead.
553+ *
554+ * ## The checks, and why each is the invariant rather than a proxy for it
555+ *
556+ * 1. **No `m` flag.** Under `m`, `$` is end-of-LINE, so an end-anchored
557+ * template would stop at the first newline of a multi-line dump and leave
558+ * the rest standing. Then `whole` is spelled end-anchored while not being
559+ * end-anchored — the exact silent shape this card exists to end.
560+ * 2. **`whole.source` ends with {@link END_OF_MESSAGE_ANCHOR}.** This is the
561+ * invariant's own wording ("`$`-anchored and group 2 empty") in one token.
562+ * 3. **Exactly two capture groups.** {@link redactDiagnosticValues} reads
563+ * `whole[1]` and `whole[2]` by index; a third group would silently reassign
564+ * what is kept after the value.
565+ *
566+ * Checks 2 and 3 are SYNTACTIC — they read the pattern, not its behaviour — and
567+ * so they reject spellings they cannot prove end-anchored even where a human
568+ * can see the pattern is fine. That is the safe direction: a novel spelling
569+ * gets a loud, named throw and its author amends this guard deliberately,
570+ * rather than the invariant quietly acquiring an exception.
571+ *
572+ * Exported for `driver-fault-redaction.test.ts`, which calls it with synthetic
573+ * rows to prove it FIRES — a guard nobody has watched fail is the same prose
574+ * this replaced.
575+ *
576+ * @param templates - the table to check; the shipped one is checked at load.
577+ * @throws when a row declares a `head` whose `whole` is not end-anchored.
578+ */
579+ export function assertHeadBearingTemplatesAreEndAnchored (
580+ templates : readonly ValueBearingTemplate [ ] ,
581+ ) : void {
582+ for ( const template of templates ) {
583+ if ( template . head === undefined ) continue ;
584+ const { source, flags } = template . whole ;
585+
586+ if ( flags . includes ( 'm' ) ) {
587+ throw new Error (
588+ "[driver-fault-redaction] Template '" + template . id + "' declares a head, but its 'whole' carries the "
589+ + "'m' flag, under which '$' matches end of LINE rather than end of MESSAGE. Only a template whose "
590+ + 'value runs to end of message may declare a head.' ,
591+ ) ;
592+ }
593+
594+ if ( ! source . endsWith ( END_OF_MESSAGE_ANCHOR ) ) {
595+ throw new Error (
596+ "[driver-fault-redaction] Template '" + template . id + "' declares a head, but its 'whole' does not end "
597+ + "with '" + END_OF_MESSAGE_ANCHOR + "', so its value does not run to end of message. Only an "
598+ + 'end-of-message template may declare a head: the head-anchored statement cut can land inside the '
599+ + "statement, and what bounds that to over-redaction is that 'whole' then swallows the remainder. A "
600+ + 'template with a right anchor keeps whatever follows that anchor, which on such a cut is statement, '
601+ + "which is caller values. Give this row a 'tail' instead." ,
602+ ) ;
603+ }
604+
605+ // `new RegExp(source + '|')` always matches the empty string through its
606+ // empty alternative, so the result is the group count without needing an
607+ // input the pattern accepts.
608+ const probe = new RegExp ( source + '|' ) . exec ( '' ) ;
609+ const groupCount = probe === null ? 0 : probe . length - 1 ;
610+ if ( groupCount !== 2 ) {
611+ throw new Error (
612+ "[driver-fault-redaction] Template '" + template . id + "' has " + groupCount + " capture group(s); "
613+ + "exactly 2 are required, because redactDiagnosticValues reads 'whole[1]' and 'whole[2]' by index." ,
614+ ) ;
615+ }
616+ }
617+ }
618+
619+ assertHeadBearingTemplatesAreEndAnchored ( VALUE_BEARING_TEMPLATES ) ;
620+
482621/**
483622 * [#9275] Every known diagnostic head, as the separator that stands before it.
484623 *
@@ -488,7 +627,7 @@ const VALUE_BEARING_TEMPLATES: readonly ValueBearingTemplate[] = [
488627 * hand.
489628 */
490629const HEAD_ANCHORED_CUTS : readonly RegExp [ ] = VALUE_BEARING_TEMPLATES
491- . filter ( ( template ) : template is ValueBearingTemplate & { head : RegExp } => template . head !== undefined )
630+ . filter ( ( template ) : template is EndOfMessageTemplate => template . head !== undefined )
492631 . map ( ( template ) => new RegExp ( `${ STATEMENT_SEPARATOR } (?=${ template . head . source } )` , 'gi' ) ) ;
493632
494633/**
0 commit comments