@bongos/core 1.21.176 → 1.21.177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.bongos-core.json +61 -361
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/config/scheduled-routines.json +3 -8
  4. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  5. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  6. package/docs/canonical-permissions.md +1 -1
  7. package/docs/file-map.md +3 -8
  8. package/docs/handoff-template.md +1 -1
  9. package/docs/module-api-changelog.md +2 -0
  10. package/docs/onboarding/diagrams/README.md +3 -7
  11. package/docs/onboarding/diagrams/assertions.json +3 -3
  12. package/docs/recipes/ops-gotchas.md +3 -1
  13. package/modules/autonomy/db.js +9 -0
  14. package/modules/autonomy/module.json +2 -2
  15. package/modules/autonomy/pollers/daily-checks.js +262 -0
  16. package/modules/autonomy/pollers/routine-timers.js +7 -5
  17. package/modules/autonomy/rank-roles.js +64 -0
  18. package/modules/autonomy/server-schedule.js +37 -0
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/release-notes.json +14 -0
  22. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  23. package/scripts/bongos/audit-authorship.sh +0 -5
  24. package/scripts/bongos/audit-rank-roles.js +15 -56
  25. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  26. package/scripts/bongos/diagram-facts.js +1 -1
  27. package/scripts/bongos/do-api.js +1 -1
  28. package/scripts/bongos/fitness-checks-identity.js +4 -15
  29. package/scripts/bongos/gen-diagrams.js +3 -3
  30. package/scripts/bongos/pulse.js +5 -3
  31. package/scripts/bongos/routine-schedule.js +38 -3
  32. package/scripts/bongos/ship-regen.js +2 -3
  33. package/scripts/security/dep-audit.js +50 -124
  34. package/src/module-api.js +1 -1
  35. package/tests/autonomy_daily_checks.mjs +201 -0
  36. package/tests/claude_materialize.mjs +12 -12
  37. package/tests/effective_visibility_predicate.mjs +0 -18
  38. package/tests/routine_capture_kind.mjs +18 -7
  39. package/tests/routine_schedule.mjs +42 -0
  40. package/scripts/bongos/artifact-staleness.js +0 -267
  41. package/scripts/bongos/classify-discipline.js +0 -153
  42. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  43. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  44. package/scripts/bongos/goal-names.js +0 -102
  45. package/scripts/bongos/rename-goals.js +0 -109
  46. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  47. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  48. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  49. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  50. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  51. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  52. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  53. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  54. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  55. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  56. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  57. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  58. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  59. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  60. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  61. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  62. package/scripts/bongos/seed-governance-tasks.js +0 -340
  63. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  64. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  65. package/scripts/bongos/seed-productization-tasks.js +0 -235
  66. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  67. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  68. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  69. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  70. package/scripts/gds/artifact-staleness.js +0 -7
  71. package/scripts/gds/classify-discipline.js +0 -7
  72. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  73. package/scripts/gds/coreB-carve-proof.sh +0 -8
  74. package/scripts/gds/goal-names.js +0 -7
  75. package/scripts/gds/rename-goals.js +0 -7
  76. package/scripts/gds/seed-adopt-tasks.js +0 -7
  77. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  78. package/scripts/gds/seed-board-room-tasks.js +0 -6
  79. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  80. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  81. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  82. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  83. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  84. package/scripts/gds/seed-bongos-tasks.js +0 -7
  85. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  86. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  87. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  88. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  89. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  90. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  91. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  92. package/scripts/gds/seed-governance-tasks.js +0 -7
  93. package/scripts/gds/seed-medusa-tasks.js +0 -7
  94. package/scripts/gds/seed-module-upstreaming.js +0 -7
  95. package/scripts/gds/seed-productization-tasks.js +0 -7
  96. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  97. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  98. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  99. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  100. package/scripts/migrate_cost_ledger.sh +0 -178
  101. package/tests/artifact_staleness.mjs +0 -207
  102. package/tests/goal_names.mjs +0 -120
  103. package/tests/newcomer_generator.mjs +0 -48
