fix(linkedin-studio): S12 — close S11 re-review + render-chain-propagation lint guard
Closes the 2 grep-verified findings from the S11 cold full-brief re-review
(docs/remediation/review.md, BLOCK 1/0/1/0, 0 dropped). Both were the NEXT
RING of the meta-class S10/S11 converged: a propagation miss — the fix had
landed where the SC named the file, not in the render-source it depends on.
BLOCKER (command->reference propagation): references/ab-testing-framework.md:166
still shipped the banned A/B "Significant? (>20%)" Yes/No verdict column while
commands/ab-test.md (which RENDERS from it, inlined at :30, presented at :69)
had been cleaned to the honest "Directional?" framing. Re-framed the reference
result template to match the command verbatim (header + the directional note)
and retuned :38 "20% significance threshold" -> "minimum-meaningful-difference
threshold". The whole render chain is now significance-verdict-free.
MINOR ($-replacement, class-closed not line-patched): the newest-first section
appends/rewrites in hooks/scripts/state-updater.mjs passed a replacement STRING
embedding untrusted user content to String.replace, so dollar-sequences
($1 / $& / dollar-backtick / dollar-apostrophe / $$) in a topic/hook/partner
(e.g. "$100 budget cut") re-injected the captured heading and dropped
characters, silently corrupting state. Converted all 5 content-bearing sites
(Recent Posts, prune, Milestone Log, First-Hour, Outreach) to replacement
FUNCTIONS; the 3 remaining $1 sites only interpolate date scalars. +4
$-bearing regression tests — incl. the prune fixture, which itself had to
switch to a function (the bug bit the fixture as it was being written).
META (generalize the lint to the propagation-miss class): new test-runner.sh
Section 11 — render-chain propagation guard. Forbids the significance-verdict
column (Significant? adjacent to "(" or a table pipe) across the WHOLE render
chain (commands + every inlined reference + adjacent surfaces), with a
permanent non-vacuity self-test (3 verdict forms caught, 6 legitimate
Significant/significance/Directional? forms ignored) and an e2e mutation-proof.
Generalizes S10/S11's "fix the class, not the line" to command->reference.
Pre-patch render-chain sweep confirmed ab-testing-framework.md was the SOLE
propagation survivor (so a 6th review finds no 3rd). test-runner.sh 70/0/0;
node --test 98/98. CLAUDE.md lint enumeration synced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
433c2efb3d
commit
36f79dd702
5 changed files with 183 additions and 10 deletions
|
|
@ -108,9 +108,14 @@ export function updatePostTracking(stateContent, { postDate, postTopic, hookText
|
|||
// 8. Append to Recent Posts section
|
||||
const hookPreview = hookText.length > 60 ? hookText.slice(0, 57) + '...' : hookText;
|
||||
const entry = `- [${postDate}] "${hookPreview}" (${charCount}) - ${postTopic}`;
|
||||
// Replacement FUNCTION, not string: `entry` embeds untrusted user content
|
||||
// (hookPreview, postTopic). In a replacement *string*, `$1`/`$&`/`` $` ``/`$'`/`$$`
|
||||
// are special, so a `$`-bearing topic (e.g. "$100 budget cut") would re-inject the
|
||||
// captured heading and drop characters, silently corrupting state. A function
|
||||
// inserts `entry` verbatim. (m === the whole captured heading.)
|
||||
content = content.replace(
|
||||
/^(## Recent Posts\n\n?)/m,
|
||||
`$1${entry}\n`
|
||||
(m) => `${m}${entry}\n`
|
||||
);
|
||||
changes.push(`Recent Posts += ${postDate} "${hookPreview.slice(0, 30)}..."`);
|
||||
|
||||
|
|
@ -154,7 +159,10 @@ export function pruneContentHistory(stateContent, maxAgeDays = 90) {
|
|||
if (pruned === 0) return null;
|
||||
|
||||
const newSection = kept.join('\n');
|
||||
const content = stateContent.replace(recentSection[1], newSection);
|
||||
// Replacement FUNCTION, not string: `newSection` is rebuilt from KEPT user
|
||||
// entries; with a string search, `$&`/`` $` ``/`$'`/`$$` in any kept post would be
|
||||
// interpreted and corrupt the rewrite. A function inserts it verbatim.
|
||||
const content = stateContent.replace(recentSection[1], () => newSection);
|
||||
return { content, pruned };
|
||||
}
|
||||
|
||||
|
|
@ -192,9 +200,12 @@ export function updateFollowerCount(stateContent, { count, month }) {
|
|||
|
||||
// Append to Milestone Log section
|
||||
const logEntry = `- [${month}] ${count} (${delta >= 0 ? '+' : ''}${delta})`;
|
||||
// Replacement FUNCTION, not string: same class as the other section appends.
|
||||
// `logEntry` is month + integers today (no `$`), but a function keeps the whole
|
||||
// append family uniform and `$`-safe by construction.
|
||||
content = content.replace(
|
||||
/^(## Milestone Log\n)/m,
|
||||
`$1${logEntry}\n`
|
||||
(m) => `${m}${logEntry}\n`
|
||||
);
|
||||
changes.push(`Milestone Log += ${month}`);
|
||||
|
||||
|
|
@ -251,7 +262,7 @@ export function recordFirstHourPlan(stateContent, { planDate, postTopic = '', ta
|
|||
|
||||
// 4. Append to ## First-Hour Plans (newest first); create the section if absent (additive)
|
||||
if (/^## First-Hour Plans\b/m.test(content)) {
|
||||
content = content.replace(/^(## First-Hour Plans\n\n?)/m, `$1${entry}\n`);
|
||||
content = content.replace(/^(## First-Hour Plans\n\n?)/m, (m) => `${m}${entry}\n`); // function, not string: entry embeds untrusted topic/targets/comments — `$`-safe
|
||||
} else {
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
content = `${trimmed}\n\n## First-Hour Plans\n\n<!-- First-hour / reply-loop plans. Format: ### [YYYY-MM-DD HH:MM] topic -->\n\n${entry}\n`;
|
||||
|
|
@ -311,7 +322,7 @@ export function recordOutreachContact(stateContent, { contactDate, track = '', p
|
|||
|
||||
// 4. Append to ## Outreach Pipeline (newest first); create the section if absent (additive)
|
||||
if (/^## Outreach Pipeline\b/m.test(content)) {
|
||||
content = content.replace(/^(## Outreach Pipeline\n\n?)/m, `$1${entry}\n`);
|
||||
content = content.replace(/^(## Outreach Pipeline\n\n?)/m, (m) => `${m}${entry}\n`); // function, not string: entry embeds untrusted partner/stage/nextAction — `$`-safe
|
||||
} else {
|
||||
const trimmed = content.replace(/\s*$/, '');
|
||||
content = `${trimmed}\n\n## Outreach Pipeline\n\n<!-- Outreach contacts / pipeline rows, newest first. Written by /linkedin:outreach. -->\n<!-- Format: ### [YYYY-MM-DD HH:MM] partner — track -->\n\n${entry}\n`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue