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
|
|
@ -216,6 +216,25 @@ describe('updatePostTracking', () => {
|
|||
assert.ok(result.content.includes('- [2026-04-05] "AI governance is not about..."'));
|
||||
});
|
||||
|
||||
test('appends a $-bearing topic/hook verbatim — special replacement patterns are not interpreted', () => {
|
||||
// Regression (S12): the append used a replacement *string*, so `$1`/`$&`/`$\``/
|
||||
// `$'`/`$$` in user content were interpreted — a "$100 budget" topic re-injected
|
||||
// the captured `## Recent Posts` heading and dropped characters. The replacement
|
||||
// function inserts the content verbatim.
|
||||
const result = updatePostTracking(SAMPLE_STATE, {
|
||||
postDate: '2026-04-09',
|
||||
postTopic: '$100 budget — $& and $1 rule',
|
||||
hookText: 'We cut $1 of $5',
|
||||
charCount: 1200,
|
||||
format: 'post'
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('$100 budget — $& and $1 rule'), 'topic with $-tokens must be inserted verbatim');
|
||||
assert.ok(result.content.includes('"We cut $1 of $5"'), 'hook with $-tokens must be inserted verbatim');
|
||||
const headings = result.content.match(/^## Recent Posts$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'heading must not be re-injected by a $1/$& expansion');
|
||||
});
|
||||
|
||||
test('updates longest_streak when current exceeds it', () => {
|
||||
const highStreak = SAMPLE_STATE.replace('current_streak: 5', 'current_streak: 12');
|
||||
const result = updatePostTracking(highStreak, {
|
||||
|
|
@ -319,6 +338,34 @@ describe('pruneContentHistory', () => {
|
|||
assert.notEqual(result, null);
|
||||
assert.equal(result.pruned, 1);
|
||||
});
|
||||
|
||||
test('preserves a $-bearing kept entry verbatim while pruning — special replacement patterns are not interpreted', () => {
|
||||
// Regression (S12): the rewrite used `.replace(section, newSectionString)`; a
|
||||
// string search has no $1 group, but `$&` still expands to the whole matched
|
||||
// section and `$$` collapses to `$`, so a kept post like "$$ and $& budget"
|
||||
// corrupted state. The replacement function inserts newSection verbatim.
|
||||
const today = new Date();
|
||||
const old = new Date(today); old.setDate(old.getDate() - 100);
|
||||
const oldDate = old.toISOString().slice(0, 10);
|
||||
const recent = new Date(today); recent.setDate(recent.getDate() - 10);
|
||||
const recentDate = recent.toISOString().slice(0, 10);
|
||||
|
||||
// Build the fixture with a replacement FUNCTION too — a string replacement here
|
||||
// would itself interpret the `$$`/`$&` we are trying to plant (the very bug under
|
||||
// test), corrupting the fixture before pruneContentHistory ever sees it.
|
||||
const stateWithMix = SAMPLE_STATE.replace(
|
||||
'## Recent Posts\n\n',
|
||||
() => `## Recent Posts\n\n- [${oldDate}] "Old..." (1000) - drop me\n- [${recentDate}] "Saved $&100" (1200) - $$ and $& budget\n`
|
||||
);
|
||||
|
||||
const result = pruneContentHistory(stateWithMix, 90);
|
||||
assert.notEqual(result, null);
|
||||
assert.equal(result.pruned, 1);
|
||||
assert.ok(!result.content.includes(oldDate), 'old entry pruned');
|
||||
assert.ok(result.content.includes(`- [${recentDate}] "Saved $&100" (1200) - $$ and $& budget`), 'kept $-bearing entry survives verbatim');
|
||||
const headings = result.content.match(/^## Recent Posts$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be duplicated by a $& expansion');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateFollowerCount', () => {
|
||||
|
|
@ -446,6 +493,24 @@ describe('recordFirstHourPlan', () => {
|
|||
assert.ok(section.includes('AI governance'));
|
||||
});
|
||||
|
||||
test('records $-bearing topic/targets/comments verbatim — special replacement patterns are not interpreted', () => {
|
||||
// Regression (S12): the section append used a replacement *string*; `$&`/`$1`/`$$`
|
||||
// in the topic/targets/comments were interpreted. The function inserts verbatim.
|
||||
const result = recordFirstHourPlan(TEMPLATE_STATE, {
|
||||
planDate: '2026-05-30 09:00',
|
||||
postTopic: '$100 launch & $& spend',
|
||||
targets: ['Whale: @big$voice ($1 ask)'],
|
||||
draftComments: ['Loved the $$ point and the $& follow-up'],
|
||||
plan: ['09:00 — live']
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('$100 launch & $& spend'), 'topic with $-tokens verbatim');
|
||||
assert.ok(result.content.includes('Whale: @big$voice ($1 ask)'), 'target with $-tokens verbatim');
|
||||
assert.ok(result.content.includes('Loved the $$ point and the $& follow-up'), 'comment with $-tokens verbatim');
|
||||
const headings = result.content.match(/^## First-Hour Plans$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be re-injected by a $1/$& expansion');
|
||||
});
|
||||
|
||||
test('no-anchor fall-through: neither last_firsthour_date nor last_post_date — scalar not written, not reported; section still appended', () => {
|
||||
// Exercises the else-fall-through of the date-scalar gate
|
||||
// (state-updater.mjs:225-231): with NO anchor field, the scalar cannot be
|
||||
|
|
@ -557,6 +622,25 @@ describe('recordOutreachContact', () => {
|
|||
assert.ok(section.includes('@bigvoice'));
|
||||
});
|
||||
|
||||
test('records $-bearing partner/stage/nextAction verbatim — special replacement patterns are not interpreted', () => {
|
||||
// Regression (S12): same class as the other section appends — replacement
|
||||
// function, not string, so `$&`/`$1`/`$$` in user content are inserted verbatim.
|
||||
const result = recordOutreachContact(TEMPLATE_STATE, {
|
||||
contactDate: '2026-05-30 14:00',
|
||||
track: 'collab',
|
||||
partner: '@big$voice & $&co',
|
||||
stage: 'pitched $100 deal',
|
||||
nextAction: 'send $$ quote, ref $1',
|
||||
dueDate: '2026-06-06'
|
||||
});
|
||||
assert.notEqual(result, null);
|
||||
assert.ok(result.content.includes('@big$voice & $&co'), 'partner with $-tokens verbatim');
|
||||
assert.ok(result.content.includes('pitched $100 deal'), 'stage with $-tokens verbatim');
|
||||
assert.ok(result.content.includes('send $$ quote, ref $1'), 'nextAction with $-tokens verbatim');
|
||||
const headings = result.content.match(/^## Outreach Pipeline$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be re-injected by a $1/$& expansion');
|
||||
});
|
||||
|
||||
test('no-anchor fall-through: none of last_outreach_date/last_firsthour_date/last_post_date — scalar not written, not reported; section still appended', () => {
|
||||
// Exercises the else-fall-through of the date-scalar gate
|
||||
// (state-updater.mjs:284-293): with NONE of the three anchors, the scalar
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue