@bongos/core 1.24.13 → 1.24.15
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.
- package/.bongos-core.json +302 -137
- package/.claude/skills/blocker-review/SKILL.md +2 -0
- package/.claude/skills/session-board/SKILL.md +29 -0
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +14 -0
- package/clients/bongos-client/index.cjs +14 -0
- package/clients/bongos-client/index.d.ts +23 -2
- package/clients/bongos-client/index.mjs +14 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +4 -0
- package/docs/adr/0397-the-payment-rail-is-stripe-connect-with-one-connected-account-per-project.md +71 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +369 -6
- package/docs/api-reference.md +19 -4
- package/docs/architecture.md +6 -0
- package/docs/copy-inventory.md +138 -123
- package/docs/copy-registry.json +296 -171
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +6 -1
- package/docs/page-readings.json +1040 -1021
- package/migrations/core_289_task_short_name.sql +34 -0
- package/modules/ai-studio/db.js +10 -0
- package/modules/ai-studio/gemini.js +73 -5
- package/modules/ai-studio/openrouter.js +17 -2
- package/modules/ai-studio/routes/ai-studio.js +7 -1
- package/modules/ai-studio/studio.js +8 -0
- package/modules/builder-settings/checkout-freshness.js +71 -0
- package/modules/builder-settings/routes/settings.js +26 -0
- package/modules/builder-settings/settings.js +2 -0
- package/modules/economy/migrations/economy_002_project_account.sql +83 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/project-account.js +166 -0
- package/modules/economy/routes/project-account.js +40 -0
- package/modules/hall-ui/public/ai-studio.css +3 -0
- package/modules/hall-ui/public/ai-studio.html +2 -1
- package/modules/hall-ui/public/ai-studio.js +72 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/github-chip.js +85 -0
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-item.js +10 -3
- package/modules/hall-ui/public/market-watch-item.states.json +41 -0
- package/modules/hall-ui/public/market-watch-lib.js +36 -5
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +1 -1
- package/modules/hall-ui/public/market-watch.css +5 -0
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.js +3 -0
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +22 -22
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/blockers.js +13 -7
- package/modules/lifecycle/db-claim-reads.js +1 -1
- package/modules/lifecycle/db-ship.js +2 -2
- package/modules/lifecycle/db-tasks.js +4 -4
- package/modules/lifecycle/migrations/lifecycle_019_pay_type_gates.sql +50 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/pay-type-gates.js +180 -0
- package/modules/lifecycle/publish-reconciler.js +31 -1
- package/modules/lifecycle/rebase-gate.js +3 -2
- package/modules/lifecycle/routes/claims.js +3 -2
- package/modules/lifecycle/routes/pay-type-gates.js +59 -0
- package/modules/lifecycle/routes/project-settings.js +4 -0
- package/modules/lifecycle/routes/task-write-routes.js +43 -0
- package/modules/lifecycle/ship-card.js +4 -1
- package/modules/lifecycle/task-name-db.js +55 -0
- package/modules/lifecycle/task-name.js +140 -0
- package/modules/market-watch/db.js +10 -1
- package/modules/market-watch/migrations/market_watch_004_entry_corrections.sql +20 -0
- package/modules/market-watch/pages.js +20 -1
- package/modules/market-watch/proposals.js +57 -7
- package/modules/market-watch/routes/pages.js +2 -0
- package/modules/market-watch/routes/rules.js +4 -0
- package/modules/platform-identity/terms-acceptance.js +9 -9
- package/modules/public-landing/public/account-google.states.json +14 -0
- package/modules/public-landing/public/assets/cosmos.css +12 -0
- package/modules/public-landing/public/assets/hub-bar.js +44 -1
- package/modules/public-landing/public/privacy.html +5 -1
- package/modules/public-landing/public/projects-google.states.json +14 -0
- package/modules/public-landing/public/projects.html +39 -0
- package/modules/public-landing/public/terms.html +12 -2
- package/modules/ui-design/kit/fixtures/me-google.json +10 -0
- package/modules/ui-design/kit/fixtures/me-sign-in-methods-google.json +4 -0
- package/modules/ui-design/kit/serve.js +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +54 -0
- package/scripts/bongos/claim.js +4 -2
- package/scripts/bongos/land-watch.js +1 -0
- package/scripts/bongos/report-checkout.js +30 -0
- package/scripts/bongos/run-unit-tests.js +11 -0
- package/scripts/bongos/sessions-board.js +264 -0
- package/scripts/bongos/ship-finish.js +1 -0
- package/scripts/bongos/ship-flow.js +5 -2
- package/scripts/bongos/strand-lease.js +5 -3
- package/scripts/bongos/strand-watch.js +38 -7
- package/scripts/gds/report-checkout.js +7 -0
- package/scripts/gds/sessions-board.js +7 -0
- package/src/bongos/agent-standing-rules.js +13 -3
- package/src/bongos/routes/me.js +7 -1
- package/src/module-api.js +1 -1
- package/tests/agent_standing_rules.mjs +18 -1
- package/tests/ai_studio_billing.mjs +13 -0
- package/tests/ai_studio_gemini_models.mjs +81 -0
- package/tests/ai_studio_requests.mjs +24 -0
- package/tests/blocker_detail_hydration.mjs +5 -3
- package/tests/blocker_tasks_named_db.mjs +74 -0
- package/tests/claim_gate_ci_unblock.mjs +1 -1
- package/tests/github_chip.mjs +217 -0
- package/tests/hall_ai_studio.mjs +129 -5
- package/tests/host_topology_skips.mjs +1 -0
- package/tests/hub_account_page.mjs +3 -2
- package/tests/landing_legal_pages.mjs +3 -2
- package/tests/market_watch_data_db.mjs +6 -0
- package/tests/market_watch_pages.mjs +87 -2
- package/tests/market_watch_rules.mjs +101 -5
- package/tests/market_watch_rules_db.mjs +42 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/pay_terms.mjs +8 -0
- package/tests/pay_type_gates.mjs +234 -0
- package/tests/pay_type_gates_db.mjs +84 -0
- package/tests/project_account.mjs +125 -0
- package/tests/project_account_db.mjs +97 -0
- package/tests/self_healing_start.mjs +162 -0
- package/tests/sessions_board.mjs +153 -0
- package/tests/stop_refcheck.mjs +40 -0
- package/tests/task_name.mjs +237 -0
- package/tests/task_name_db.mjs +79 -0
- package/tests/terms_acceptance.mjs +25 -3
- package/tests/terms_grandfather_db.mjs +11 -4
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// modules/lifecycle/task-name.js — every task's short name: 2-3 plain words, so a
|
|
3
|
+
// person reads "task 1004745 - payout approvals" instead of a bare number (task 1004958).
|
|
4
|
+
//
|
|
5
|
+
// WHY. Sessions mention task numbers in passing ("task 1004745 is stranded", "after
|
|
6
|
+
// 1004927 lands") as if the builder could keep up with them. A number alone means
|
|
7
|
+
// nothing to the reader. The name is STORED (tasks.short_name, lifecycle_019), so it
|
|
8
|
+
// is the same on every surface; createTask fills it, the publish-reconciler backfills
|
|
9
|
+
// the rows created before it, and the task's author may change it (PATCH /tasks/:id).
|
|
10
|
+
//
|
|
11
|
+
// NOT A TRUNCATED TITLE. Titles open with words that describe nothing — "Goal 1000092
|
|
12
|
+
// N9:", "fix(auth):", "PMS-V2 —", "V4.R38 —" — so `deriveTaskName` strips those labels,
|
|
13
|
+
// keeps the title's head clause, drops filler words and keeps the first three that say
|
|
14
|
+
// what the work is about.
|
|
15
|
+
//
|
|
16
|
+
// A SAFE STRING BY CONSTRUCTION. The startup notes inject text into every session and
|
|
17
|
+
// print no builder-authored text raw (scripts/bongos/strand-watch.js). A name is at most
|
|
18
|
+
// three lowercase words of [a-z0-9-] and 40 characters; `safeTaskName` re-checks that on
|
|
19
|
+
// every read and the table CHECK holds it on every write, so a name can carry a topic
|
|
20
|
+
// and nothing else. An author's edit goes through the same normaliser.
|
|
21
|
+
//
|
|
22
|
+
// Requires nothing, so a CLI can print a name without loading the pool.
|
|
23
|
+
|
|
24
|
+
const MAX_WORDS = 3;
|
|
25
|
+
const MAX_LEN = 40;
|
|
26
|
+
const MAX_WORD_LEN = 20;
|
|
27
|
+
// The same pattern, character for character, as core_289's CHECK (tests/task_name.mjs).
|
|
28
|
+
const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*( [a-z0-9]+(-[a-z0-9]+)*){0,2}$/;
|
|
29
|
+
|
|
30
|
+
// Words that carry no topic: articles, prepositions, auxiliaries, quantifiers.
|
|
31
|
+
const STOP = new Set((
|
|
32
|
+
'a an the and or but nor of to in on at by for from with without into onto over under about as '
|
|
33
|
+
+ 'is are was were be been being am do does did done has have had having can could will would '
|
|
34
|
+
+ 'shall should may might must need needs needed get gets got let lets make makes it its this '
|
|
35
|
+
+ 'that these those there their them they we our us you your i me my he she his her so if then '
|
|
36
|
+
+ 'than when what which who whom whose why how where while not no yes all any each every some '
|
|
37
|
+
+ 'more most less only just also too very via per vs etc one two n new now still again once '
|
|
38
|
+
// …and labels that say how a task was filed, not what it is about.
|
|
39
|
+
+ 'phase part step urgent hotfix wip tracking bundle proof spike'
|
|
40
|
+
).split(' '));
|
|
41
|
+
|
|
42
|
+
// Labels a title opens with that describe nothing. Applied repeatedly from the start.
|
|
43
|
+
const LABELS = [
|
|
44
|
+
/^goal\s+\d+\s*(?:[a-z]{1,4}\d+[a-z]?\s*)?[:—–-]?\s*/i, // "Goal 1000092 N9:"
|
|
45
|
+
/^(?:fix|feat|chore|docs|diag|refactor|test|tests|perf|ci|build|style|revert)(?:\([^)]*\))?!?\s*:\s*/i, // "fix(auth):"
|
|
46
|
+
// Every repeat is bounded (no nested unbounded quantifier), so a hostile title
|
|
47
|
+
// cannot make this backtrack super-linearly.
|
|
48
|
+
/^[A-Z][A-Za-z]{0,5}\d{0,4}(?:[.-][A-Za-z]{0,6}\d{1,4}[a-z]?){1,3}\s{0,3}[:—–-]\s*/, // "V4.R38 —", "PMS-V2 —", "BV2.TW14:"
|
|
49
|
+
/^[A-Z]{2,6}\s*[:—–]\s*/, // "SEC:", "PMS —"
|
|
50
|
+
/^(?:task|idea|blocker)\s+#?\d+\s*[:—–-]?\s*/i, // "task 123:"
|
|
51
|
+
/^\[[^\]]{0,40}\]\s*/, // "[wip]"
|
|
52
|
+
/^#?\d+\s*[:—–-]\s*/, // "123 —"
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// The head clause ends at the first strong separator.
|
|
56
|
+
const HEAD_CUT = /\s[—–]\s|:\s|;\s|\s\(|\s\+\s|\s-\s|\s→\s|\.\s/;
|
|
57
|
+
|
|
58
|
+
// Only the opening of a title can hold a name; reading at most this much bounds the
|
|
59
|
+
// work per title whatever its length.
|
|
60
|
+
const MAX_TITLE_READ = 200;
|
|
61
|
+
|
|
62
|
+
function stripLabels(title) {
|
|
63
|
+
let s = String(title || '').slice(0, MAX_TITLE_READ).trim();
|
|
64
|
+
for (let guard = 0; guard < 6; guard++) {
|
|
65
|
+
const before = s;
|
|
66
|
+
for (const re of LABELS) s = s.replace(re, '');
|
|
67
|
+
s = s.trim();
|
|
68
|
+
if (s === before) break;
|
|
69
|
+
}
|
|
70
|
+
return s;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// One raw token to a name word, or null. "ship.js" → "ship", "gds_session" →
|
|
74
|
+
// "gds-session", "#898" / "2026-06-12" / "V2" → null.
|
|
75
|
+
function toWord(raw) {
|
|
76
|
+
let w = String(raw).normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
|
77
|
+
w = w.replace(/\.(?:js|mjs|cjs|ts|md|json|sql|sh|yml|yaml|html|css)$/, '');
|
|
78
|
+
w = w.replace(/_/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
79
|
+
if (!w || STOP.has(w)) return null;
|
|
80
|
+
if (/\d/.test(w)) return null; // ids, dates, version labels, counts
|
|
81
|
+
if (w.length > MAX_WORD_LEN) w = w.slice(0, MAX_WORD_LEN).replace(/-+$/, '');
|
|
82
|
+
return w.length >= 2 ? w : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wordsOf(text) {
|
|
86
|
+
return String(text).normalize('NFKD').replace(/[\u0300-\u036f]/g, '').split(/[\s/,]+/).map((t) => t.replace(/^[^\w]+|[^\w]+$/g, '')).map(toWord).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fit(words) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const w of words) {
|
|
92
|
+
if (out.includes(w)) continue;
|
|
93
|
+
const next = [...out, w].join(' ');
|
|
94
|
+
if (next.length > MAX_LEN) break;
|
|
95
|
+
out.push(w);
|
|
96
|
+
if (out.length === MAX_WORDS) break;
|
|
97
|
+
}
|
|
98
|
+
return out.length ? out.join(' ') : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nameFrom(text) {
|
|
102
|
+
const body = stripLabels(text);
|
|
103
|
+
const cut = body.search(HEAD_CUT);
|
|
104
|
+
const head = cut > 0 ? body.slice(0, cut) : body;
|
|
105
|
+
const headWords = wordsOf(head);
|
|
106
|
+
// A head clause of one word ("Proof: forced-drift …") is itself a label; read on.
|
|
107
|
+
return fit(headWords.length >= 2 ? headWords : wordsOf(body));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The name for a task: from its title, else (a title of only ids or labels) from the
|
|
111
|
+
// opening of its description. Null only when neither has a topic word at all.
|
|
112
|
+
function deriveTaskName(title, description) {
|
|
113
|
+
return nameFrom(title) || (description ? nameFrom(String(description).split(/\n\s*\n/)[0]) : null);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A stored or authored name, re-checked. Returns the name or null — never anything else.
|
|
117
|
+
function safeTaskName(v) {
|
|
118
|
+
if (typeof v !== 'string') return null;
|
|
119
|
+
return v.length <= MAX_LEN && NAME_RE.test(v) ? v : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// An author's edit, normalised to the same alphabet. Null when nothing usable is left.
|
|
123
|
+
function normaliseAuthoredName(input) {
|
|
124
|
+
if (typeof input !== 'string') return null;
|
|
125
|
+
const words = input.split(/\s+/).map((t) => {
|
|
126
|
+
const w = t.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
127
|
+
.replace(/_/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
128
|
+
return w.slice(0, MAX_WORD_LEN).replace(/-+$/, '');
|
|
129
|
+
}).filter(Boolean);
|
|
130
|
+
if (!words.length || words.length > MAX_WORDS) return null;
|
|
131
|
+
return safeTaskName(words.join(' '));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// How a task is written to a person: "task N - name", or "task N" when it has none yet.
|
|
135
|
+
function taskRef(id, name) {
|
|
136
|
+
const n = safeTaskName(name);
|
|
137
|
+
return n ? `task ${id} - ${n}` : `task ${id}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { deriveTaskName, safeTaskName, normaliseAuthoredName, taskRef, MAX_WORDS, MAX_LEN, NAME_RE };
|
|
@@ -163,9 +163,18 @@ async function getItem(pool, { project, id, knownRuns = null }) {
|
|
|
163
163
|
|
|
164
164
|
const CASE_FILE_SQL = `
|
|
165
165
|
SELECT e.id, e.run_id, e.type, e.found_on, e.takeaway, e.dimension, e.source_url,
|
|
166
|
-
e.source_label, e.confidence, e.pinned, e.created_at
|
|
166
|
+
e.source_label, e.confidence, e.pinned, e.created_at,
|
|
167
|
+
c.decided_at AS corrected_at, c.reason AS correction_reason, COALESCE(c.wrong, false) AS marked_wrong
|
|
167
168
|
FROM market_watch_log_entries e
|
|
168
169
|
JOIN market_watch_items i ON i.id = e.item_id
|
|
170
|
+
-- the entry's latest applied correction (task 1005016): the proposal row is where who
|
|
171
|
+
-- proposed it, who approved it, when, why and the text it replaced are kept
|
|
172
|
+
LEFT JOIN LATERAL (
|
|
173
|
+
SELECT p.decided_at, p.reason, p.payload->>'wrong' = 'true' AS wrong
|
|
174
|
+
FROM market_watch_proposals p
|
|
175
|
+
WHERE p.project = i.project AND p.log_entry_id = e.id AND p.kind = 'correct_entry' AND p.status = 'applied'
|
|
176
|
+
ORDER BY p.decided_at DESC, p.id DESC
|
|
177
|
+
LIMIT 1) c ON true
|
|
169
178
|
WHERE i.project = $1 AND e.item_id = $2
|
|
170
179
|
ORDER BY e.pinned DESC, e.found_on DESC, e.id DESC`;
|
|
171
180
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- market_watch_004_entry_corrections.sql - the lookup of an entry's applied correction
|
|
2
|
+
-- (task 1005016, goal 1000137).
|
|
3
|
+
--
|
|
4
|
+
-- An approved correct_entry proposal is applied to its log entry, and the proposal row
|
|
5
|
+
-- keeps who, when, why and the replaced text. Two reads look that correction up once per
|
|
6
|
+
-- entry: the item page's case file (modules/market-watch/db.js CASE_FILE_SQL, a LATERAL
|
|
7
|
+
-- join for the latest applied correction) and the home's weekly reports
|
|
8
|
+
-- (modules/market-watch/pages.js WEEK_ENTRIES_SQL, which skips an entry marked wrong).
|
|
9
|
+
-- Without this index each of those scans the project's proposals per entry. The partial
|
|
10
|
+
-- index holds exactly the rows they can match, in the order they read them.
|
|
11
|
+
--
|
|
12
|
+
-- Additive and idempotent, like 001 to 003.
|
|
13
|
+
|
|
14
|
+
BEGIN;
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS market_watch_proposals_entry_corrections
|
|
17
|
+
ON market_watch_proposals (log_entry_id, decided_at DESC, id DESC)
|
|
18
|
+
WHERE kind = 'correct_entry' AND status = 'applied';
|
|
19
|
+
|
|
20
|
+
COMMIT;
|
|
@@ -30,6 +30,7 @@ const STREAK_SLOTS = 9;
|
|
|
30
30
|
const REASON_MAX = 280;
|
|
31
31
|
const NAME_MAX = 120;
|
|
32
32
|
const URL_MAX = 500;
|
|
33
|
+
const TAKEAWAY_MAX = 500;
|
|
33
34
|
const DIM_RE = /^[a-z][a-z0-9_]{0,59}$/;
|
|
34
35
|
// The four the item page's "Propose a change" offers, then the two the other buttons file.
|
|
35
36
|
const ITEM_KINDS = Object.freeze(['rescore', 'move_kind', 'add_source', 'correct_entry', 'remove_item']);
|
|
@@ -59,6 +60,11 @@ const WEEK_ENTRIES_SQL = `
|
|
|
59
60
|
FROM market_watch_log_entries e
|
|
60
61
|
JOIN market_watch_items i ON i.id = e.item_id
|
|
61
62
|
WHERE i.project = $1 AND i.status <> 'removed' AND e.found_on >= $2::date
|
|
63
|
+
-- an entry whose latest applied correction marked it wrong (task 1005016) is no report
|
|
64
|
+
AND NOT COALESCE((SELECT p.payload->>'wrong' = 'true' FROM market_watch_proposals p
|
|
65
|
+
WHERE p.project = i.project AND p.log_entry_id = e.id AND p.kind = 'correct_entry'
|
|
66
|
+
AND p.status = 'applied'
|
|
67
|
+
ORDER BY p.decided_at DESC, p.id DESC LIMIT 1), false)
|
|
62
68
|
ORDER BY e.found_on DESC, e.id DESC
|
|
63
69
|
LIMIT 1000`;
|
|
64
70
|
|
|
@@ -208,6 +214,9 @@ function shapeItem({ item, entries, history, rules, openProposals = 0 }) {
|
|
|
208
214
|
id: Number(e.id), run: e.run, type: e.type, found_on: isoDay(e.found_on), takeaway: e.takeaway,
|
|
209
215
|
dimension: e.dimension, source_url: e.source_url, source_label: e.source_label,
|
|
210
216
|
confidence: Number(e.confidence), pinned: !!e.pinned,
|
|
217
|
+
// An applied correction (task 1005016): when it was made and why. The takeaway
|
|
218
|
+
// above already reads as corrected; a wrong entry keeps its text, flagged.
|
|
219
|
+
corrected: e.corrected_at ? { on: isoDay(e.corrected_at), reason: e.correction_reason || '', wrong: !!e.marked_wrong } : null,
|
|
211
220
|
})),
|
|
212
221
|
open_proposals: Number(openProposals) || 0,
|
|
213
222
|
};
|
|
@@ -287,9 +296,19 @@ function normalizeProposal(body, { item = null, rubricKeys = null } = {}) {
|
|
|
287
296
|
return { ok: true, value: { ...base, payload: { source_url: url } } };
|
|
288
297
|
}
|
|
289
298
|
if (kind === 'correct_entry') {
|
|
299
|
+
// The proposal carries the correction itself (task 1005016): either the takeaway as
|
|
300
|
+
// it should read, or a flag that the entry is wrong. The reason says why. Approving
|
|
301
|
+
// it on the rules page applies it to the entry (proposals.js).
|
|
290
302
|
const entry = String(b.log_entry_id || '');
|
|
291
303
|
if (!/^[1-9]\d{0,17}$/.test(entry)) return { ok: false, error: 'invalid_entry', message: 'Pick the entry to correct.' };
|
|
292
|
-
|
|
304
|
+
const wrong = b.wrong === true || b.wrong === 'true';
|
|
305
|
+
const takeaway = oneLine(b.takeaway);
|
|
306
|
+
if (wrong && takeaway) return { ok: false, error: 'invalid_correction', message: 'Either correct the entry or mark it as wrong, not both.' };
|
|
307
|
+
if (wrong) return { ok: true, value: { ...base, log_entry_id: Number(entry), payload: { wrong: true, summary: 'Mark an entry as wrong' } } };
|
|
308
|
+
if (!takeaway) return { ok: false, error: 'correction_required', message: 'Write the entry as it should read, or mark it as wrong.' };
|
|
309
|
+
if (takeaway.length > TAKEAWAY_MAX) return { ok: false, error: 'correction_too_long', message: `Keep the correction to ${TAKEAWAY_MAX} characters.` };
|
|
310
|
+
const short = takeaway.length > 90 ? `${takeaway.slice(0, 89)}…` : takeaway;
|
|
311
|
+
return { ok: true, value: { ...base, log_entry_id: Number(entry), payload: { takeaway, summary: `Correct an entry to read "${short}"` } } };
|
|
293
312
|
}
|
|
294
313
|
return { ok: true, value: { ...base, payload: {} } }; // remove_item: stop tracking
|
|
295
314
|
}
|
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
// name and link are already tracked as the 'to' kind.
|
|
24
24
|
// add_source payload { source_url }: the link joins the item's facts.watch_sources
|
|
25
25
|
// (shown on the item page; the weekly run does not read it yet).
|
|
26
|
-
// correct_entry payload {}
|
|
27
|
-
//
|
|
26
|
+
// correct_entry payload { takeaway } or { wrong: true } with log_entry_id (task 1005016):
|
|
27
|
+
// the entry's takeaway is rewritten to the corrected one, or the entry is
|
|
28
|
+
// left as found and read as marked wrong (db.caseFile; the home's reports
|
|
29
|
+
// skip it). The text it replaced is kept on the proposal (payload.was),
|
|
30
|
+
// beside who proposed it, who approved it, when, and the reason why. A
|
|
31
|
+
// correct_entry filed before then carries no correction and is refused.
|
|
28
32
|
// The four item-page payloads are written by modules/market-watch/pages.js (task 1004939).
|
|
29
33
|
// A carried-out approval is status 'applied'; a recorded one is 'approved'. A decline is
|
|
30
34
|
// 'rejected'. Every decision names who made it and when (decided_by, decided_at).
|
|
@@ -58,7 +62,7 @@ const SQL = Object.freeze({
|
|
|
58
62
|
VALUES ($1, $2, 'member', $3, $4, $5::jsonb, $6, $7)
|
|
59
63
|
RETURNING id, kind, status, created_at`,
|
|
60
64
|
lock: `-- mw:proposal-lock
|
|
61
|
-
SELECT id, kind, origin, item_id, rule_version_id, payload, reason, proposed_by, status
|
|
65
|
+
SELECT id, kind, origin, item_id, log_entry_id, rule_version_id, payload, reason, proposed_by, status
|
|
62
66
|
FROM market_watch_proposals WHERE id = $1 AND project = $2 FOR UPDATE`,
|
|
63
67
|
maxVersion: `-- mw:rules-max-version
|
|
64
68
|
SELECT COALESCE(max(version), 0)::int AS v FROM market_watch_rule_versions WHERE project = $1`,
|
|
@@ -117,6 +121,22 @@ const SQL = Object.freeze({
|
|
|
117
121
|
updated_at = now()
|
|
118
122
|
WHERE id = $1 AND project = $2 AND status <> 'removed'
|
|
119
123
|
RETURNING id`,
|
|
124
|
+
// The entry a correction names, locked, scoped to the proposal's item and this project.
|
|
125
|
+
entryLock: `-- mw:entry-lock
|
|
126
|
+
SELECT e.id, e.item_id, e.run_id, e.takeaway
|
|
127
|
+
FROM market_watch_log_entries e
|
|
128
|
+
JOIN market_watch_items i ON i.id = e.item_id
|
|
129
|
+
WHERE e.id = $1 AND e.item_id = $3 AND i.project = $2
|
|
130
|
+
FOR UPDATE OF e`,
|
|
131
|
+
// The corrected takeaway, unless the same item already holds that takeaway from the
|
|
132
|
+
// same run (the unique index market_watch_log_entries_once would refuse it).
|
|
133
|
+
entryCorrect: `-- mw:entry-correct
|
|
134
|
+
UPDATE market_watch_log_entries e SET takeaway = $2::text
|
|
135
|
+
WHERE e.id = $1
|
|
136
|
+
AND NOT EXISTS (SELECT 1 FROM market_watch_log_entries o
|
|
137
|
+
WHERE o.item_id = e.item_id AND o.run_id = e.run_id AND o.id <> e.id
|
|
138
|
+
AND md5(o.takeaway) = md5($2::text))
|
|
139
|
+
RETURNING e.id`,
|
|
120
140
|
atVote: `-- mw:proposal-at-vote
|
|
121
141
|
UPDATE market_watch_proposals SET payload = payload || jsonb_build_object('matter_id', $3::text)
|
|
122
142
|
WHERE id = $1 AND project = $2 AND status = 'open'`,
|
|
@@ -206,9 +226,20 @@ function itemChange(kind, payload) {
|
|
|
206
226
|
return null;
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
// entryCorrection(payload) → { takeaway } | { wrong: true, takeaway: null } | null — what a
|
|
230
|
+
// correct_entry carries (pages.normalizeProposal writes it), re-checked here. A proposal
|
|
231
|
+
// filed before task 1005016 carries neither, so it cannot be applied (no_correction).
|
|
232
|
+
function entryCorrection(payload) {
|
|
233
|
+
const p = payload || {};
|
|
234
|
+
if (p.wrong === true) return { wrong: true, takeaway: null };
|
|
235
|
+
const t = String(p.takeaway == null ? '' : p.takeaway).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
236
|
+
return t && t.length <= 500 ? { takeaway: t } : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
209
239
|
// decideProposal(pool, { project, id, decision, decidedBy, via, withTx }) →
|
|
210
240
|
// { ok: true, status, applied, version?, item_id? } |
|
|
211
|
-
// { ok: false, reason: 'not_found' | 'not_open' | 'no_active_rules' | 'no_longer_applies' | 'bad_payload' | 'item_gone'
|
|
241
|
+
// { ok: false, reason: 'not_found' | 'not_open' | 'no_active_rules' | 'no_longer_applies' | 'bad_payload' | 'item_gone'
|
|
242
|
+
// | 'no_correction' | 'entry_gone' | 'same_as_another_entry' }
|
|
212
243
|
// `via` is recorded on the proposal's payload when a government vote decided it
|
|
213
244
|
// ({ decided_via: 'vote', matter_id }); an owner's own decision records nothing extra.
|
|
214
245
|
// `withTx(fn)` runs fn(client) in one transaction (the doorway's withTx in production).
|
|
@@ -219,8 +250,8 @@ async function decideProposal({ project, id, decision, decidedBy, via = null, wi
|
|
|
219
250
|
const { rows: [p] } = await client.query(SQL.lock, [id, project]);
|
|
220
251
|
if (!p) return { ok: false, reason: 'not_found' };
|
|
221
252
|
if (p.status !== 'open') return { ok: false, reason: 'not_open', status: p.status };
|
|
222
|
-
const
|
|
223
|
-
|
|
253
|
+
const decide = async (status, { versionId = null, itemId = null, kept = null } = {}) => {
|
|
254
|
+
const extra = JSON.stringify({ ...(kept || {}), ...(via || {}) });
|
|
224
255
|
const { rows: [d] } = await client.query(SQL.decide, [id, project, status, decidedBy, versionId, itemId, extra]);
|
|
225
256
|
return d;
|
|
226
257
|
};
|
|
@@ -268,9 +299,28 @@ async function decideProposal({ project, id, decision, decidedBy, via = null, wi
|
|
|
268
299
|
await decide('applied');
|
|
269
300
|
return { ok: true, status: 'applied', applied: true, item_id: Number(p.item_id) };
|
|
270
301
|
}
|
|
302
|
+
if (p.kind === 'correct_entry') {
|
|
303
|
+
const c = entryCorrection(p.payload);
|
|
304
|
+
if (!c || p.log_entry_id == null || p.item_id == null) return { ok: false, reason: 'no_correction' };
|
|
305
|
+
const { rows: [e] } = await client.query(SQL.entryLock, [p.log_entry_id, project, p.item_id]);
|
|
306
|
+
if (!e) return { ok: false, reason: 'entry_gone' };
|
|
307
|
+
// A correction that reads the same as the entry already does (compared after the
|
|
308
|
+
// same one-line normalising entryCorrection applies) writes nothing to the entry;
|
|
309
|
+
// the proposal is still 'applied', since the entry now reads as it asked.
|
|
310
|
+
const now = String(e.takeaway).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
311
|
+
if (c.takeaway != null && c.takeaway !== now) {
|
|
312
|
+
const { rows: [done] } = await client.query(SQL.entryCorrect, [e.id, c.takeaway]);
|
|
313
|
+
if (!done) return { ok: false, reason: 'same_as_another_entry' };
|
|
314
|
+
}
|
|
315
|
+
// Who proposed it (proposed_by), who approved it and when (decided_by, decided_at)
|
|
316
|
+
// and why (reason) are the proposal row's own columns; the text it replaced is kept
|
|
317
|
+
// on the row too, so the original finding is never lost.
|
|
318
|
+
await decide('applied', { kept: { was: e.takeaway } });
|
|
319
|
+
return { ok: true, status: 'applied', applied: true, item_id: Number(p.item_id), log_entry_id: Number(e.id) };
|
|
320
|
+
}
|
|
271
321
|
await decide('approved');
|
|
272
322
|
return { ok: true, status: 'approved', applied: false };
|
|
273
323
|
});
|
|
274
324
|
}
|
|
275
325
|
|
|
276
|
-
module.exports = { createProposal, decideProposal, getOpen, markAtVote, itemFromPayload, itemChange, MAX_OPEN_PER_MEMBER };
|
|
326
|
+
module.exports = { createProposal, decideProposal, getOpen, markAtVote, itemFromPayload, itemChange, entryCorrection, MAX_OPEN_PER_MEMBER };
|
|
@@ -87,6 +87,8 @@ module.exports = function buildMarketWatchPagesRouter(deps = {}) {
|
|
|
87
87
|
value: {},
|
|
88
88
|
url: { type: 'string', maxLength: 2000 },
|
|
89
89
|
log_entry_id: {},
|
|
90
|
+
takeaway: { type: 'string', maxLength: 2000 }, // correct_entry: the entry as it should read
|
|
91
|
+
wrong: {}, // correct_entry: or the entry is wrong
|
|
90
92
|
scout: { type: 'string', enum: pages.KINDS },
|
|
91
93
|
name: { type: 'string', maxLength: 2000 },
|
|
92
94
|
})) return;
|
|
@@ -87,6 +87,10 @@ const DECIDE_REFUSALS = Object.freeze({
|
|
|
87
87
|
no_longer_applies: [409, 'The rules changed since this was proposed, so it no longer fits. Decline it and propose again.'],
|
|
88
88
|
bad_payload: [409, 'This proposal does not name anything to add.'],
|
|
89
89
|
item_gone: [409, 'That item is no longer on this project.'],
|
|
90
|
+
// a correct_entry (task 1005016)
|
|
91
|
+
no_correction: [409, 'This proposal does not say how the entry should read. Decline it and propose the correction again.'],
|
|
92
|
+
entry_gone: [409, 'That entry is no longer in this item\'s case file.'],
|
|
93
|
+
same_as_another_entry: [409, 'The item already has an entry from that run that reads the same. Decline this one.'],
|
|
90
94
|
});
|
|
91
95
|
|
|
92
96
|
module.exports = function buildMarketWatchRulesRouter() {
|
|
@@ -48,11 +48,10 @@
|
|
|
48
48
|
// ever READS the marker. tests/terms_acceptance.mjs holds both rules, and fails
|
|
49
49
|
// any other server file that writes these columns or names the marker.
|
|
50
50
|
//
|
|
51
|
-
// A
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// NOT built here (ADR 0213 D4): it is a product flow, not a storage shape.
|
|
51
|
+
// A REVISION IS RE-ACCEPTED AT THE NEXT BROWSER SIGN-IN (2026-10-30, the AI
|
|
52
|
+
// features revision; supersedes ADR 0213 D4). Until then a row keeps naming the
|
|
53
|
+
// text it actually agreed to, so the platform can see who has not accepted the
|
|
54
|
+
// new one. A sign-in under the SAME version still never restamps.
|
|
56
55
|
'use strict';
|
|
57
56
|
|
|
58
57
|
// The version of the platform terms currently served at /terms. It is stamped
|
|
@@ -65,7 +64,7 @@
|
|
|
65
64
|
// The terms_version column it fills is WRITE-ONLY today, on purpose: nothing
|
|
66
65
|
// reads it until a revision needs to know who accepted which text (ADR 0213 D5).
|
|
67
66
|
// It is not dead — dropping it would erase exactly that answer.
|
|
68
|
-
const TERMS_VERSION = '2026-
|
|
67
|
+
const TERMS_VERSION = '2026-10-30';
|
|
69
68
|
|
|
70
69
|
// A Postgres placeholder, e.g. '$5'. Interpolated into SQL, so it is validated
|
|
71
70
|
// rather than trusted — the account-visibility.js alias-guard pattern.
|
|
@@ -96,8 +95,9 @@ function acceptanceInsertValues(activateParam, versionParam) {
|
|
|
96
95
|
|
|
97
96
|
// The ON CONFLICT DO UPDATE SET fragment — the write-once half. On the
|
|
98
97
|
// activating sign-in: a grandfathered acceptance is replaced outright; any other
|
|
99
|
-
// acceptance
|
|
100
|
-
//
|
|
98
|
+
// acceptance of an older version is replaced too (the person signed in under
|
|
99
|
+
// the new text); any other acceptance on record is kept (COALESCE), so a
|
|
100
|
+
// second sign-in re-reads the terms without restamping them; and the source becomes 'signin' either way, since a
|
|
101
101
|
// human just accepted. Keying the replacement on the grandfather marker, not on
|
|
102
102
|
// "no signin recorded", matters: an acceptance with no source (written by the
|
|
103
103
|
// pre-024 code in the deploy window) is a real sign-in and keeps its date. A
|
|
@@ -111,7 +111,7 @@ function acceptanceConflictSet(activateParam, versionParam) {
|
|
|
111
111
|
assertPlaceholder('acceptanceConflictSet', versionParam);
|
|
112
112
|
const self = 'platform_identity_accounts';
|
|
113
113
|
const signin = `${activateParam}::boolean`;
|
|
114
|
-
const replaces = `WHEN ${signin} AND ${self}.terms_accepted_source = 'grandfathered'`;
|
|
114
|
+
const replaces = `WHEN ${signin} AND (${self}.terms_accepted_source = 'grandfathered' OR ${self}.terms_version IS DISTINCT FROM ${versionParam}::text)`;
|
|
115
115
|
return `terms_accepted_at = CASE ${replaces} THEN now()`
|
|
116
116
|
+ ` WHEN ${signin} THEN COALESCE(${self}.terms_accepted_at, now()) ELSE ${self}.terms_accepted_at END,`
|
|
117
117
|
+ ` terms_version = CASE ${replaces} THEN ${versionParam}::text`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The Link GitHub chip (task 1004996): a Google-only person, no GitHub way in, so the hub bar draws one very small chip with the engineer's icon at the head of its right side, linking to the GitHub link start; the ways-in section lists Google only with Link GitHub beside GitHub. stub.fixtures points the identity at fixtures/me-google.json (the stand-in login ~builder) and the ways-in read at fixtures/me-sign-in-methods-google.json. The default account.states.json (GitHub linked) draws no chip.",
|
|
3
|
+
"page": "/account",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"stub": { "fixtures": { "me": "me-google", "me__sign-in-methods": "me-sign-in-methods-google" } },
|
|
6
|
+
"modes": ["dark"],
|
|
7
|
+
"modeQuery": false,
|
|
8
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/cross-project$"],
|
|
9
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized"],
|
|
10
|
+
"ignoreControls": [".vh"],
|
|
11
|
+
"states": {
|
|
12
|
+
"in-google": { "auth": true, "url": "/account#aWaysIn", "actions": [["wait", 500]], "expect": { "visible": ["#ghChip", "#who", "#waysList"], "attr": [["#ghChip", "href", "/api/bongos/auth/link/github/start"], ["#ghChip", "aria-label", "Link GitHub — needed for coding work"]] } }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -647,6 +647,16 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
647
647
|
border-radius:var(--radius-pill);background:var(--accent);color:var(--accent-on);
|
|
648
648
|
font-family:var(--font-body);font-size:12px;font-weight:700;line-height:1;letter-spacing:0;vertical-align:middle;}
|
|
649
649
|
.who>.invBadge{max-width:none;overflow:visible;}
|
|
650
|
+
/* the Link GitHub chip (task 1004996; hub-bar.js, projects.html): very small and quiet, for
|
|
651
|
+
someone with no GitHub linked — a hairline pill with the engineer's glyph, never a banner;
|
|
652
|
+
under 900 the words fold away and the glyph keeps a 24px target */
|
|
653
|
+
.ghChip{display:inline-flex;align-items:center;justify-content:center;gap:5px;min-height:24px;min-width:24px;padding:0 9px 0 7px;
|
|
654
|
+
flex:none;border:1px solid var(--rule);border-radius:var(--radius-pill);color:var(--ink-soft);
|
|
655
|
+
font-family:var(--label-face);font-size:11.5px;line-height:1;white-space:nowrap;text-decoration:none;
|
|
656
|
+
transition:color .2s cubic-bezier(.2,.7,.3,1),border-color .2s cubic-bezier(.2,.7,.3,1);}
|
|
657
|
+
.ghChip:hover{color:var(--ink);border-color:var(--ink-faint);}
|
|
658
|
+
.ghChip:focus-visible{outline:2px solid var(--focus);outline-offset:2px;}
|
|
659
|
+
.ghChip svg{width:13px;height:13px;flex:none;fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round;}
|
|
650
660
|
.bandT .invBadge{margin-left:10px;}
|
|
651
661
|
.invWaiting{margin:6px 0 4px;font-size:15.5px;font-weight:600;}
|
|
652
662
|
/* keyboard focus on the two search pills is VISIBLE — the input's own outline
|
|
@@ -728,6 +738,8 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
728
738
|
.qWrap{flex:1 1 90px;max-width:480px;gap:10px;}
|
|
729
739
|
.trig{font-size:13.5px;}
|
|
730
740
|
.who>span{display:none;}
|
|
741
|
+
.ghChip{padding:0;}
|
|
742
|
+
.ghChip__label{display:none;}
|
|
731
743
|
.signinDoor{padding:0 16px;font-size:13.5px;}
|
|
732
744
|
.bandIn{padding:0 12px 16px;}
|
|
733
745
|
.bandRow{grid-template-columns:1fr;gap:0;}
|
|
@@ -116,14 +116,57 @@
|
|
|
116
116
|
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
// ---- the Link GitHub chip (task 1004996): signed in with no GitHub linked ----
|
|
120
|
+
// Very small and quiet: one link at the head of the bar's right side, carrying
|
|
121
|
+
// the engineer's icon (the hall's craft-mode.js ICONS.building), never a pop-up
|
|
122
|
+
// or banner. It starts the link (ADR 0396), whose answer lands on /account. A
|
|
123
|
+
// read that fails, or a child with no sign-in-methods route, draws nothing.
|
|
124
|
+
// Only a stand-in login (~<handle>, the Google-only row) is asked about:
|
|
125
|
+
// linking GitHub writes the real login over it, so anyone else has GitHub
|
|
126
|
+
// and costs no read. projects.html keeps its own copy, as it does of the bands.
|
|
127
|
+
var GH_TITLE = 'Link GitHub — needed for coding work';
|
|
128
|
+
function githubChip(me) {
|
|
129
|
+
var old = $('ghChip');
|
|
130
|
+
if (!me || String(me.github_login || '').charAt(0) !== '~') { if (old) old.remove(); return null; }
|
|
131
|
+
return fetch(API + '/me/sign-in-methods', { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
132
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
133
|
+
.then(function (d) {
|
|
134
|
+
var methods = d && Array.isArray(d.methods) ? d.methods : null;
|
|
135
|
+
var show = !!methods && !methods.some(function (m) { return m && m.provider === 'github'; });
|
|
136
|
+
var bar = show && document.querySelector('.barRight');
|
|
137
|
+
if (!bar) { if ($('ghChip')) $('ghChip').remove(); return false; }
|
|
138
|
+
if ($('ghChip')) return true;
|
|
139
|
+
var a = document.createElement('a');
|
|
140
|
+
a.className = 'ghChip';
|
|
141
|
+
a.id = 'ghChip';
|
|
142
|
+
a.href = API + '/auth/link/github/start';
|
|
143
|
+
a.title = GH_TITLE;
|
|
144
|
+
a.setAttribute('aria-label', GH_TITLE);
|
|
145
|
+
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
146
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
147
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
148
|
+
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
149
|
+
path.setAttribute('d', 'M2 13.5h12M3.5 13.5V8.5h4v5M7.5 13.5V4.5h5v9');
|
|
150
|
+
svg.appendChild(path);
|
|
151
|
+
var label = document.createElement('span');
|
|
152
|
+
label.className = 'ghChip__label';
|
|
153
|
+
label.textContent = 'Link GitHub';
|
|
154
|
+
a.appendChild(svg);
|
|
155
|
+
a.appendChild(label);
|
|
156
|
+
bar.insertBefore(a, bar.firstChild);
|
|
157
|
+
return true;
|
|
158
|
+
}, function () { return false; });
|
|
159
|
+
}
|
|
160
|
+
|
|
119
161
|
// ---- the identity read: the hub's own GET /me ----
|
|
120
162
|
var ready = fetch(API + '/me', { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
121
163
|
.then(function (res) { return res.ok ? res.json().catch(function () { return null; }) : null; })
|
|
122
164
|
.then(function (d) {
|
|
123
165
|
var me = d ? (d.builder || d.me || d) : null;
|
|
124
166
|
paint(me);
|
|
167
|
+
githubChip(me);
|
|
125
168
|
return me;
|
|
126
169
|
}, function () { paint(null); return null; });
|
|
127
170
|
|
|
128
|
-
window.HubBar = { paint: paint, ready: ready };
|
|
171
|
+
window.HubBar = { paint: paint, ready: ready, githubChip: githubChip };
|
|
129
172
|
})();
|
|
@@ -124,10 +124,14 @@
|
|
|
124
124
|
<ul>
|
|
125
125
|
<li><b>GitHub</b> — for signing in, and for the repositories your projects ship to.</li>
|
|
126
126
|
<li><b>The AI model provider</b> — when a shipped task is graded, the change it made is sent to the model the grader runs on, Anthropic's Claude, under the key of the builder or the instance.</li>
|
|
127
|
+
<li><b>AI features, through OpenRouter</b> — when you use AI Studio, your prompt, the files you attach, and short notes about the project are sent through OpenRouter to the AI company whose model answers (for example Google's Gemini). We set OpenRouter to use only companies that do not keep your prompts or train on them. Your requests, files and results are stored here so you can see them again, with what each one cost.</li>
|
|
127
128
|
<!--apex-only:start--><li><b>Hosting</b> — projects hosted by the platform run on servers rented from DigitalOcean, with their web address and traffic passing through Cloudflare.</li><!--apex-only:end-->
|
|
128
129
|
</ul>
|
|
129
130
|
<p>Nothing is sold, and nothing is shared for advertising.</p>
|
|
130
131
|
|
|
132
|
+
<h2>Your age</h2>
|
|
133
|
+
<p>If you record your age, only its band is kept (for example "adult" or "16 to 17"), not your birthday. It decides whether you can be paid and whether you can use the AI features, which are for adults only.</p>
|
|
134
|
+
|
|
131
135
|
<h2>Session length, and Leaving</h2>
|
|
132
136
|
<p>Sign out to end a session. Browser sessions expire on their own; command-line tokens last until you revoke them from your settings.</p>
|
|
133
137
|
<p>Your builder record and your shipped work stay attributed to you after you leave — the ledger of who built what is the point of the platform. The owner can deactivate a builder: that ends their sessions and releases their claims, and keeps the record.</p>
|
|
@@ -135,7 +139,7 @@
|
|
|
135
139
|
|
|
136
140
|
<h2>The code is open</h2>
|
|
137
141
|
<p>Every part of this — what is stored, where, and for how long — is public. This site runs Cloud Bongos, free software under the AGPL-3.0. <a href="{{sourceRepoUrl}}">Read the source.</a></p>
|
|
138
|
-
<p>This statement describes the software as it ships and changes when the software does. Revised
|
|
142
|
+
<p>This statement describes the software as it ships and changes when the software does. Revised October 2026.</p>
|
|
139
143
|
</section>
|
|
140
144
|
</main>
|
|
141
145
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The Link GitHub chip on the hub (task 1004996): a Google-only person signed in on the sky, so projects.html's own bar copy draws the very small chip with the engineer's icon at the head of its right side. stub.fixtures points the identity at fixtures/me-google.json (the stand-in login ~builder) and the ways-in read at fixtures/me-sign-in-methods-google.json; the default projects.states.json (GitHub linked) draws none.",
|
|
3
|
+
"page": "/projects",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"stub": { "fixtures": { "me": "me-google", "me__sign-in-methods": "me-sign-in-methods-google" } },
|
|
6
|
+
"modes": ["dark"],
|
|
7
|
+
"modeQuery": false,
|
|
8
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/api/bongos/instance$"],
|
|
9
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "\\b404\\b", "Unauthorized"],
|
|
10
|
+
"ignoreControls": [".skip", ".vh"],
|
|
11
|
+
"states": {
|
|
12
|
+
"in-google": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["#ghChip", "#who"], "attr": [["#ghChip", "href", "/api/bongos/auth/link/github/start"]] } }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -11818,6 +11818,44 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11818
11818
|
if (!signedIn && $('onlyMine').checked) { $('onlyMine').checked = false; applyMapFilter(); }
|
|
11819
11819
|
}
|
|
11820
11820
|
|
|
11821
|
+
/* The Link GitHub chip (task 1004996): signed in with no GitHub linked, one very
|
|
11822
|
+
small link at the head of the bar's right side with the engineer's icon. It
|
|
11823
|
+
starts the link (ADR 0396); the answer lands on /account. A failed read, or a
|
|
11824
|
+
child with no sign-in-methods route, draws nothing. Only a stand-in login
|
|
11825
|
+
(~<handle>) is asked about: linking GitHub writes the real login over it.
|
|
11826
|
+
assets/hub-bar.js keeps the same lines for the pages without views; change both. */
|
|
11827
|
+
var GH_TITLE = 'Link GitHub — needed for coding work';
|
|
11828
|
+
function githubChip() {
|
|
11829
|
+
if (!me || String(me.github_login || '').charAt(0) !== '~') return;
|
|
11830
|
+
fetch(API + '/me/sign-in-methods', { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
11831
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
11832
|
+
.then(function (d) {
|
|
11833
|
+
var methods = d && Array.isArray(d.methods) ? d.methods : null;
|
|
11834
|
+
var show = !!methods && !methods.some(function (m) { return m && m.provider === 'github'; });
|
|
11835
|
+
var bar = show && document.querySelector('.barRight');
|
|
11836
|
+
if (!bar) { if ($('ghChip')) $('ghChip').remove(); return; }
|
|
11837
|
+
if ($('ghChip')) return;
|
|
11838
|
+
var a = document.createElement('a');
|
|
11839
|
+
a.className = 'ghChip';
|
|
11840
|
+
a.id = 'ghChip';
|
|
11841
|
+
a.href = API + '/auth/link/github/start';
|
|
11842
|
+
a.title = GH_TITLE;
|
|
11843
|
+
a.setAttribute('aria-label', GH_TITLE);
|
|
11844
|
+
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
11845
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
11846
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
11847
|
+
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
11848
|
+
path.setAttribute('d', 'M2 13.5h12M3.5 13.5V8.5h4v5M7.5 13.5V4.5h5v9');
|
|
11849
|
+
svg.appendChild(path);
|
|
11850
|
+
var label = document.createElement('span');
|
|
11851
|
+
label.className = 'ghChip__label';
|
|
11852
|
+
label.textContent = 'Link GitHub';
|
|
11853
|
+
a.appendChild(svg);
|
|
11854
|
+
a.appendChild(label);
|
|
11855
|
+
bar.insertBefore(a, bar.firstChild);
|
|
11856
|
+
}, function () { /* no answer: no chip */ });
|
|
11857
|
+
}
|
|
11858
|
+
|
|
11821
11859
|
/* Boot. The view is set FIRST, from the URL, so the page is navigable before
|
|
11822
11860
|
any request resolves — /projects?view=new must land on the create view even
|
|
11823
11861
|
if the identity call is slow or fails. */
|
|
@@ -11838,6 +11876,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11838
11876
|
/* renderWho rebuilds the chip, so the waiting count is painted again on it (task 1004646) */
|
|
11839
11877
|
if (window.InviteBadge) window.InviteBadge.refresh();
|
|
11840
11878
|
paintShellIdentity();
|
|
11879
|
+
githubChip();
|
|
11841
11880
|
wizard.onAuth();
|
|
11842
11881
|
$('gate').hidden = true;
|
|
11843
11882
|
$('mine').hidden = false;
|