Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion scripts/check-outbound-send-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,13 @@ try {
}

const { roots, strict } = config;
const outboundFiles = new Set(["next-message.md", "send-package.md", "outreach.md", "reply-package.md", "call-booked-package.md", "close-package.md"]);
const outboundFiles = new Set(["next-message.md", "send-package.md", "recording-notes.md", "outreach.md", "reply-package.md", "call-booked-package.md", "close-package.md"]);
const optOutPattern = /\b(reply no|do not follow up|unsubscribe|opt out|ignore me)\b/i;
const placeholderPattern = /\[(?:add Loom link|link|specific fault|Name)\]|Here is the Loom:\s*$/i;
const salesPlaceholderPattern = /\badd (?:meeting link|payment link|call time)\b/i;
// Retired broad-agency offers must never project into an outbound send package.
// Mirrors the canonical retired-ask guard in export-recording-rehearsal-check.mjs.
const retiredOfferPattern = /7[-\s]day (?:site|website) revenue (?:leak|fault) (?:fix )?sprint|tangible revenue (?:leak|fault) sprint|30[-\s]day action plan|growth desk|three pages|\$\s?500\b/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict retired-offer matching to the actual offer ask

Because this expression treats standalone three pages and $500 as retired offers and is tested against the entire send package or recording-notes file, legitimate prospect evidence such as “pricing starts at $500” or “navigation spans three pages” makes send:check fail with “sells a retired offer.” The rehearsal guard this is intended to mirror scopes the test to the ask; this gate should likewise inspect the clean-ask/message section or require those generic terms to appear with retired-offer context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover all retired seven-day and 30-day variants

When recording notes contain variants such as 7-day sprint or 30-day plan, this pattern does not match and send:check can pass the retired offer. These are not hypothetical synonyms: test-sales-intake-contract.mjs explicitly classifies 7-day sprint and seven-day sprint as stale promises, while check-product-truth.mjs treats 30-day plan as stale by making action optional. Broaden the retired-ask detection while scoping it to the actual ask to avoid the previously reported whole-file false positives.

Useful? React with 👍 / 👎.


function walk(path) {
if (!existsSync(path)) return [];
Expand Down Expand Up @@ -124,6 +127,10 @@ for (const file of files) {
findings.push({ file, rule: "sales package still has meeting/payment placeholders" });
}

if (["send-package.md", "recording-notes.md"].includes(filename) && retiredOfferPattern.test(content)) {
findings.push({ file, rule: "outbound package sells a retired offer" });
}
Comment on lines +130 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce the retired-offer check in the send flow

When an operator follows the documented prospect:send-prep or market:after-recording flow, neither send preparation nor export-prospect-outbox.mjs invokes this checker; the outbox admits packages based only on Loom approval and the existing readiness marker. Consequently, recording notes containing a retired pitch can still produce a ready package and an actionable outbox entry unless the operator separately remembers to run send:check, so this new rule does not actually block the unsafe send path.

Useful? React with 👍 / 👎.


if (filename === "outreach.md" && !optOutPattern.test(content)) {
warnings.push({ file, rule: "template outreach lacks opt-out language" });
}
Expand Down
10 changes: 10 additions & 0 deletions scripts/test-active-offer-projection.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ If useful, I can run a 7-Day Site Revenue Fault Sprint with a 30-day action plan
}
dnm(loomPackage, retiredOfferPattern)

const sendResult = spawnSync(process.execPath, [sp("prepare-prospect-send.mjs"), legacyProspectPath, "https://www.loom.com/share/1234567890abcdef1234567890abcdef", "--approved", "--force"], {cwd: repositoryRoot, encoding: "utf8"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Isolate the send-prep test from the live prospect workspace

Running this test invokes prepare-prospect-send.mjs with the repository as its working directory and the default outbox path. The resulting export-prospect-outbox.mjs execution scans the real relative prospects/ directory, regenerates next-message.md for every eligible prospect through ensureMessage, and overwrites prospects/outbox.html; therefore npm test can modify operator-authored prospect data outside this temporary fixture. Run the send flow against a fully isolated service workspace/output instead.

Useful? React with 👍 / 👎.

eq(sendResult.status, 0, `send prep failed: ${sendResult.stderr}`)
const sendPackage = readFileSync(join(legacyProspectPath, "send-package.md"), "utf8")
mat(sendPackage, /The Website Correction/)
mat(sendPackage, /one highest-leverage page/)
for (const forbiddenOutcome of NO_GUARANTEE_OUTCOMES) {
mat(sendPackage, new RegExp(forbiddenOutcome.replace("-", "[- ]"), "i"))
}
dnm(sendPackage, retiredOfferPattern)

console.log("Active offer projection checks passed.")
} finally {
rmSync(testRoot, {recursive: true, force: true})
Expand Down
17 changes: 17 additions & 0 deletions scripts/test-outbound-send-readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ try {
assert.notEqual(result.status, 0)
assert(output(result).findings.some(finding => finding.rule === "send package still has placeholders"))

// The send package must never sell a retired broad-agency offer, and
// recording-notes.md is embedded verbatim into the send package.
for (const [file, content] of [
["send-package.md", `${packageHeader}7-Day Site Revenue Fault Sprint with a 30-day action plan. ${optOut}`],
["recording-notes.md", `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a Tangible Revenue Fault Sprint with a 30-day action plan for $500.\n`]
Comment on lines +54 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate the recording-notes case from the prior package

When the loop reaches recording-notes.md, the retired send-package.md written by the first iteration is still present, and the assertion checks only the shared rule name rather than the finding's file. Consequently, this test still passes if recording-notes.md scanning is removed or broken because the stale send package supplies the expected failure; reset the send package to safe content before the second case or assert that the finding points to recording-notes.md.

Useful? React with 👍 / 👎.

]) {
writeFileSync(join(prospect, file), content)
result = run()
assert.notEqual(result.status, 0, `expected retired-offer failure for ${file}`)
assert(output(result).findings.some(finding => finding.rule === "outbound package sells a retired offer"))
}

writeFileSync(join(prospect, "send-package.md"), `${packageHeader}The Website Correction, one highest-leverage page, 14-day implementation tracking. ${optOut}`)
writeFileSync(join(prospect, "recording-notes.md"), `# Recording Notes\n\n## Quality Notes\n\n- Clean ask: If useful, I can run a human-reviewed The Website Correction on this one highest-leverage page with a measurement plan.\n`)
result = run()
assert.equal(result.status, 0, "canonical send package must not trigger the retired-offer rule")

result = run(["--roots=alternate"])
assert.equal(result.status, 0)
assert.equal(output(result).filesScanned, 1)
Expand Down