Archive-reason content rule: ban call expressions inside a reason interpolation (shape validator cannot reach it)
The content half of PLUTO-767, split out because the grammar validator can never enforce it. Measured at 66e29b7: the candidate grammar passes 65 of 65 reason literals INCLUDING the three sites that still interpolate truncated operator free text, because that text sits inside a template interpolation and the lint sees a placeholder. Shape and content are two rules; the lint closes only the first, and a green corpus must never be cited as evidence the no-free-text ruling holds. THE PREDICATE IS BUILDABLE AND THE SEPARATION IS MEASURED, not guessed. Census of the live corpus: 65 literals, 39 fully static, 26 interpolated. Of the 26, TWENTY-THREE interpolate only identifiers, member paths or a ternary selecting between two string literals - requestId, matchedUserId, userId, rowSubtype, batchId, rowNum, activo, key, membershipTable, beforeRow.comisionId, isPrincipal ? promotePrincipal : demotePrincipal, and so on. All THREE violations are CALL EXPRESSIONS, specifically a .slice() of user text: admin-actions.ts:1009 trimmed.slice(0,200), admin-actions.ts:2484 trimmedReason.slice(0,180), support-admin-access-core.ts:151 trimmed.slice(0,170). So the rule is: a reason interpolation may contain an identifier, a member path, or a ternary over string literals - never a call expression. That is 23/3 clean on the live tree with no exceptions to carve out. Build it with the PLUTO-768 lesson applied: the check must be able to REACH the condition it claims to test, so lex before matching (comment-blanked copy, already in the hook) and force it RED against a .slice() interpolation before trusting it - a placeholder hiding the datum is exactly the condition being hidden from the shape check. ORDERING: this lands only after the three DDL legs are cleared (PLUTO-769 plus the accessRequests rejection-motivo column), because until then the live corpus FAILS this predicate by design and a validator the corpus fails must never be merged. Also required of the shape validator when it lands: its header and its failure message must state that it is SHAPE-ONLY, so the next author reading a green run does not conclude the content rule holds.
Activity
-
GO from pmmaster, with a coverage gap that must appear in the check header before it ships. THE PREDICATE IS A SYNTACTIC APPROXIMATION OF A PROVENANCE QUESTION. The real question is whether the interpolated value derives from user-supplied text; the check can only see expression shape. beforeRow.comisionId is a member path and is legitimate; beforeRow.motivo is also a member path and is precisely what the rule exists to ban. The three current violations all happen to be call expressions, which is why the census reads 23/3 clean today - but a member path resolving to free text PASSES, and the next such site will ship. Required header wording, same honesty the shape validator carries: this check enforces EXPRESSION SHAPE, not PROVENANCE; a member path that resolves to user-supplied text passes it. Without that line, two green checks read as "content is enforced" to anyone arriving later. FALSE-POSITIVE DIRECTION is the acceptable one and goes in the failure message: a legitimate coded call such as reasonFor(x) returning a server-side code will be REJECTED, and the remedy is to assign it to a const first. Failing closed toward "name it before you log it" is the intended bias, and saying so in the message saves the next author the diagnosis. Build order unchanged: after PLUTO-769 and the accessRequests rejection-motivo column clear, because the live corpus fails this predicate by design until then.
-
BLOCKER 1 OF 2 CLEARED, and a misattribution to correct. The accessRequests rejection-motivo leg landed as 451efb9 / v2.23.139 and is tracked on PLUTO-778, not here. The commit SUBJECT of 451efb9 reads 'PLUTO-771' - that is wrong and cannot be amended on a pushed commit: 771 is the lint content rule, and its own spec event names the rejection-motivo column as one of its BLOCKERS, not its deliverable. Read PLUTO-778 for that work; do not read 451efb9's subject as closing this row. Remaining blocker: PLUTO-769 (reactivateInactive motivo, db-pluto-cc, needs a new column). Census impact, not yet re-measured: the three call-expression violations the 23/3 census counted included the two reject sites, which no longer interpolate anything - the reason literals there are now fully static strings. So the live corpus is closer to passing the content predicate than the 2026-10-06 census says, but RE-MEASURE at the SHA you build against rather than subtracting two from a stale count. The required header wording from the GO stands unchanged: the check enforces EXPRESSION SHAPE, not PROVENANCE, and a member path resolving to user-supplied text passes it.