6a7c5161ff
Marty's read was right — the upper tiers felt flat, and the reason is that they scaled QUANTITY, not KIND. L1->L2 was a category change (you couldn't write, now you can). Everything above L4 was a multiplier on a capability already owned at L2: sound more like you, measure it, more pages, five at once. Meanwhile the price doubles each rung — Corona costs ~17x Culmen. No batch button is worth 17x a voice profile. The fix is a change of axis. Every tool from L1 to L8 was a "me" tool, but past the first levels this business isn't about you: get two, help your two, teach them to teach. So the top now operates on the ORGANISATION. L7 Team Grants — hand Suite capacity down to anyone in your own org. Deliberately from a separate pool that the level grants, NOT out of the leader's own allowance: making someone choose between equipping their team and using their own tools means nobody ever grants anything. Recipients are verified in-org against the contract. Grants can lift a tool the recipient's level hasn't unlocked — that's the point, not a loophole. Being helped is made visible to the recipient, since half the value is knowing an upline backed you. L8 Network Intelligence — every team ad pooled and anonymised: which angles, headlines, creative and formats actually earn their impressions. No member alone has enough traffic to learn anything from display; together they do. It can't be bought, and it compounds monthly with no further work. Holds two lines: nothing identifies anyone, and nothing under 2,000 impressions gets a reported rate — under-evidenced rows are counted and disclosed rather than silently dropped. The written readout is hand-authored, not generated: these are conclusions about money, and a model paraphrasing a table is exactly where an invented number slips in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
144 lines
5.9 KiB
JavaScript
144 lines
5.9 KiB
JavaScript
// Circle Suite — Team Grants (Vertex L7, Corona L8).
|
|
//
|
|
// The problem this fixes: every tool from L1 to L8 was a "me" tool — help ME
|
|
// write, help ME post, help ME measure. But past the first couple of levels the
|
|
// business is not about you. It is get two, help your two, teach them to teach.
|
|
// So the top of the ladder was scaling the wrong axis: more of your own
|
|
// capability, at double the price each rung.
|
|
//
|
|
// A grant lets a leader hand Suite capacity DOWN into their own organisation —
|
|
// impressions, generations, page builds, ad batches. Three things make it work:
|
|
//
|
|
// 1. It comes from a POOL that is a perk of the level, not a tax on the
|
|
// leader's own allowance. Making leaders choose between equipping their
|
|
// team and using their own tools would just mean nobody grants anything.
|
|
// 2. It can only go to someone inside the grantor's own organisation, checked
|
|
// against the contract. This is not a way to hand capacity to strangers.
|
|
// 3. It expires with the month, like every other allowance here. Grants are
|
|
// for unblocking someone now, not for building up a balance.
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let DATA_DIR = null;
|
|
function init(opts) { DATA_DIR = opts.dataDir; }
|
|
function file() { return path.join(DATA_DIR, 'team-grants.json'); }
|
|
function readAll() { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch (e) { return {}; } }
|
|
function writeAll(v) { try { fs.writeFileSync(file(), JSON.stringify(v)); } catch (e) {} }
|
|
function monthKey() { const d = new Date(); return d.getUTCFullYear() + '-' + String(d.getUTCMonth() + 1).padStart(2, '0'); }
|
|
|
|
const MIN_LEVEL = 7;
|
|
|
|
// What a leader can give away each month, by level (index = level - 1).
|
|
// Zero below Vertex — this is the thing the top of the ladder buys.
|
|
const POOLS = {
|
|
traffic: { label: 'Ad impressions', unit: 'impressions', step: 1000, max: 50000,
|
|
pool: [0, 0, 0, 0, 0, 0, 60000, 200000] },
|
|
copy: { label: 'Copy Engine generations', unit: 'generations', step: 25, max: 300,
|
|
pool: [0, 0, 0, 0, 0, 0, 300, 1000] },
|
|
page: { label: 'Page builds', unit: 'pages', step: 1, max: 20,
|
|
pool: [0, 0, 0, 0, 0, 0, 15, 50] },
|
|
textad: { label: 'Text ad batches', unit: 'batches', step: 5, max: 60,
|
|
pool: [0, 0, 0, 0, 0, 0, 60, 200] }
|
|
};
|
|
function tools() {
|
|
return Object.keys(POOLS).map(function (k) {
|
|
return { key: k, label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max };
|
|
});
|
|
}
|
|
|
|
function poolFor(tool, level) {
|
|
const t = POOLS[tool];
|
|
if (!t) return 0;
|
|
const lv = Math.max(1, Math.min(8, Number(level) || 1));
|
|
return t.pool[lv - 1] || 0;
|
|
}
|
|
|
|
function rows(mk) {
|
|
const all = readAll();
|
|
return all[mk || monthKey()] || [];
|
|
}
|
|
|
|
// How much of `tool` this position has RECEIVED this month.
|
|
function receivedBy(memberId, tool) {
|
|
return rows().reduce(function (s, r) {
|
|
return s + ((Number(r.to) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0);
|
|
}, 0);
|
|
}
|
|
|
|
// How much of `tool` this position has GIVEN AWAY this month.
|
|
function givenBy(memberId, tool) {
|
|
return rows().reduce(function (s, r) {
|
|
return s + ((Number(r.by) === Number(memberId) && r.tool === tool) ? Number(r.n) || 0 : 0);
|
|
}, 0);
|
|
}
|
|
|
|
// Who gave it to them — so a recipient can see it came from their upline and
|
|
// not from nowhere. That is half the point: it should feel like being helped.
|
|
function receivedDetail(memberId) {
|
|
const out = {};
|
|
rows().forEach(function (r) {
|
|
if (Number(r.to) !== Number(memberId)) return;
|
|
if (!out[r.tool]) out[r.tool] = { total: 0, from: [] };
|
|
out[r.tool].total += Number(r.n) || 0;
|
|
out[r.tool].from.push({ by: Number(r.by), n: Number(r.n), at: r.at });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
function status(memberId, level) {
|
|
const t = {};
|
|
Object.keys(POOLS).forEach(function (k) {
|
|
const pool = poolFor(k, level);
|
|
const given = givenBy(memberId, k);
|
|
t[k] = {
|
|
label: POOLS[k].label, unit: POOLS[k].unit, step: POOLS[k].step, max: POOLS[k].max,
|
|
pool: pool, given: given, remaining: Math.max(0, pool - given)
|
|
};
|
|
});
|
|
return {
|
|
level: Number(level) || 0, canGrant: Number(level) >= MIN_LEVEL,
|
|
tools: t, resets: monthKey(),
|
|
history: rows().filter(function (r) { return Number(r.by) === Number(memberId); }).slice(-40).reverse(),
|
|
received: receivedDetail(memberId)
|
|
};
|
|
}
|
|
|
|
// Hand capacity to someone. The caller is responsible for having verified that
|
|
// `to` is inside `by`'s organisation — that check needs the chain and lives in
|
|
// the route, not here.
|
|
function grant(opts) {
|
|
const by = Number(opts.by), to = Number(opts.to);
|
|
const tool = String(opts.tool || '');
|
|
const n = Math.floor(Number(opts.n) || 0);
|
|
const level = Number(opts.level) || 0;
|
|
|
|
if (!POOLS[tool]) throw new Error('That is not something you can grant.');
|
|
if (level < MIN_LEVEL) throw new Error('Team Grants unlock at Vertex (level 7).');
|
|
if (!to || to === by) throw new Error('Pick someone in your team other than yourself.');
|
|
if (n <= 0) throw new Error('Choose how much to give.');
|
|
if (n > POOLS[tool].max) throw new Error('The most you can give one person at a time is ' + POOLS[tool].max.toLocaleString() + '.');
|
|
|
|
const pool = poolFor(tool, level);
|
|
const given = givenBy(by, tool);
|
|
if (given + n > pool) {
|
|
throw new Error('That is more than you have left to give this month — ' +
|
|
Math.max(0, pool - given).toLocaleString() + ' ' + POOLS[tool].unit + ' remaining.');
|
|
}
|
|
|
|
const all = readAll();
|
|
const mk = monthKey();
|
|
if (!all[mk]) all[mk] = [];
|
|
const rec = { by: by, to: to, tool: tool, n: n, at: new Date().toISOString() };
|
|
all[mk].push(rec);
|
|
|
|
// Keep three months, same as every other ledger here.
|
|
const keep = Object.keys(all).sort().slice(-3);
|
|
const trimmed = {};
|
|
keep.forEach(function (k) { trimmed[k] = all[k]; });
|
|
writeAll(trimmed);
|
|
return rec;
|
|
}
|
|
|
|
module.exports = { init, MIN_LEVEL, POOLS, tools, poolFor, status, grant, receivedBy, givenBy, receivedDetail };
|