Circle Suite: complete the ladder — every paid level now unlocks live software
Four new tools, so no tier is a placeholder any more: L4 Voice Profile (/suite/voice) — five short answers that ride along with every Copy Engine and Email Engine generation. It sits BEFORE the compliance block in the prompt on purpose: a personal voice must never be able to talk the model out of the honesty rules. suite-email builds its own prompt, so the profile is threaded in there separately or email would keep sounding generic while the Copy Engine sounded like the member. L5 Split Tester (/suite/split) — 2-3 variants launched as one test with the impressions split evenly, then click counters compared. Deliberately conservative: it will not declare a winner until both arms have real volume AND the leader is clearly ahead, because a 3-click gap on 400 impressions is noise. A genuine tie is reported as a tie, which is useful information too. A failed arm rolls back the whole test rather than leaving half of it running. Apex is the right home for it — that is where impressions jump to 50,000. L6 Funnel Factory (/suite/funnel) — extra named pages at /p/<id>/<slug>, so a leader can run one page per audience and point different ads at each. Missing slugs fall back to the member's main page, same no-dead-ends rule as /p/<id>. L7 Leader Ops (/suite/leader) — the coaching radar already existed in chain.js and only admin could see it, which was backwards: the leaders running those legs need it more than anyone. Now scoped to the member's own organisation, with a written plan built on contract-read facts only. Follows the teach-forward rule — the digest gives the leader words to hand down, not just numbers to act on. Tile copy across the wall and the payout alerts now describes what actually exists rather than what was sketched. suite-tools.js was stale in both directions (it still had the Traffic Desk at L5 and L3 not live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// Circle Suite — Voice Profile (level 4).
|
||||
//
|
||||
// The Copy Engine writes in the TEAM voice. That is the right default: it is
|
||||
// compliant, it is consistent, and a brand-new member has no voice of their own
|
||||
// to write in yet. But a member who has been at this a while sounds like
|
||||
// themselves, and copy that sounds like the team instead of like them is the
|
||||
// first thing their own audience notices.
|
||||
//
|
||||
// So this is not a second engine. It is a small profile that rides along with
|
||||
// every Copy Engine and Email Engine generation and biases the output toward
|
||||
// how this particular person actually talks.
|
||||
//
|
||||
// Deliberately kept short. Five specific answers beat a long questionnaire: the
|
||||
// profile becomes prompt text, and prompt text that rambles just dilutes the
|
||||
// instructions that matter (the compliance block especially).
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let DATA_DIR = null;
|
||||
function init(opts) {
|
||||
DATA_DIR = opts.dataDir;
|
||||
try { fs.mkdirSync(dir(), { recursive: true }); } catch (e) {}
|
||||
}
|
||||
function dir() { return path.join(DATA_DIR, 'voice'); }
|
||||
function file(id) { return path.join(dir(), String(Number(id)) + '.json'); }
|
||||
|
||||
// field -> {label, hint, max}. Order is the order they appear in the form.
|
||||
const FIELDS = [
|
||||
{ key: 'background', label: 'What did you do before this?',
|
||||
hint: 'A trade, a job, a business, retired, still working — one line is plenty.', max: 220 },
|
||||
{ key: 'audience', label: 'Who are you usually talking to?',
|
||||
hint: 'Friends and family? Other marketers? People in your church, your gym, your old industry?', max: 220 },
|
||||
{ key: 'tone', label: 'How would a friend describe the way you talk?',
|
||||
hint: 'Blunt. Funny. Careful. Encouraging. Skeptical. Pick a few real words.', max: 160 },
|
||||
{ key: 'never', label: 'What would you never say?',
|
||||
hint: 'Words or promises that would make you cringe coming out of your own mouth.', max: 220 },
|
||||
{ key: 'sample', label: 'Paste something you actually wrote',
|
||||
hint: 'A post, a text, an email — anything a few sentences long, in your own words. This teaches it more than the rest combined.', max: 900 }
|
||||
];
|
||||
|
||||
function blank() {
|
||||
const o = {};
|
||||
FIELDS.forEach(function (f) { o[f.key] = ''; });
|
||||
return o;
|
||||
}
|
||||
|
||||
function load(id) {
|
||||
try { return JSON.parse(fs.readFileSync(file(id), 'utf8')); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function save(id, body) {
|
||||
const rec = blank();
|
||||
FIELDS.forEach(function (f) {
|
||||
rec[f.key] = String((body && body[f.key]) || '').replace(/\s+/g, ' ').trim().slice(0, f.max);
|
||||
});
|
||||
rec.id = Number(id);
|
||||
rec.updatedAt = new Date().toISOString();
|
||||
rec.complete = complete(rec);
|
||||
try { fs.writeFileSync(file(id), JSON.stringify(rec)); } catch (e) {}
|
||||
return rec;
|
||||
}
|
||||
|
||||
function clear(id) {
|
||||
try { fs.unlinkSync(file(id)); } catch (e) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
// "Complete enough to be worth using" — not every field, because the writing
|
||||
// sample alone carries most of the signal.
|
||||
function complete(rec) {
|
||||
if (!rec) return false;
|
||||
const filled = FIELDS.filter(function (f) { return String(rec[f.key] || '').trim().length > 2; }).length;
|
||||
return filled >= 2 && String(rec.sample || rec.tone || '').trim().length > 10;
|
||||
}
|
||||
|
||||
// Render the profile as prompt text. Returns '' when there is nothing useful,
|
||||
// so callers can concatenate unconditionally.
|
||||
function promptBlock(id) {
|
||||
const rec = load(id);
|
||||
if (!complete(rec)) return '';
|
||||
const bits = [];
|
||||
if (rec.background) bits.push('Their background: ' + rec.background);
|
||||
if (rec.audience) bits.push('Who they are writing to: ' + rec.audience);
|
||||
if (rec.tone) bits.push('How they come across: ' + rec.tone);
|
||||
if (rec.never) bits.push('Things they would never say — avoid these completely: ' + rec.never);
|
||||
if (rec.sample) bits.push('A sample of their own writing, for rhythm and word choice (do NOT copy its content, only its voice):\n"""\n' + rec.sample + '\n"""');
|
||||
if (!bits.length) return '';
|
||||
return 'WRITE AS THIS PERSON. Match their rhythm, vocabulary and level of formality. ' +
|
||||
'This overrides the generic team voice above, EXCEPT for the compliance rules, which are absolute and can never be relaxed to sound more like them.\n' +
|
||||
bits.join('\n');
|
||||
}
|
||||
|
||||
module.exports = { init, FIELDS, blank, load, save, clear, complete, promptBlock };
|
||||
Reference in New Issue
Block a user