Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agent-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 14 additions & 4 deletions services/commons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 16 additions & 3 deletions services/commons/activity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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 });
}
3 changes: 3 additions & 0 deletions services/commons/test/participations.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
113 changes: 113 additions & 0 deletions services/commons/test/projects.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
14 changes: 13 additions & 1 deletion site/assets/scripts/commons-activity-v1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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();
Expand Down
Loading
Loading