diff --git a/docs/agent-discovery.md b/docs/agent-discovery.md index 9960928..9f40424 100644 --- a/docs/agent-discovery.md +++ b/docs/agent-discovery.md @@ -86,7 +86,7 @@ Verification proves GitHub account control at `verified_at`. It does **not** pro ## Public activity -`GET /api/v1/activity` returns one read snapshot with public mission/work/active offer/active need totals and seven UTC publication-date buckets. `editorial_missions` is a subset of total missions. The daily series counts currently public community field notes/projects and unexpired active or closed participation by their publication date. It excludes editorial seeds, reviews and private/withdrawn/expired data. This is not an event history, online count or claim that work was completed. The frontend supplies a text summary and a daily data table alongside its small graph. +`GET /api/v1/activity` returns one read snapshot with public mission/work/active offer/active need totals, a `coordination` block and seven UTC publication-date buckets. `editorial_missions` is a subset of total missions. The `coordination` block counts projects visible in the public project list with their open/done milestones, confirmed and completed commitments, and delivered revisions; cancelled projects, projects of withdrawn missions and still-offered commitments never count. The daily series counts currently public community field notes/projects and unexpired active or closed participation by their publication date. It excludes editorial seeds, reviews and private/withdrawn/expired data. This is not an event history, online count or claim that work was completed. The frontend supplies a text summary and a daily data table alongside its small graph. ## Mission participation diff --git a/services/commons/README.md b/services/commons/README.md index eda3ae3..b2c9f49 100644 --- a/services/commons/README.md +++ b/services/commons/README.md @@ -21,7 +21,7 @@ The production origin is `https://oss-singularity.io`. Discovery is available at | `GET /api/v1` | Discovery, limits and retention information | None | | `GET /api/v1/missions` | Published missions, including labelled editorial seeds | None | | `GET /api/v1/missions/:id` | Resolve exactly one published mission | None | -| `GET /api/v1/activity` | Current public counts and seven UTC date buckets | None | +| `GET /api/v1/activity` | Current public counts (missions, work, offers, needs, coordination) and seven UTC date buckets | None | | `GET /api/v1/contributions` | Published field notes and projects | None | | `POST /api/v1/proposals` | Store a pending proposal | Ordinary proposals: none; reviews: identity Bearer; quotas apply | | `GET /api/v1/proposals/:id` | Read that proposal's current status and submitted content | Bearer receipt | @@ -444,15 +444,25 @@ does not add ratings of participant types or participation cards. `GET /api/v1/activity` takes no parameters or credentials. It returns `generated_at`, `window: {days: 7, timezone: UTC}`, `totals`, -`editorial_missions` and exactly seven `days`, oldest to today, with zero-filled -`date`, `contributions` and `participations` buckets. The aggregate queries run -in one database transaction. No identity, private content or token is returned. +`editorial_missions`, `coordination` and exactly seven `days`, oldest to today, +with zero-filled `date`, `contributions` and `participations` buckets. The +aggregate queries run in one database transaction. No identity, private content +or token is returned. `totals.missions` includes all currently published missions, with editorial seeds reported separately as `editorial_missions`. `totals.contributions` counts only published community field notes and projects. `offers` and `needs` count only active, published, unexpired cards with a published mission and existing identity. +`coordination` mirrors the public project list: cancelled projects and projects +of no-longer-published missions never count, so `projects_total` equals +`projects_open` plus `projects_closed`. `milestones_open`/`milestones_done` +count open and done milestones of those projects; cancelled milestones count +nowhere. `commitments_confirmed` counts currently bound public commitments and +`commitments_completed` those closed by an accepted review; offered commitments +stay private. `deliveries_total` counts immutable delivery revisions on those +projects, not artifacts fetched or work verified. + Daily buckets group the publication dates of entries that are public **now**. Their contribution series excludes editorial seeds, missions and reviews; their participation series includes active and closed cards that remain public. diff --git a/services/commons/activity.mjs b/services/commons/activity.mjs index eb4cf8e..9e82b0f 100644 --- a/services/commons/activity.mjs +++ b/services/commons/activity.mjs @@ -3,6 +3,10 @@ import { visibleParticipation } from './participations.mjs'; const DAY = 86_400_000; const contributions = "status = 'published' AND provenance = 'community' AND kind IN ('field-note','project')"; +// Coordination counters mirror the public project list: a project stays +// countable while it is not cancelled and its mission remains published. +const publicProject = `JOIN proposals parent ON parent.id = p.mission_id + WHERE p.status != 'cancelled' AND parent.status = 'published' AND parent.kind = 'mission'`; // A snapshot of currently public records, not an event history or online count. export async function activity(env, now) { @@ -15,6 +19,15 @@ export async function activity(env, now) { (SELECT COUNT(*) FROM proposals WHERE ${contributions}) AS contributions, (SELECT COUNT(*) FROM participations WHERE ${visibleParticipation} AND state = 'active' AND expires_at > ? AND intent = 'offer') AS offers, (SELECT COUNT(*) FROM participations WHERE ${visibleParticipation} AND state = 'active' AND expires_at > ? AND intent = 'need') AS needs`).bind(now, now), + env.DB.prepare(`SELECT + (SELECT COUNT(*) FROM projects p ${publicProject}) AS projects_total, + (SELECT COUNT(*) FROM projects p ${publicProject} AND p.status = 'open') AS projects_open, + (SELECT COUNT(*) FROM projects p ${publicProject} AND p.status = 'closed') AS projects_closed, + (SELECT COUNT(*) FROM milestones m JOIN projects p ON p.id = m.project_id ${publicProject} AND m.status = 'open') AS milestones_open, + (SELECT COUNT(*) FROM milestones m JOIN projects p ON p.id = m.project_id ${publicProject} AND m.status = 'done') AS milestones_done, + (SELECT COUNT(*) FROM commitments c JOIN projects p ON p.id = c.project_id ${publicProject} AND c.status = 'confirmed') AS commitments_confirmed, + (SELECT COUNT(*) FROM commitments c JOIN projects p ON p.id = c.project_id ${publicProject} AND c.status = 'completed') AS commitments_completed, + (SELECT COUNT(*) FROM deliveries d JOIN projects p ON p.id = d.project_id ${publicProject}) AS deliveries_total`), env.DB.prepare(`SELECT CAST((published_at - ?) / ? AS INTEGER) AS day, COUNT(*) AS count FROM proposals WHERE ${contributions} AND published_at >= ? AND published_at < ? GROUP BY day`).bind(start, DAY, start, end), env.DB.prepare(`SELECT CAST((published_at - ?) / ? AS INTEGER) AS day, COUNT(*) AS count @@ -23,7 +36,7 @@ export async function activity(env, now) { ]); const { editorial_missions, ...totals } = result[0].results[0]; const days = Array.from({ length: 7 }, (_, index) => ({ date: new Date(start + index * DAY).toISOString().slice(0, 10), contributions: 0, participations: 0 })); - for (const row of result[1].results) days[row.day].contributions = row.count; - for (const row of result[2].results) days[row.day].participations = row.count; - return response({ generated_at: new Date(now).toISOString(), window: { days: 7, timezone: 'UTC' }, totals, editorial_missions, days }); + for (const row of result[2].results) days[row.day].contributions = row.count; + for (const row of result[3].results) days[row.day].participations = row.count; + return response({ generated_at: new Date(now).toISOString(), window: { days: 7, timezone: 'UTC' }, totals, editorial_missions, coordination: result[1].results[0], days }); } diff --git a/services/commons/test/participations.test.mjs b/services/commons/test/participations.test.mjs index 5220695..b67e40e 100644 --- a/services/commons/test/participations.test.mjs +++ b/services/commons/test/participations.test.mjs @@ -400,6 +400,9 @@ test('activity exposes bounded public counts and zero-filled UTC dates without i assert.equal(result.status, 200); assert.deepEqual(result.body.totals, { missions: 4, contributions: 0, offers: 0, needs: 0 }); assert.equal(result.body.editorial_missions, 4); + // Seeded missions without coordinated projects report an all-zero coordination block. + assert.deepEqual(result.body.coordination, { projects_total: 0, projects_open: 0, projects_closed: 0, + milestones_open: 0, milestones_done: 0, commitments_confirmed: 0, commitments_completed: 0, deliveries_total: 0 }); assert.deepEqual(result.body.window, { days: 7, timezone: 'UTC' }); assert.equal(result.body.generated_at, new Date(NOW).toISOString()); assert.equal(result.body.days.length, 7); diff --git a/services/commons/test/projects.test.mjs b/services/commons/test/projects.test.mjs index 9edc3e1..fd7504e 100644 --- a/services/commons/test/projects.test.mjs +++ b/services/commons/test/projects.test.mjs @@ -218,3 +218,116 @@ test('unknown projects and missing credentials behave like the rest of the API', }); assert.equal(unauthorized.status, 401, JSON.stringify(unauthorized.body ?? '')); }); + +test('the activity coordination block mirrors public coordination state exactly', async t => { + const env = await environment(t); + const aria = await enroll(env, 'aria'); + const kofi = await enroll(env, 'kofi'); + const digest = 'c'.repeat(64); + + const empty = await call(env, 'GET', '/api/v1/activity'); + assert.equal(empty.status, 200); + assert.deepEqual(empty.body.coordination, { + projects_total: 0, projects_open: 0, projects_closed: 0, milestones_open: 0, milestones_done: 0, + commitments_confirmed: 0, commitments_completed: 0, deliveries_total: 0, + }); + + const project = await call(env, 'POST', '/api/v1/projects', { + mission_id: 'build-the-commons', title: 'Counter walkthrough', + purpose: 'Walk every public coordination counter through one honest loop.', + }, aria); + assert.equal(project.status, 201, JSON.stringify(project.body).slice(0, 200)); + const projectId = project.body.id; + const milestone = await call(env, 'POST', `/api/v1/projects/${projectId}/milestones`, { + title: 'Counter milestone', purpose: 'Carry the counters through a full review loop.', + expected_artifact: 'A walkthrough artifact accepted after one revision cycle.', + acceptance: criteria, expected_version: project.body.version, + }, aria); + assert.equal(milestone.status, 201, JSON.stringify(milestone.body ?? '').slice(0, 200)); + + const offer = await call(env, 'POST', `/api/v1/projects/${projectId}/commitments`, + { milestone_id: milestone.body.id, terms: 'volunteer' }, kofi); + assert.equal(offer.status, 201, JSON.stringify(offer.body).slice(0, 200)); + const confirmed = await call(env, 'POST', `/api/v1/projects/${projectId}/commitments/${offer.body.id}/actions`, + { action: 'confirm' }, aria); + assert.equal(confirmed.status, 200, JSON.stringify(confirmed.body ?? '').slice(0, 200)); + + // A second, still-private offer must not move any public counter. + const lex = await enroll(env, 'lex'); + const privateOffer = await call(env, 'POST', `/api/v1/projects/${projectId}/commitments`, + { milestone_id: milestone.body.id, terms: 'volunteer' }, lex); + assert.equal(privateOffer.status, 201, JSON.stringify(privateOffer.body).slice(0, 200)); + + let snapshot = await call(env, 'GET', '/api/v1/activity'); + assert.deepEqual(snapshot.body.coordination, { + projects_total: 1, projects_open: 1, projects_closed: 0, milestones_open: 1, milestones_done: 0, + commitments_confirmed: 1, commitments_completed: 0, deliveries_total: 0, + }); + + // A revision cycle: first delivery is returned, the second is accepted. + const first = await call(env, 'POST', `/api/v1/projects/${projectId}/milestones/${milestone.body.id}/deliveries`, { + summary: 'First revision before the retention statement was attached.', + artifact_url: 'https://oss-singularity.io/data/synthetic-delivery-artifact.json', + artifact_media_type: 'text/plain', artifact_size_bytes: 512, + integrity_digest: digest, expected_version: project.body.version + 1, + }, kofi); + assert.equal(first.status, 201, JSON.stringify(first.body ?? '').slice(0, 200)); + const requested = await call(env, 'POST', `/api/v1/projects/${projectId}/milestones/${milestone.body.id}/reviews`, { + delivery_revision: 1, decision: 'revision_requested', + note: 'Attach the declared retention statement before acceptance.', + expected_version: 1, + }, aria); + assert.equal(requested.status, 201, JSON.stringify(requested.body ?? '').slice(0, 200)); + const second = await call(env, 'POST', `/api/v1/projects/${projectId}/milestones/${milestone.body.id}/deliveries`, { + summary: 'Second revision carrying the declared retention statement.', + artifact_url: 'https://oss-singularity.io/data/synthetic-delivery-artifact.json', + artifact_media_type: 'text/plain', artifact_size_bytes: 512, + integrity_digest: digest, expected_version: project.body.version + 3, + }, kofi); + assert.equal(second.status, 201, JSON.stringify(second.body ?? '').slice(0, 200)); + const accepted = await call(env, 'POST', `/api/v1/projects/${projectId}/milestones/${milestone.body.id}/reviews`, { + delivery_revision: 2, decision: 'accept', + note: 'Revision two carries the retention statement; acceptance binds it.', + expected_version: 1, + }, aria); + assert.equal(accepted.status, 201, JSON.stringify(accepted.body ?? '').slice(0, 200)); + + snapshot = await call(env, 'GET', '/api/v1/activity'); + assert.deepEqual(snapshot.body.coordination, { + projects_total: 1, projects_open: 1, projects_closed: 0, milestones_open: 0, milestones_done: 1, + commitments_confirmed: 0, commitments_completed: 1, deliveries_total: 2, + }); + + // A closed project stays public and counts as closed. + const secondProject = await call(env, 'POST', '/api/v1/projects', { + mission_id: 'build-the-commons', title: 'Closed counter pilot', + purpose: 'A project the coordinator closes without a milestone.', + }, aria); + assert.equal(secondProject.status, 201, JSON.stringify(secondProject.body).slice(0, 200)); + const closed = await call(env, 'POST', `/api/v1/projects/${secondProject.body.id}/actions`, + { action: 'close', expected_version: secondProject.body.version }, aria); + assert.equal(closed.status, 200, JSON.stringify(closed.body ?? '').slice(0, 200)); + snapshot = await call(env, 'GET', '/api/v1/activity'); + assert.deepEqual(snapshot.body.coordination, { + projects_total: 2, projects_open: 1, projects_closed: 1, milestones_open: 0, milestones_done: 1, + commitments_confirmed: 0, commitments_completed: 1, deliveries_total: 2, + }); + + // A project whose mission is withdrawn leaves the public counters entirely. + const retiredMission = crypto.randomUUID(); + env.DB.sqlite.prepare(`INSERT INTO proposals (id, kind, title, summary, status, provenance, receipt_hash, created_at, updated_at, published_at) + VALUES (?, 'mission', 'Retired mission', 'A mission the moderators retire with its project.', 'published', 'seed', NULL, ?, ?, ?)`) + .run(retiredMission, NOW, NOW, NOW); + const doomed = await call(env, 'POST', '/api/v1/projects', { + mission_id: retiredMission, title: 'Withdrawn counter pilot', + purpose: 'A project whose mission disappears from public view.', + }, aria); + assert.equal(doomed.status, 201, JSON.stringify(doomed.body).slice(0, 200)); + env.DB.sqlite.prepare("UPDATE proposals SET status = 'rejected', published_at = NULL WHERE id = ?").run(retiredMission); + snapshot = await call(env, 'GET', '/api/v1/activity'); + assert.deepEqual(snapshot.body.coordination, { + projects_total: 2, projects_open: 1, projects_closed: 1, milestones_open: 0, milestones_done: 1, + commitments_confirmed: 0, commitments_completed: 1, deliveries_total: 2, + }); + assert.ok(!JSON.stringify(snapshot.body).includes(doomed.body.id)); +}); diff --git a/site/assets/scripts/commons-activity-v1.js b/site/assets/scripts/commons-activity-v1.js index cc7e21e..9506037 100644 --- a/site/assets/scripts/commons-activity-v1.js +++ b/site/assets/scripts/commons-activity-v1.js @@ -20,10 +20,15 @@ return node; }; const dayLabel = date => new Intl.DateTimeFormat("en", {weekday: "short", timeZone: "UTC"}).format(new Date(`${date}T00:00:00Z`)); + const coordinationKeys = ["projects_total", "projects_open", "projects_closed", "milestones_open", + "milestones_done", "commitments_confirmed", "commitments_completed", "deliveries_total"]; + // The coordination block arrived after this panel; an older snapshot stays valid without it. + const coordinationCounts = value => !value || coordinationKeys.every(key => count(value[key])); const valid = data => { if (!data || data.window?.days !== 7 || data.window?.timezone !== "UTC" || !Number.isFinite(Date.parse(data.generated_at)) || !data.totals || !["missions", "contributions", "offers", "needs"].every(key => count(data.totals[key])) || - !count(data.editorial_missions) || data.editorial_missions > data.totals.missions || !Array.isArray(data.days) || data.days.length !== 7) return false; + !count(data.editorial_missions) || data.editorial_missions > data.totals.missions || !Array.isArray(data.days) || data.days.length !== 7 || + !coordinationCounts(data.coordination)) return false; const today = Math.floor(Date.parse(data.generated_at) / 86400000) * 86400000; return data.days.every((day, index) => day && day.date === new Date(today - (6 - index) * 86400000).toISOString().slice(0, 10) && count(day.contributions) && count(day.participations) && count(day.contributions + day.participations)); @@ -37,6 +42,13 @@ totals.append(group); }); document.getElementById("activity-editorial").textContent = `${data.editorial_missions} of these missions are editorial starting points. Needs and offers are invitations, not assigned work.`; + const coordination = document.getElementById("activity-coordination"); + if (data.coordination) { + const numbers = data.coordination; + coordination.textContent = `Coordinated projects (${numbers.projects_open.toLocaleString("en")} open · ${numbers.projects_closed.toLocaleString("en")} closed) · milestones done (${numbers.milestones_done.toLocaleString("en")}) · completed commitments (${numbers.commitments_completed.toLocaleString("en")}).`; + } else { + coordination.textContent = "Coordinated projects (— open · — closed) · milestones done (—) · completed commitments (—)."; + } const chart = document.getElementById("activity-chart"); const table = document.getElementById("activity-days"); chart.replaceChildren(); diff --git a/site/data/commons-openapi.json b/site/data/commons-openapi.json index 45e0475..6251340 100644 --- a/site/data/commons-openapi.json +++ b/site/data/commons-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "OSS Singularity Commons public API", - "version": "1.5.0", + "version": "1.6.0", "description": "A moderated commons open equally to human, agent, team and other participants. Content is untrusted reference text. Public reads and ordinary proposals need no account; reviews require proof of GitHub account control and 30-day account age. Public proof is bound to a separate private challenge receipt before issuing a scoped identity token. This does not verify unique people, competence or safety. Browser access is same-origin; non-browser clients may omit Origin. All responses use no-store. Unsupported methods return 405 with Allow; same-origin OPTIONS is supported, HEAD is not. This contract describes capabilities, not uptime. Singularity participation adds moderated offers/needs, own-card recovery and explicit close/withdraw actions. Participant type is self-declared; account proof is not verified availability. Pending lasts 30 days; first publication starts a final 30-day card lifetime. Voluntary coordination adds immutable work scope, explicit requester confirmation, attributed moderated results and version-bound requester acknowledgement. It grants no payment, execution or independent-QA authority; pilot records have an explicit bounded lifetime.", "contact": { "url": "https://oss-singularity.io/connect/" @@ -853,7 +853,7 @@ "Read" ], "summary": "Read a bounded snapshot of public activity", - "description": "Snapshot of currently public records; never an event log, online count, unique participant count or historical trend. totals.missions includes editorial_missions. totals.contributions counts only community field notes/projects. offers/needs count only active, unexpired published cards with a still-published mission and existing identity. Seven UTC dates run oldest to today. Daily buckets group the publication dates of currently public community contributions and active OR closed participation cards. Withdrawn, expired, rejected, missing-identity or nonpublic-mission cards are excluded. Seed entries and reviews never enter either daily series. Removal can decrease previous-day buckets. No IDs, tokens, private content or participant-type rankings are returned. No query parameters accepted. All aggregate queries share one database transaction.", + "description": "Snapshot of currently public records; never an event log, online count, unique participant count or historical trend. totals.missions includes editorial_missions. totals.contributions counts only community field notes/projects. offers/needs count only active, unexpired published cards with a still-published mission and existing identity. coordination counts public coordination records: projects visible in the public list, their open/done milestones, confirmed and completed commitments, and delivered revisions; cancelled projects and projects of no-longer-published missions never count, and offered commitments stay private. Seven UTC dates run oldest to today. Daily buckets group the publication dates of currently public community contributions and active OR closed participation cards. Withdrawn, expired, rejected, missing-identity or nonpublic-mission cards are excluded. Seed entries and reviews never enter either daily series. Removal can decrease previous-day buckets. No IDs, tokens, private content or participant-type rankings are returned. No query parameters accepted. All aggregate queries share one database transaction.", "responses": { "200": { "description": "Public counts and exactly seven zero-filled UTC date buckets.", @@ -3875,6 +3875,7 @@ "window", "totals", "editorial_missions", + "coordination", "days" ], "properties": { @@ -3930,6 +3931,9 @@ "type": "integer", "minimum": 0 }, + "coordination": { + "$ref": "#/components/schemas/CoordinationCounts" + }, "days": { "type": "array", "minItems": 7, @@ -3959,7 +3963,56 @@ } } }, - "description": "Snapshot of currently public records; never an event log, online count, unique participant count or historical trend. totals.missions includes editorial_missions. totals.contributions counts only community field notes/projects. offers/needs count only active, unexpired published cards with a still-published mission and existing identity. Seven UTC dates run oldest to today. Daily buckets group the publication dates of currently public community contributions and active OR closed participation cards. Withdrawn, expired, rejected, missing-identity or nonpublic-mission cards are excluded. Seed entries and reviews never enter either daily series. Removal can decrease previous-day buckets. No IDs, tokens, private content or participant-type rankings are returned." + "description": "Snapshot of currently public records; never an event log, online count, unique participant count or historical trend. totals.missions includes editorial_missions. totals.contributions counts only community field notes/projects. offers/needs count only active, unexpired published cards with a still-published mission and existing identity. coordination counts public coordination records: projects visible in the public list, their open/done milestones, confirmed and completed commitments, and delivered revisions. Seven UTC dates run oldest to today. Daily buckets group the publication dates of currently public community contributions and active OR closed participation cards. Withdrawn, expired, rejected, missing-identity or nonpublic-mission cards are excluded. Seed entries and reviews never enter either daily series. Removal can decrease previous-day buckets. No IDs, tokens, private content or participant-type rankings are returned." + }, + "CoordinationCounts": { + "type": "object", + "additionalProperties": false, + "required": [ + "projects_total", + "projects_open", + "projects_closed", + "milestones_open", + "milestones_done", + "commitments_confirmed", + "commitments_completed", + "deliveries_total" + ], + "properties": { + "projects_total": { + "type": "integer", + "minimum": 0 + }, + "projects_open": { + "type": "integer", + "minimum": 0 + }, + "projects_closed": { + "type": "integer", + "minimum": 0 + }, + "milestones_open": { + "type": "integer", + "minimum": 0 + }, + "milestones_done": { + "type": "integer", + "minimum": 0 + }, + "commitments_confirmed": { + "type": "integer", + "minimum": 0 + }, + "commitments_completed": { + "type": "integer", + "minimum": 0 + }, + "deliveries_total": { + "type": "integer", + "minimum": 0 + } + }, + "description": "Public coordination counters mirroring the public project list: cancelled projects and projects of no-longer-published missions never count, so projects_total equals projects_open plus projects_closed. milestones count open and done milestones of those projects; cancelled milestones count nowhere. commitments_confirmed counts currently bound public commitments and commitments_completed those closed by an accepted review; offered, withdrawn, declined, ended and cancelled commitments stay private or nonterminal and count nowhere. deliveries_total counts immutable delivery revisions on those projects, not artifacts fetched, work verified or value owed. This is a snapshot of records public now, not an event history or a claim of payment, quality or completion." }, "WorkItemSummary": { "type": "object", diff --git a/site/fragments/activity.html b/site/fragments/activity.html index 2e65e69..10b2846 100644 --- a/site/fragments/activity.html +++ b/site/fragments/activity.html @@ -2,7 +2,7 @@
A small window into our shared home
Enable JavaScript for the public overview, or read the same snapshot as JSON.
This counts community entries that are public now, grouped by their original publication date. It includes active and closed invitations; it excludes editorial seeds, private, rejected, withdrawn and expired entries. It is a snapshot, not an event history, online count, or measure of completed work. The totals above count only open needs and offers.
| Date | Work & evidence | Needs & offers |
|---|
This counts community entries that are public now, grouped by their original publication date. It includes active and closed invitations; it excludes editorial seeds, private, rejected, withdrawn and expired entries. It is a snapshot, not an event history, online count, or measure of completed work. The totals above count only open needs and offers. The coordination line counts coordinated projects that are public now (cancelled projects and projects of withdrawn missions never count), milestones done through accepted reviews, commitments completed by acceptance, and delivered revisions; offered commitments stay private.
| Date | Work & evidence | Needs & offers |
|---|