enhance(#3717): make the edge probe's shape cues language-aware via an optional text_en field (#4156)

* test(#3717): add failing-first coverage for text_en language-aware classification

Adds unit tests for the not-yet-implemented text_en field on Requirement
(fallback selection, empty/whitespace/non-string rejection, shapes-override
precedence), a SHAPE_CUES/VALID_SHAPES parity guard (RULESET.GENERATIVE-FIX),
and workflow-prose contract tests asserting spec-phase.md Step 5.5 documents
populating text_en for response_language projects. All new tests are RED
until src/edge-probe.cts and the workflow docs are updated.

* feat(#3717): make edge-probe shape classification read an optional text_en field

Requirement gains an optional text_en; classifyShape's own signature stays
untouched (a locked, directly-tested export), and the text_en ?? text
selection is pushed to proposeEdges' single call site instead. text_en is
validated fail-closed: an empty or whitespace-only value throws rather than
silently winning the ?? fallback and degrading classification to zero shapes.

This makes the #2773 doc-only translation convention an explicit,
validatable field instead of an invisible instruction, per the approved
Form-1 scope on #3717.

* docs(#3717): document the text_en field across spec-phase, reference and how-to docs

Updates Step 5.5's response_language instructions, the edge-probe reference
Inputs contract, the FEATURES.md fragment, and the non-English how-to guide
to describe the new text_en field: text keeps the requirement's own wording
in all cases, text_en (when populated) is the engine-only English rendering
the classifier prefers.

* docs(#3717): record the text_en locked-surface change in CONTEXT.md and ADR-550

Updates the Edge Probe Module glossary entry to describe the text_en field
and its fail-closed validation, and appends an ADR-550 amendment recording
why this is additive and does not re-open the #652 LLM-classifier rejection
(text_en is a plain field read by the existing deterministic regex
classifier, not a new model-dependent surface).

* docs(#3717): add changeset fragment and regenerate FEATURES.md

pr:0 placeholder — backfilled with the real PR number after the PR opens.

* docs(#3717): attribute the text_en machine check to engine-level validation, not prose tests

Code-review (Spec axis) finding: the workflow-prose contract tests and the
ADR-550 amendment overclaimed themselves as "the machine check the #2773
doc-only stopgap lacked." That check is actually engine-level
(validateRequirement/classifyShape, covered in tests/edge-probe.test.cjs) —
the prose tests are the same style of assertion #2773 already used. Reworded
both to attribute the claim correctly.

* fix(#3717): rewrap spec-phase.md so the id-unchanged sentence stays on one line

The #3717 rewrite of Step 5.5's response_language paragraph moved a line
break so "requirement `id`s" ended one physical line and "are never
translated" started the next. The pre-existing #2773 regression test
(tests/edge-probe-spec-phase-contract.test.cjs) asserts id + "never
translated" on the SAME line (no \n in between, matching git's own
line-oriented prose), so the reflow silently broke it. Rewrapped so the
sentence lands on one line again, verified against every #2773/#3717
regex assertion in that test file.

Emitted-Drift-Ack-Growth: spec-phase.md — #3717 adds text_en documentation to Step 5.5 (response_language paragraph + REQS_JSON heredoc comment); this growth is this PR's own diff, not incidental drift.

* chore(#3717): backfill changeset PR number

pr:0 -> pr:4156 now that the PR exists.

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-01 21:39:53 -04:00
committed by GitHub
parent 4dfc46bbe7
commit bf4485ada2
11 changed files with 286 additions and 44 deletions

View File

@@ -46,10 +46,21 @@ export interface TaxonomyEntry {
probe: string;
}
/** A SPEC requirement; `shapes` is an optional authored override of classification. */
/**
* A SPEC requirement; `shapes` is an optional authored override of classification.
*
* `text_en` (#3717) is an optional English translation of `text`, read by shape
* classification in preference to `text` when present (`text_en ?? text`). `SHAPE_CUES`
* are English-only word-boundary patterns, so a non-English `text` (e.g. a project running
* with `response_language` set) classifies to zero shapes unless `text_en` supplies an
* English rendering. `text` itself is unaffected and keeps its own meaning (the
* requirement's own text, in whatever language the SPEC uses) — only classification reads
* `text_en` preferentially.
*/
export interface Requirement {
id: string;
text: string;
text_en?: string;
shapes?: Shape[];
}
@@ -135,7 +146,7 @@ export const EDGE_VALIDATORS: Validators = {
*/
export function validateRequirement(requirement: Requirement): void {
coreValidateRequirement(requirement);
const r = requirement as unknown as { shapes?: unknown; text?: unknown };
const r = requirement as unknown as { shapes?: unknown; text?: unknown; text_en?: unknown };
if (r.shapes != null && !Array.isArray(r.shapes)) {
throw new Error(`requirement ${requirement.id} shapes must be an array when present`);
}
@@ -144,6 +155,15 @@ export function validateRequirement(requirement: Requirement): void {
`requirement ${requirement.id} text must be a non-empty string when no shapes override is provided`,
);
}
// text_en (#3717) is optional, but when present it must be a non-empty string. An empty
// string is NOT caught by `??` (only null/undefined are), so an unvalidated `text_en: ''`
// would silently win `text_en ?? text` and classify against '' — the same fail-open shape
// #1110/#2773 already exist to eliminate, just moved one field over. Validated
// unconditionally (not gated on whether `shapes` will make it unused) so bad data fails
// closed even when it happens to be dead for this particular call.
if (r.text_en != null && !(typeof r.text_en === 'string' && r.text_en.trim())) {
throw new Error(`requirement ${requirement.id} text_en must be a non-empty string when present`);
}
}
/** Validate an edge resolution against the edge verification vocabulary. */
@@ -172,7 +192,11 @@ export function proposeEdges(requirement: Requirement): Edge[] {
}
shapes = requirement.shapes;
} else {
shapes = classifyShape(requirement.text);
// #3717: prefer the English translation when present — SHAPE_CUES are English-only
// word-boundary patterns, so a non-English `text` (e.g. response_language projects)
// would otherwise classify to zero shapes. validateRequirement (called above) has
// already guaranteed text_en, if present, is a non-empty string.
shapes = classifyShape(requirement.text_en ?? requirement.text);
if (shapes.length === 0) {
// Prose present but no shape cue matched. Do NOT silently drop it (#1110): an
// edge-relevant requirement whose phrasing missed every cue would otherwise vanish from