@@ -1,104 +0,0 @@
1
- // Seed the tasks for goal 1000048 "SEO & public discoverability — rankable per-project
2
- // profiles" (BONGOS-V1, internal). Source of truth:
3
- // docs/specs/bongos-v1-seo-public-discoverability.md (status: final).
4
- // Idempotent on (source, source_ref). Re-running resolves to the existing rows.
5
- //
6
- // Run: node scripts/bongos/seed-bongos-v1-seo-tasks.js
7
- const os = require('os');
8
- const sess = require(os.homedir() + '/.config/cloudbongos/gds-session.json');
9
- const BASE = (sess.api_base || 'https://cloudbongos.com').replace(/\/$/, '');
10
- const TOKEN = sess.token;
11
- const VER = 'BONGOS-V1';
12
- const GOAL = 1000048;
13
- const SRC = 'planning-session';
14
- const H = { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' };
15
-
16
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
17
- async function api(method, path, body, _tries = 0) {
18
- await sleep(400); // base throttle to stay under the per-IP rate limit
19
- const res = await fetch(`${BASE}/api/bongos${path}`, {
20
- method, headers: H, body: body ? JSON.stringify(body) : undefined,
21
- });
22
- const text = await res.text();
23
- let json; try { json = JSON.parse(text); } catch { json = { _raw: text.slice(0, 300) }; }
24
- if (res.status === 429 && _tries < 8) {
25
- const wait = ((json.error && json.error.details && json.error.details.retry_after_seconds) || 30) + 1;
26
- console.log(` …rate-limited, waiting ${wait}s`);
27
- await sleep(wait * 1000);
28
- return api(method, path, body, _tries + 1);
29
- }
30
- return { status: res.status, ok: res.ok, json };
31
- }
32
-
33
- // criterion slugs (position-independent — the goal's done_when_criteria.criterion_id)
34
- const PP = 'seo-project-profile-page', NS = 'seo-toplevel-namespace', PC = 'seo-project-content',
35
- PM = 'seo-page-metadata', CC = 'seo-canonical-consolidation', SR = 'seo-sitemap-robots',
36
- VG = 'seo-visibility-gated', SB = 'seo-site-baseline';
37
-
38
- // disc: engineer (backend/API/spike) | ui (page/markup/meta). kind: spike | feature | refactor | infra | verify (proofs).
39
- // deps: 'R##' resolve to the just-created id; a NUMBER is a real cross-goal task id, passed through.
40
- const TASKS = [
41
- { ref:'R01', kind:'spike', disc:'engineer', prio:5, est:180, crit:[PP,NS,PM,CC], deps:[], title:'R01 · SEO + namespace architecture ADR', desc:'Needs-exploration: profile-page rendering (server-rendered HTML from the Node/Express hub), routing precedence (/<slug> vs /bongos/* vs /api/*), the owner-picked slug model + reserved-word list, the cross-instance canonical mechanism, and how the profile reads goal-1000046 setup content. Output an ADR + confirmed sub-tasks. See docs/specs/bongos-v1-seo-public-discoverability.md.' },
42
- { ref:'R02', kind:'feature', disc:'engineer', prio:5, est:240, crit:[PP,NS], deps:['R01'], title:'R02 · Owner-picked slug at creation + reserved-name validation + backfill', desc:'Projects get a unique owner-chosen vanity slug at creation (username-style), validated vs a reserved list (bongos, api, sso, gate, create, explore, builders, status, assets, sitemap.xml, robots.txt, ...). Auto-derive + backfill a default slug for existing projects (owner can change). Adds a step to the creation flow — coordinate with goals 1000035/1000046. Deps R01.' },
43
- { ref:'R03', kind:'refactor',disc:'engineer', prio:5, est:240, crit:[NS], deps:['R01'], title:'R03 · Move platform pages under /bongos/… ; top-level /<slug> resolves to a project', desc:'Serve hall/status/create/explore/gate under /bongos/… with 301 redirects from old paths; never move /api, /sso, /auth, /.well-known. Top-level /<slug> resolves to a project profile after the reserved-word check. Deps R01.' },
44
- { ref:'R04', kind:'feature', disc:'ui', prio:5, est:240, crit:[PP,PM], deps:['R02','R03'],title:'R04 · Server-rendered project profile page at /<slug>', desc:'Crawlable server-rendered HTML: project name, description, cover art, builders, tech/modules, and a prominent "Open the live app" CTA. This page is the primary shareable destination. Deps R02,R03.' },
45
- { ref:'R05', kind:'feature', disc:'ui', prio:4, est:180, crit:[PC], deps:['R04'], title:'R05 · Render goal-1000046 setup content on the profile + auto-derive SEO copy', desc:'Read the project\'s setup content (goal 1000046) and render it on the profile; auto-derive SEO title/description from it. No separate editor (per interview). Coordinate with 1000046 — needs a real description + tags; render current fields (name/tagline) meanwhile and enrich as 1000046 lands. Deps R04.' },
46
- { ref:'R06', kind:'feature', disc:'ui', prio:4, est:150, crit:[PM,CC], deps:['R04'], title:'R06 · Per-page SEO metadata (title/description/OG/Twitter/self-canonical)', desc:'Dynamic <title>, meta description, Open Graph + Twitter card tags, and a self-referencing canonical on each profile. Deps R04.' },
47
- { ref:'R07', kind:'feature', disc:'ui', prio:4, est:120, crit:[PM], deps:['R04'], title:'R07 · JSON-LD structured data on profiles', desc:'Emit SoftwareApplication/CreativeWork JSON-LD on each profile page. Deps R04.' },
48
- { ref:'R08', kind:'feature', disc:'ui', prio:4, est:150, crit:[PP], deps:['R04'], title:'R08 · Route discovery through profiles (orbs + links → /<slug>)', desc:'Make /<slug> the shareable destination: orbs cards and "view project" links point to the profile, which carries the "Open the live app" CTA — maximizing internal links to the ranking page. Deps R04. NOTE: the orbs front door is a separate static site — the orbs-side link change likely lands instance-side.' },
49
- { ref:'R09', kind:'feature', disc:'ui', prio:4, est:180, crit:[PP,SR], deps:['R04'], title:'R09 · Crawlable projects directory via the orbs table-view', desc:'Make the orbs page\'s table view a server-rendered, crawlable, indexable HTML index of all Public projects (a plain-link list to each /<slug>), linked from the landing. This IS the directory (no separate directory page, per interview). Deps R04.' },
50
- { ref:'R10', kind:'feature', disc:'engineer', prio:4, est:180, crit:[SR], deps:['R04','R11'],title:'R10 · sitemap.xml (public projects + core pages)', desc:'Auto-generated sitemap.xml at /sitemap.xml listing all Public project profiles + core pages; regenerates on project create/update/visibility-change (cached). Deps R04,R11.' },
51
- { ref:'R11', kind:'feature', disc:'engineer', prio:4, est:210, crit:[VG], deps:['R04'], title:'R11 · Visibility-gated indexing (visibility metaphor)', desc:'Public→full indexable profile; Private (star)→minimal page + join/request CTA, noindex; Stealth (black hole)→404; Unlisted (dark matter)→no public page until published. HARD conceptual dependency on goal 1000046\'s visibility states (not yet seeded) — coordinate; gate on the state field once it exists. Deps R04.' },
52
- { ref:'R12', kind:'feature', disc:'engineer', prio:3, est:120, crit:[SR], deps:['R10'], title:'R12 · robots.txt', desc:'Serve robots.txt: allow public pages, disallow /api + internal, reference /sitemap.xml. Deps R10.' },
53
- { ref:'R13', kind:'feature', disc:'engineer', prio:3, est:240, crit:[CC], deps:['R04'], title:'R13 · Cross-instance canonical (full scope)', desc:'rel=canonical from each live instance back to its hub profile: federated/co-tenant instances emit it; hub-hosted subdomains when that tier lands; publish a documented canonical snippet for self-hosted owners. Touches instance-side core rendering → Metic+ auto-floor. Coordinate with provisioning/federation (goals 1000032/1000041). Deps R04.' },
54
- { ref:'R14', kind:'feature', disc:'ui', prio:3, est:210, crit:[PP,PM], deps:['R04',1002278], title:'R14 · Public builder-profile SEO page (handle-based path)', desc:'Server-rendered, SEO-optimized public builder profile at a handle path (e.g. /u/<handle>): metadata + Person/ProfilePage JSON-LD, crawlable HTML. Extends goal 1000045\'s builder profiles with a public, rankable rendering. Deps R04 + goal-1000045 profile API (task 1002278).' },
55
- { ref:'R15', kind:'feature', disc:'engineer', prio:3, est:150, crit:[SR,VG], deps:['R14','R10'],title:'R15 · Builder profiles in sitemap, consent-gated', desc:'Add consented/public builder profiles to sitemap.xml; honor goal-1000045\'s hide/opt-out flag (coordinate with 1000045 R03). Non-consented builders excluded/noindex. Deps R14,R10.' },
56
- { ref:'R16', kind:'feature', disc:'ui', prio:2, est:120, crit:[PC], deps:['R05'], title:'R16 · SEO content nudges in the setup/edit flow', desc:'Rule-based nudges where owners author content (goal 1000046 setup/edit): description length, tags, cover image — so profiles have enough real text to rank. Coordinate with 1000046. Deps R05.' },
57
- { ref:'R17', kind:'feature', disc:'ui', prio:3, est:150, crit:[SB], deps:['R01'], title:'R17 · Landing + Explore SEO metadata', desc:'Give the landing page (goal 1000031) and the Explore page proper title/description/Open Graph/structured data and ensure they are indexable. Coordinate with 1000031. Deps R01.' },
58
- { ref:'R18', kind:'infra', disc:'engineer', prio:2, est:180, crit:[SB], deps:['R04'], title:'R18 · Technical-SEO + Core Web Vitals pass', desc:'Audit + fix technical SEO across public pages: mobile-friendly, load speed / Core Web Vitals, indexability. Deps R04.' },
59
- { ref:'R19', kind:'verify', disc:'engineer', prio:2, est:120, crit:[PP,PM,CC,SR,VG,PC], deps:['R04','R06','R07','R10','R11','R14','R15'], title:'R19 · PROOF: a public project + builder profile rank-ready; non-public hidden', desc:'End-to-end proof: a real Public project profile is live at cloudbongos.com/<slug> with correct metadata + JSON-LD, appears in sitemap.xml, passes Google Rich Results + mobile-friendly tests; a public builder profile is likewise rank-ready; and a Private project is noindex + absent from the sitemap (Stealth 404s). Deps R04,R06,R07,R10,R11,R14,R15.' },
60
- ];
61
-
62
- const ROOTS = ['R01']; // zero-dep entry task to open for claiming
63
-
64
- (async () => {
65
- const idByRef = {};
66
- let created = 0, existing = 0;
67
- console.log(`Seeding ${TASKS.length} tasks under goal ${GOAL} (${VER})…`);
68
- for (const t of TASKS) {
69
- const r = await api('POST', '/tasks', {
70
- version_id: VER, goal_id: GOAL, title: t.title, description: t.desc,
71
- kind: t.kind, discipline: t.disc, priority: t.prio, est_minutes: t.est,
72
- criterion_ids: t.crit, source: SRC, source_ref: `bv1-seo-${t.ref}`,
73
- });
74
- if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
75
- const task = r.json.task || r.json;
76
- idByRef[t.ref] = task.id;
77
- if (r.json.idempotent || r.json.existing) existing++; else created++;
78
- console.log(` ${t.ref} → task ${task.id} (${t.kind}/${t.disc}, P${t.prio}, [${t.crit.join(',')}])`);
79
- }
80
-
81
- console.log('Wiring dependencies…');
82
- let edges = 0;
83
- for (const t of TASKS) {
84
- if (!t.deps.length) continue;
85
- const id = idByRef[t.ref];
86
- const depIds = t.deps.map((d) => (typeof d === 'number' ? d : idByRef[d])).filter(Boolean);
87
- if (!id || depIds.length !== t.deps.length) { console.log(` ${t.ref}: dep resolve gap, skipping`); continue; }
88
- const r = await api('POST', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
89
- if (!r.ok) { console.log(` ${t.ref}: dep FAILED ${r.status} ${JSON.stringify(r.json)}`); continue; }
90
- edges += (r.json.added || depIds.length);
91
- console.log(` ${t.ref} depends on ${t.deps.join(',')}`);
92
- }
93
-
94
- console.log('Promoting root(s) to ready…');
95
- for (const ref of ROOTS) {
96
- const id = idByRef[ref];
97
- if (!id) continue;
98
- const r = await api('POST', `/tasks/${id}/promote`, {});
99
- console.log(` ${ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.json).slice(0, 120)}`);
100
- }
101
-
102
- console.log(`\nDONE. created=${created} existing=${existing} depEdges=${edges}. Roots opened: ${ROOTS.join(', ')}.`);
103
- console.log('ref→id:', JSON.stringify(idByRef));
104
- })();
@@ -1,131 +0,0 @@
1
- // Seed the community-tab build tasks the 1002782 spike scoped, into the
2
- // community-surface goal (created by the spike; pass --goal <id>).
3
- //
4
- // Source of truth: docs/specs/bongos-v1-community-tab.md (owner-signed
5
- // 2026-08-16: own goal; builder-of-week auto-computed). Idempotent on
6
- // source_ref bv1-community-CTn; safe to re-run (tasks re-home if --goal moves).
7
- //
8
- // Run: node scripts/bongos/seed-bv1-community-tasks.js --goal 1000065
9
-
10
- const { apiCall, arg } = require('./cli-lib');
11
-
12
- const VER = 'BONGOS-V1';
13
- const SRC = 'planning-session';
14
- const SPEC = 'docs/specs/bongos-v1-community-tab.md';
15
-
16
- // Cross-goal dependency: the shared privacy predicate (privacy spike PV2).
17
- const PV2 = 1002967;
18
-
19
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
-
21
- async function api(method, path, body, _tries = 0) {
22
- await sleep(1500);
23
- const r = await apiCall(method, `/api/bongos${path}`, body);
24
- if (r.status === 429 && _tries < 8) {
25
- const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
26
- console.log(` …rate-limited, waiting ${wait}s`);
27
- await sleep(wait * 1000);
28
- return api(method, path, body, _tries + 1);
29
- }
30
- return r;
31
- }
32
-
33
- const NEW_TASKS = [
34
- {
35
- ref: 'CT1', kind: 'feature', prio: 3, est: 120, crit: ['CS5'], deps: [], ready: true,
36
- title: 'Windowed data plane: activity snapshots appended on changed-totals rollup reports',
37
- desc: `New table platform_identity_activity_snapshots (client_id, github_id, credits, tasks_shipped, karma, reported_at) — additive platform_identity_* migration — plus one INSERT on the existing rollup-report path, skipped when totals are unchanged. Window value per builder = latest totals minus last snapshot at-or-before window start, summed across projects; missing baseline counts from zero. Reporting-time (not earning-time) semantics — document honestly, options B/C recorded in ${SPEC} D2. Compaction (daily granularity after 90 days) is a later, volume-triggered task: note it in this task's handoff, do not build it.`,
38
- },
39
- {
40
- ref: 'CT2', kind: 'feature', prio: 3, est: 180, crit: ['CS1', 'CS5'], deps: ['CT1', PV2],
41
- title: 'Windowed leaderboard API: GET /community/leaderboard',
42
- desc: `Public endpoint: ?window=7d|30d|365d|all&discipline=. All-time reads the existing rollup; windowed reads CT1 deltas. Eligibility via the SHARED privacy predicate (task ${PV2} / privacy spike PV2) — active + public + stats shown; no surface-local variant. Columns: credits (rank), tasks_shipped, karma; response identical for every viewer. ${SPEC} D3.`,
43
- },
44
- {
45
- ref: 'CT3', kind: 'feature', prio: 3, est: 120, crit: ['CS2'], deps: [PV2],
46
- title: 'Builder search API: name + discipline under public-profile rules',
47
- desc: `Public search over active+public accounts: handle/display-name prefix + trigram, discipline filter. Matrix consequences: hide_stats accounts findable by NAME but excluded from discipline-FILTERED results (disciplines are rollup-derived); private + provisional absent. This is not the recruiting search — the discoverable sliver population never appears here. ${SPEC} D4.`,
48
- },
49
- {
50
- ref: 'CT4', kind: 'feature', prio: 3, est: 180, crit: ['CS1', 'CS2'], deps: ['CT2', 'CT3'],
51
- title: 'The /community page: leaderboards + search + feature cards',
52
- desc: `The public tab on the hub apex (status-ui, beside Explore): leaderboard windows + discipline tabs, search, builder-of-week + rising-builder card slots, server-render-friendly markup (goal 1000048 indexes later; their specifics). 'community' is already in the reserved slug list (edited by spike 1002782). ${SPEC} D1/D3/D4.`,
53
- },
54
- {
55
- ref: 'CT5', kind: 'feature', prio: 3, est: 120, crit: ['CS3'], deps: ['CT2'],
56
- title: 'Builder of the Week: crown-at-render + weekly_features table + archive',
57
- desc: `Owner-signed formula: top 7d credit delta among leaderboard-eligible builders, floor one shipped work in window, ties by ships then karma. Crowned at first render after Monday 00:00 UTC and recorded to platform_identity_weekly_features (github_id, week_start, credits_delta, ships_delta) so the crown is pinned and the archive section renders from real rows. No cron. Formula shown on the page. ${SPEC} D5.`,
58
- },
59
- {
60
- ref: 'CT6', kind: 'feature', prio: 4, est: 90, crit: ['CS4'], deps: ['CT4'],
61
- title: 'Recruit affordance for project.recruit holders',
62
- desc: `Leaderboard rows + search results grow a Recruit button rendered only for holders of project.recruit (the console wave's atom — coordinate spike 1002786 R2/R4), linking into the console invite flow. A door to a gated surface, never a data reveal. ${SPEC} D6.`,
63
- },
64
- {
65
- ref: 'CT7', kind: 'feature', prio: 4, est: 90, crit: ['CS1'], deps: ['CT2'],
66
- title: 'Rising builder + most-active-projects cards',
67
- desc: `Rising builder: biggest relative 30d delta on a small all-time base (same CT1 snapshots). Most-active projects: the snapshots aggregated on the project axis, a strip on the community tab (orb-sizing reuse is goal 1000044 territory — feed it, don't build it here). ${SPEC} D7 items 1-2.`,
68
- },
69
- {
70
- ref: 'CT8', kind: 'infra', prio: 4, est: 90, crit: ['CS3'], deps: ['CT2', 'CT5'],
71
- title: 'Weekly Discord community digest',
72
- desc: `A scheduled routine posting top movers + builder of the week via the existing postWebhook plumbing (scripts/discord/lib.js channels). Owner-audience copy, no jargon. ${SPEC} D7 item 4.`,
73
- },
74
- {
75
- ref: 'CT9', kind: 'verify', prio: 3, est: 90, crit: ['CS1', 'CS2', 'CS3'], deps: ['CT2', 'CT3', 'CT4', 'CT5'],
76
- title: 'PROOF: matrix enforcement + window math on the community surfaces',
77
- desc: `Seed fixture accounts in every privacy state and snapshot rows across window boundaries; assert: private/provisional/hidden absent from every board, search result, feature card; name-vs-discipline search asymmetry for hide_stats; window deltas exact against the seeded snapshots; anonymous and signed-in responses identical. ${SPEC} D8.`,
78
- },
79
- ];
80
-
81
- (async () => {
82
- const goalId = Number(arg('--goal'));
83
- if (!goalId) {
84
- console.error('usage: node scripts/bongos/seed-bv1-community-tasks.js --goal <id>');
85
- process.exit(2);
86
- }
87
- console.log(`Seeding community-tab tasks under goal ${goalId} (${VER})…`);
88
- const idByRef = {};
89
- let created = 0, existing = 0;
90
-
91
- for (const t of NEW_TASKS) {
92
- const r = await api('POST', '/tasks', {
93
- version_id: VER,
94
- goal_id: goalId,
95
- title: t.title,
96
- description: t.desc,
97
- kind: t.kind,
98
- discipline: 'engineer',
99
- priority: t.prio,
100
- est_minutes: t.est,
101
- criterion_ids: t.crit,
102
- source: SRC,
103
- source_ref: `bv1-community-${t.ref}`,
104
- });
105
- if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`); continue; }
106
- const task = r.data.task || r.data;
107
- idByRef[t.ref] = Number(task.id);
108
- if (r.data.idempotent || r.data.existing) existing++; else created++;
109
- console.log(` ${t.ref} → task ${task.id} (${t.kind}, P${t.prio})`);
110
- }
111
-
112
- console.log('Wiring dependencies…');
113
- for (const t of NEW_TASKS.filter((x) => x.deps.length)) {
114
- const id = idByRef[t.ref];
115
- const deps = t.deps.map((d) => (typeof d === 'number' ? d : idByRef[d])).filter(Boolean);
116
- if (!id || !deps.length) continue;
117
- const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: deps });
118
- console.log(` ${t.ref} (task ${id}) depends on [${deps.join(', ')}]: ${r.ok ? 'ok' : 'FAILED ' + r.status + ' ' + JSON.stringify(r.data).slice(0, 160)}`);
119
- }
120
-
121
- console.log('Promoting the zero-dep root…');
122
- for (const t of NEW_TASKS.filter((x) => x.ready)) {
123
- const id = idByRef[t.ref];
124
- if (!id) continue;
125
- const r = await api('POST', `/tasks/${id}/promote`, {});
126
- console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
127
- }
128
-
129
- console.log(`\nDONE. created=${created} existing=${existing}.`);
130
- console.log('ref→id:', JSON.stringify(idByRef));
131
- })();
@@ -1,133 +0,0 @@
1
- // Seed the privacy-model + invite-inbox build tasks the 1002785 spike scoped,
2
- // under goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
3
- //
4
- // Source of truth: docs/specs/bongos-v1-privacy-and-invites.md (owner-signed
5
- // 2026-08-16 on the sliver + expiry decisions). Idempotent on source_ref
6
- // bv1-privacy-PVnn; safe to re-run. PV4 stays backlog deliberately — its
7
- // instance-invite half attaches to the console wave's R2/R6 (spike 1002786),
8
- // which seeds into its own goal; only PV6 is promoted (fully independent).
9
- //
10
- // Run: node scripts/bongos/seed-bv1-privacy-invites-tasks.js
11
-
12
- const { apiCall } = require('./cli-lib');
13
-
14
- const VER = 'BONGOS-V1';
15
- const GOAL = 1000045;
16
- const SRC = 'planning-session';
17
- const SPEC = 'docs/specs/bongos-v1-privacy-and-invites.md';
18
-
19
- // Existing tasks referenced by id (seed-bv1-profiles-tasks.js).
20
- const R02 = 1002278, R03 = 1002279;
21
-
22
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
23
-
24
- // Throttled + 429-aware wrapper (same shape as the sibling seeders).
25
- async function api(method, path, body, _tries = 0) {
26
- await sleep(1500);
27
- const r = await apiCall(method, `/api/bongos${path}`, body);
28
- if (r.status === 429 && _tries < 8) {
29
- const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
30
- console.log(` …rate-limited, waiting ${wait}s`);
31
- await sleep(wait * 1000);
32
- return api(method, path, body, _tries + 1);
33
- }
34
- return r;
35
- }
36
-
37
- const NEW_TASKS = [
38
- {
39
- ref: 'PV1', kind: 'feature', prio: 3, est: 120, crit: ['C2'], deps: [R03],
40
- title: 'Privacy flags: account_visibility + recruiter_discoverable on platform accounts',
41
- desc: `Add account_visibility ('public'|'private', default 'public') and recruiter_discoverable (boolean NULL — tri-state; effective = COALESCE(flag, visibility='public')) to platform_identity_accounts via an additive platform_identity_* migration. Extend the own-scoped PATCH write path task ${R03} (R03) establishes for hide_stats to cover both, and add the two settings rows to the profile rail (coordinate P04, task 1002800). The discoverable toggle's copy must state exactly what it grants: the D3 sliver (name, disciplines, active-recently), name-only if stats are hidden. Model + defaults: ${SPEC} D1.`,
42
- },
43
- {
44
- ref: 'PV2', kind: 'feature', prio: 3, est: 180, crit: ['C1', 'C2'], deps: [R02],
45
- title: 'Effective-visibility enforcement: one shared predicate across every public read',
46
- desc: `One helper (SQL predicate + port function, the ADR 0171 D2 "enforced next to the data" pattern) that every public surface uses: /u/<handle> returns 404 (never 403) for private accounts TO EVERYONE INCLUDING SELF on the public read (the page renders self-view via the own-scope side door instead — D2's viewer-independence reconciliation); private accounts absent from search, leaderboards, sitemap; the rollup predicate extends profile_state='active' AND NOT hide_stats with visibility. No surface writes its own variant — drift here is a privacy leak. Matrix: ${SPEC} D2. Also depends on PV1 (bv1-privacy-PV1).`,
47
- },
48
- {
49
- ref: 'PV3', kind: 'feature', prio: 3, est: 120, crit: ['C2', 'C5'], deps: [],
50
- title: 'getRecruiterSliver port + recruiting-surface gating (the pre-accept boundary)',
51
- desc: `The owner-signed D3 sliver: handle, display name, avatar, disciplines union, active-in-last-30-days boolean — nothing else, name-only when hide_stats is true, never for provisional accounts, never a profile-page link. Authorization-scoped port getRecruiterSliver(githubId) beside getPublicProfileExtras, whitelist-shaped inside platform-identity, its own planted-sentinel leak test (tests/profile_route.mjs pattern), route-gated by project.recruit (the console wave's atom — coordinate spike 1002786 R3). Depends on PV1+PV2 (bv1-privacy-PV1/PV2). Boundary: ${SPEC} D3.`,
52
- },
53
- {
54
- ref: 'PV4', kind: 'feature', prio: 3, est: 120, crit: ['C3'], deps: [],
55
- title: 'Invite lifecycle: 30-day read-time expiry, seen_at, decline closes the door',
56
- desc: `Owner-signed: a recruit invite's pre-approved sign-in door expires 30 days after creation, enforced READ-TIME (the sign-in gate's invited-check + every queue view compute created_at + 30 days; no sweeper). seen_at lands on the hub notification row (set on first inbox render) and on goal_membership_requests for parity (goal invites get NO expiry — not an admission door). Decline in the inbox relays a door-close to the instance (invite row → dismissed) so no declined invite leaves a live door. COORDINATION: the invite storage itself is the console wave's R2 (access_requests kind='invite') and rescind-cleans-both-sides is its R6 (spike 1002786) — this task lands the states on whatever R2/R6 ship, which is why it stays backlog until that wave seeds. Lifecycle: ${SPEC} D4.`,
57
- },
58
- {
59
- ref: 'PV5', kind: 'feature', prio: 3, est: 90, crit: ['C3'], deps: [],
60
- title: 'Hub inbox project slice: recruit invites in the attention card',
61
- desc: `A project slice in the EXISTING attention card (goal-inbox.js slice pattern + window.OTBAttention.refresh()), on the hub hall + apex home, fed by the existing GET /my-projects/invites. Accept = "open <project> — sign in" link (clicking through IS acceptance, console D2); Decline = the PV4 door-close path. No parallel inbox; federated instances' halls link out only (V1). Notification path IS the inbox — SSE/email explicitly deferred. Depends on PV4 (bv1-privacy-PV4). Shape: ${SPEC} D5.`,
62
- },
63
- {
64
- ref: 'PV6', kind: 'feature', prio: 4, est: 90, crit: [], deps: [], ready: true,
65
- title: 'Private goals: accepting_requests toggle + owner rescind-pending-invite route',
66
- desc: `goals.accepting_requests boolean default true — a private goal with it false refuses join-requests with honest copy ("this goal isn't taking requests right now"); invitations unaffected; UI shows the toggle only on private goals. Plus the missing half of core_188's 'cancelled' state: an owner/manager route to rescind a PENDING invitation (only requester-withdraw exists today — modules/lifecycle/routes/goals.js). Design: ${SPEC} D6.`,
67
- },
68
- {
69
- ref: 'PV7', kind: 'feature', prio: 3, est: 90, crit: ['C4'], deps: [],
70
- title: 'Applications carry the applicant profile: note through the relay, live render in the queue',
71
- desc: `One rule, no new disclosure class: the reviewer sees what the applicant's profile rules would show them + the applicant's note, evaluated LIVE at review time (never a snapshot — hide takes effect on next render, C2). Public account → public profile view; private account → the D3 sliver (the apply UI states "your application shares: …"). Plumbing: POST /my-projects/join gains an applicant-supplied note (capped ~500 chars, sanitized; replaces the hardcoded 'via platform My Projects'), and the reviewer queue (console R4's page — spike 1002786) resolves the profile from the hub at render. Depends on PV3 (bv1-privacy-PV3). Rule: ${SPEC} D7.`,
72
- },
73
- {
74
- ref: 'PV8', kind: 'verify', prio: 3, est: 60, crit: ['C2'], deps: [],
75
- title: 'PROOF: the privacy matrix as a test',
76
- desc: `The D2 matrix, asserted: a private account's /u/<handle> public read 404s to a stranger AND to its own authenticated session (the page self-view comes from the own-scope endpoint — assert both halves, this is P10 task 1002806 sharpened); hidden stats never appear in any response (planted sentinels); the recruiter sliver is exactly D3's shape (leak test on the port); an expired invite refuses sign-in; a declined invite's door is closed instance-side. Depends on PV2+PV3+PV4 (bv1-privacy-PV2/PV3/PV4). Matrix: ${SPEC} D2.`,
77
- },
78
- ];
79
-
80
- (async () => {
81
- console.log(`Seeding privacy+invites tasks under goal ${GOAL} (${VER})…`);
82
- const idByRef = {};
83
- let created = 0, existing = 0;
84
-
85
- for (const t of NEW_TASKS) {
86
- const r = await api('POST', '/tasks', {
87
- version_id: VER,
88
- goal_id: GOAL,
89
- title: t.title,
90
- description: t.desc,
91
- kind: t.kind,
92
- discipline: 'engineer',
93
- priority: t.prio,
94
- est_minutes: t.est,
95
- criterion_ids: t.crit,
96
- source: SRC,
97
- source_ref: `bv1-privacy-${t.ref}`,
98
- });
99
- if (!r.ok) { console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`); continue; }
100
- const task = r.data.task || r.data;
101
- idByRef[t.ref] = Number(task.id);
102
- if (r.data.idempotent || r.data.existing) existing++; else created++;
103
- console.log(` ${t.ref} → task ${task.id} (${t.kind}, P${t.prio})`);
104
- }
105
-
106
- console.log('Wiring dependencies…');
107
- const DEP_SETS = [
108
- ['PV1', [R03]],
109
- ['PV2', [R02, idByRef.PV1]],
110
- ['PV3', [idByRef.PV1, idByRef.PV2]],
111
- ['PV5', [idByRef.PV4]],
112
- ['PV7', [idByRef.PV3]],
113
- ['PV8', [idByRef.PV2, idByRef.PV3, idByRef.PV4]],
114
- ];
115
- for (const [ref, deps] of DEP_SETS) {
116
- const id = idByRef[ref];
117
- const clean = deps.filter(Boolean);
118
- if (!id || !clean.length) continue;
119
- const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: clean });
120
- console.log(` ${ref} (task ${id}) depends on [${clean.join(', ')}]: ${r.ok ? 'ok' : 'FAILED ' + r.status + ' ' + JSON.stringify(r.data).slice(0, 160)}`);
121
- }
122
-
123
- console.log('Promoting the independent root…');
124
- for (const t of NEW_TASKS.filter((x) => x.ready)) {
125
- const id = idByRef[t.ref];
126
- if (!id) continue;
127
- const r = await api('POST', `/tasks/${id}/promote`, {});
128
- console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
129
- }
130
-
131
- console.log(`\nDONE. created=${created} existing=${existing}.`);
132
- console.log('ref→id:', JSON.stringify(idByRef));
133
- })();
@@ -1,161 +0,0 @@
1
- // Seed the delta tasks the profile-page spike (task 1002783) surfaced, under
2
- // goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
3
- //
4
- // Source of truth: docs/specs/bongos-v1-builder-profile-page.md (status: final).
5
- // The Phase-1 spine (R01-R06) was already seeded by seed-bv1-profiles-tasks.js;
6
- // this file adds ONLY what that spec found on top of it, so the two seeders are
7
- // disjoint and both stay re-runnable.
8
- //
9
- // Idempotent on (source, source_ref) — re-running resolves to the existing rows
10
- // and re-asserts the dependency edges.
11
- //
12
- // Run: node scripts/bongos/seed-bv1-profile-page-tasks.js
13
-
14
- const { apiCall } = require('./cli-lib');
15
-
16
- const VER = 'BONGOS-V1';
17
- const GOAL = 1000045;
18
- const SRC = 'planning-session';
19
- const SPEC = 'docs/specs/bongos-v1-builder-profile-page.md';
20
-
21
- // Tasks that already exist and that these depend on — referenced by id, since
22
- // they were seeded by the sibling seeder (or by the wave task 1002781).
23
- const R01 = 1002277; // spike · design profile↔rollup join + consent model
24
- const R02 = 1002278; // feature · profile API returns cross-project record
25
- const R03 = 1002279; // feature · hide flag
26
- const R05 = 1002281; // feature · shareable-by-login profile URL
27
- const SETTINGS_SPIKE = 1002784; // spike · account settings page — inventory + placement
28
- const PRIVACY_SPIKE = 1002785; // spike · account privacy model + invite inbox
29
-
30
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
31
-
32
- // Throttled + 429-aware wrapper. The seeder makes ~20 writes in a burst, which
33
- // is exactly the shape the per-IP limiter is there to catch.
34
- async function api(method, path, body, _tries = 0) {
35
- await sleep(400);
36
- const r = await apiCall(method, `/api/bongos${path}`, body);
37
- if (r.status === 429 && _tries < 8) {
38
- const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
39
- console.log(` …rate-limited, waiting ${wait}s`);
40
- await sleep(wait * 1000);
41
- return api(method, path, body, _tries + 1);
42
- }
43
- return r;
44
- }
45
-
46
- // deps entries are either a local ref ('P01') or a numeric id of an existing task.
47
- const TASKS = [
48
- {
49
- ref: 'P01', kind: 'feature', disc: 'engineer', prio: 5, est: 180, crit: ['C1'], deps: [R01],
50
- title: 'P01 · Builder handle on platform accounts (seed, dedupe, reserved list, rename)',
51
- desc: `Add a \`handle\` column to platform_identity_accounts: seeded from github_login at creation, deduped with a numeric suffix on collision, validated against a small reserved list (me, new, edit, settings, admin, api, u), builder-editable at a low rate limit, previous handle retained for a 301 window. github_id stays the join key everywhere — the handle is presentation only. WHY: github_login is mutable, so hanging the canonical profile URL on it means every GitHub rename silently 404s that builder's shared links. Namespaced module migration (ADR 0083). Spec: ${SPEC} (Decision 3).`,
52
- },
53
- {
54
- ref: 'P02', kind: 'feature', disc: 'engineer', prio: 5, est: 180, crit: ['C1'], deps: [R01],
55
- title: 'P02 · Profile fields: bio + typed links on the platform account',
56
- desc: `Add \`bio\` (plain text, ~280 chars, NO markdown and no auto-linking) and \`links\` (a TYPED, capped set — website, X, LinkedIn, Discord; max 4) to platform_identity_accounts, plus the authenticated write. Server-side validation must enforce https-only AND reject non-http(s) schemes and userinfo/host tricks — do not rely on the client. Render with rel="nofollow ugc noopener". WHY typed-and-capped rather than a free-form link list: a public, indexable page with user-controlled links is an SEO-spam magnet, and this removes the incentive without a moderation queue. Spec: ${SPEC} (Decision 4).`,
57
- },
58
- {
59
- ref: 'P03', kind: 'decision', disc: 'engineer', prio: 5, est: 60, crit: ['C1'], deps: [], ready: true,
60
- title: 'P03 · Owner decision: provisional vs active profile state for check-in-created accounts',
61
- desc: `Goal 1000045 justifies "public by default" as tied to having a platform account, "which already carries platform T&C consent". That premise is not currently true for every account: POST /sso/membership/check-in and POST /sso/activity/rollup cause the hub to upsert an account for a builder who has never visited the hub and never saw a platform T&C. Under auto-create (see task 1002783) those people would get a public, indexable, cross-project profile from a sign-in they performed on someone else's instance. RECOMMENDATION: a profile_state of 'provisional' (created by check-in — still counted in aggregates, but not publicly listed, not at the handle URL, not in the sitemap, not in hub search) vs 'active' (has signed in at the hub at least once). One column, one branch. This is an owner product/consent call, and it changes the shape of P09. Spec: ${SPEC}.`,
62
- },
63
- {
64
- ref: 'P04', kind: 'feature', disc: 'ui', prio: 4, est: 180, crit: ['C1'], deps: ['P01', 'P02', SETTINGS_SPIKE],
65
- title: 'P04 · Settings → Profile rail group (the single profile-edit surface)',
66
- desc: `One edit surface for the profile, as a new rail group in the hall Settings page (the RAIL_GROUPS pattern in modules/hall-ui/public/settings.js) — not a separate edit page. Holds handle, bio, links alongside the display-name and disciplines controls that already live there. Account-global fields must be edited where they are stored; final hub-vs-per-project placement is task ${SETTINGS_SPIKE}'s call, which is why this waits on it. Spec: ${SPEC} (Decision 4).`,
67
- },
68
- {
69
- ref: 'P05', kind: 'verify', disc: 'engineer', prio: 5, est: 120, crit: ['C1'], deps: [R02],
70
- title: 'P05 · Leak test for the hub profile read (mirror of tests/profile_route.mjs)',
71
- desc: `The instance route GET /builders/:id/profile projects an explicit public column whitelist and tests/profile_route.mjs proves the private columns (anthropic_email, anthropic_user_id, monthly_budget_usd, system_role, box_blocked*, deactivated_reason) never appear anywhere in the serialized response. The new cross-project read needs its OWN mirror test over the account + rollup projection before it is public — same sentinel-string technique, same 404-on-unknown-id assertion. Never spread a raw row. Spec: ${SPEC} (Constraints §1).`,
72
- },
73
- {
74
- ref: 'P06', kind: 'feature', disc: 'ui', prio: 3, est: 120, crit: ['C1'], deps: ['P02'],
75
- title: 'P06 · "Complete your profile" needs-attention inbox line item (dismissible)',
76
- desc: `When bio or preferred_disciplines are empty, nudge the builder — as a DISMISSIBLE line item in the needs-attention inbox (goal-inbox.js / #attention-scroll), never a new Home card. That is the hall placement rule (modules/hall-ui/CLAUDE.md → Placement): an action awaiting the builder is an inbox line. This is the replacement for the profile-setup wizard that task 1002783 rejected. Spec: ${SPEC} (Decision 2).`,
77
- },
78
- {
79
- ref: 'P07', kind: 'feature', disc: 'engineer', prio: 3, est: 210, crit: ['C1'], deps: [R02],
80
- title: 'P07 · Hub recent works via push — recent_ships[] on the activity rollup',
81
- desc: `The hub has no tasks table for other projects, so "recent works" cannot render there today. The existing PULL pattern (modules/platform-identity/project-stats.js) fans out to each project's public API — right for a builder count on a catalog page, wrong per-profile-view, and it cannot serve a CLI-only project at all. Extend the POST /sso/activity/rollup payload (which already carries credits/ships/karma/rank/disciplines per project) with a capped recent_ships[] — 3 per project: title, id, shipped_at, and a link into that instance — plus the instance-side reporter in core auth.js. Reuses a table and a reporter that already exist; costs no fan-out read; stays descriptive-analytics-only. Spec: ${SPEC} (page spec, section 4).`,
82
- },
83
- {
84
- ref: 'P08', kind: 'feature', disc: 'ui', prio: 4, est: 150, crit: ['C1'], deps: ['P01', R05],
85
- title: 'P08 · Canonical profile URL wiring (instance rel=canonical → hub; numeric id redirects)',
86
- desc: `One canonical public profile, two render paths: the hub's server-rendered handle URL is canonical, and the hall's own profile page stays the LOCAL view and emits rel=canonical to it when the builder has a platform account. The numeric ?id= form never dies — it is stable and rename-proof, so keep it as a permanent fallback that redirects to the canonical handle URL. Coordinate with goal 1000048 R14 (public builder-profile SEO page) so only ONE page gets built. Spec: ${SPEC} (Decision 1).`,
87
- },
88
- {
89
- ref: 'P09', kind: 'feature', disc: 'engineer', prio: 3, est: 150, crit: ['C1'], deps: ['P01', PRIVACY_SPIKE],
90
- title: 'P09 · noindex until first ship + sitemap consent gate for builder profiles',
91
- desc: `A public, indexable profile that anyone can mint is a backlink-farming surface. Gate indexing on the builder having at least one shipped work, and gate sitemap inclusion on consent (private accounts and — if the owner accepts P03 — provisional accounts are excluded). Coordinates with goal 1000048 R15 (builder profiles in sitemap, consent-gated). Shape depends on the P03 decision. Spec: ${SPEC} (Visibility rules).`,
92
- },
93
- {
94
- ref: 'P10', kind: 'verify', disc: 'engineer', prio: 3, est: 90, crit: ['C2'], deps: [PRIVACY_SPIKE, R03],
95
- title: 'P10 · PROOF: a private account\'s profile 404s to a stranger and renders to its owner',
96
- desc: `Verify the profile page honours the privacy model: a private account's handle URL returns 404 — NOT 403, because a 403 confirms the handle exists — to everyone except the account itself, which sees its own profile with a "private" banner. Also assert the stats-hidden case renders no partial numbers (a count beside a hidden total is a leak by subtraction). The authoritative model is task ${PRIVACY_SPIKE}'s; if it lands a different rule, this proof follows it and the spec is the thing that is wrong. Spec: ${SPEC} (Visibility rules).`,
97
- },
98
- ];
99
-
100
- (async () => {
101
- const idByRef = {};
102
- let created = 0;
103
- let existing = 0;
104
- console.log(`Seeding ${TASKS.length} tasks under goal ${GOAL} (${VER})…`);
105
- for (const t of TASKS) {
106
- const r = await api('POST', '/tasks', {
107
- version_id: VER,
108
- goal_id: GOAL,
109
- title: t.title,
110
- description: t.desc,
111
- kind: t.kind,
112
- discipline: t.disc,
113
- priority: t.prio,
114
- est_minutes: t.est,
115
- criterion_ids: t.crit,
116
- source: SRC,
117
- source_ref: `bv1-profile-page-${t.ref}`,
118
- });
119
- if (!r.ok) {
120
- console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`);
121
- continue;
122
- }
123
- const task = r.data.task || r.data;
124
- idByRef[t.ref] = task.id;
125
- if (r.data.idempotent || r.data.existing) existing++; else created++;
126
- console.log(` ${t.ref} → task ${task.id} (${t.kind}/${t.disc}, P${t.prio}, [${t.crit.join(',')}])`);
127
- }
128
-
129
- console.log('Wiring dependencies…');
130
- let edges = 0;
131
- for (const t of TASKS) {
132
- if (!t.deps.length) continue;
133
- const id = idByRef[t.ref];
134
- // A dep is either a local ref (resolved from this run) or a literal task id.
135
- const depIds = t.deps.map((d) => (typeof d === 'number' ? d : idByRef[d])).filter(Boolean);
136
- if (!id || depIds.length !== t.deps.length) {
137
- console.log(` ${t.ref}: dep resolve gap, skipping`);
138
- continue;
139
- }
140
- // PUT replaces the full set, so a re-run converges instead of erroring on
141
- // an edge that already exists.
142
- const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
143
- if (!r.ok) {
144
- console.log(` ${t.ref}: dep FAILED ${r.status} ${JSON.stringify(r.data)}`);
145
- continue;
146
- }
147
- edges += depIds.length;
148
- console.log(` ${t.ref} depends on ${t.deps.join(',')}`);
149
- }
150
-
151
- console.log('Promoting zero-dep roots to ready…');
152
- for (const t of TASKS.filter((x) => x.ready)) {
153
- const id = idByRef[t.ref];
154
- if (!id) continue;
155
- const r = await api('POST', `/tasks/${id}/promote`, {});
156
- console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
157
- }
158
-
159
- console.log(`\nDONE. created=${created} existing=${existing} depEdges=${edges}.`);
160
- console.log('ref→id:', JSON.stringify(idByRef));
161
- })();