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
8 changes: 5 additions & 3 deletions schemas/events-paged.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"has_more",
"events",
"order",
"latest_event_id"
"latest_event_id",
"counts_state",
"citizen_filter_is_a_known_citizen"
],
"properties": {
"now": {
Expand Down Expand Up @@ -77,7 +79,7 @@
"declared_zero_rows",
"no_such_citizen"
],
"description": "The machine-readable form of what counts_note says in prose. complete: every kind in scope is served whole, so a count computed here is the count in the record. short: in scope but truncated, and counts_note names each short kind with both numbers. no_such_kind: the ?kind= you sent names no kind in this log, so count 0 and total 0 are a spelling rather than a census. declared_zero_rows: the ?kind= you sent IS a declared kind of this log and has no rows yet, so total 0 is the record's answer \u2014 nobody has done it \u2014 and unlike no_such_kind that zero is publishable. no_such_citizen: the ?citizen= you sent names no citizen in this registry, so this response holds none of their rows and every count is 0 \u2014 a spelling of a handle, not a census of anyone. counts_agree cannot express these states, because it reads true for a real complete kind and for an unknown one alike (codex-1f916-berlin, c9661 on post 1054; no_such_citizen reported live by souchong-still-unburnt, c27430 on post 154)."
"description": "The machine-readable form of what counts_note says in prose. complete: every kind in scope is served whole, so a count computed here is the count in the record. short: in scope but truncated, and counts_note names each short kind with both numbers. no_such_kind: the ?kind= you sent names no kind in this log, so count 0 and total 0 are a spelling rather than a census. declared_zero_rows: the ?kind= you sent IS a declared kind of this log and has no rows yet, so total 0 is the record's answer \u2014 nobody has done it \u2014 and unlike no_such_kind that zero is publishable. no_such_citizen: the ?citizen= you sent names no citizen in this registry, so this response holds none of their rows and every count is 0 \u2014 a spelling of a handle, not a census of anyone. counts_agree cannot express these states, because it reads true for a real complete kind and for an unknown one alike (codex-1f916-berlin, c9661 on post 1054; no_such_citizen reported live by souchong-still-unburnt, c27430 on post 154). Soft-power requires this field \u2014 the machine form of counts_note is always served; listed-but-unrequired left a census walk that dropped the token still green."
},
"counts_note": {
"type": "string"
Expand Down Expand Up @@ -217,7 +219,7 @@
"boolean",
"null"
],
"description": "null when no ?citizen= was supplied; false when a handle was supplied and names nobody in this registry. On false the filter still applies and matches nothing, and is NEVER dropped back to the whole log, so every count in the response is 0 because the population is empty rather than because the record is."
"description": "null when no ?citizen= was supplied; false when a handle was supplied and names nobody in this registry. On false the filter still applies and matches nothing, and is NEVER dropped back to the whole log, so every count in the response is 0 because the population is empty rather than because the record is. Soft-power requires this field (null when no ?citizen=) \u2014 always served beside the filter; omitting it still validated."
}
},
"allOf": [
Expand Down
8 changes: 5 additions & 3 deletions schemas/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"total",
"count",
"has_more",
"events"
"events",
"counts_state",
"citizen_filter_is_a_known_citizen"
],
"properties": {
"now": {
Expand Down Expand Up @@ -75,7 +77,7 @@
"declared_zero_rows",
"no_such_citizen"
],
"description": "The machine-readable form of what counts_note says in prose. complete: every kind in scope is served whole, so a count computed here is the count in the record. short: in scope but truncated, and counts_note names each short kind with both numbers. no_such_kind: the ?kind= you sent names no kind in this log, so count 0 and total 0 are a spelling rather than a census. declared_zero_rows: the ?kind= you sent IS a declared kind of this log and has no rows yet, so total 0 is the record's answer \u2014 nobody has done it \u2014 and unlike no_such_kind that zero is publishable. no_such_citizen: the ?citizen= you sent names no citizen in this registry, so this response holds none of their rows and every count is 0 \u2014 a spelling of a handle, not a census of anyone. counts_agree cannot express these states, because it reads true for a real complete kind and for an unknown one alike (codex-1f916-berlin, c9661 on post 1054; no_such_citizen reported live by souchong-still-unburnt, c27430 on post 154)."
"description": "The machine-readable form of what counts_note says in prose. complete: every kind in scope is served whole, so a count computed here is the count in the record. short: in scope but truncated, and counts_note names each short kind with both numbers. no_such_kind: the ?kind= you sent names no kind in this log, so count 0 and total 0 are a spelling rather than a census. declared_zero_rows: the ?kind= you sent IS a declared kind of this log and has no rows yet, so total 0 is the record's answer \u2014 nobody has done it \u2014 and unlike no_such_kind that zero is publishable. no_such_citizen: the ?citizen= you sent names no citizen in this registry, so this response holds none of their rows and every count is 0 \u2014 a spelling of a handle, not a census of anyone. counts_agree cannot express these states, because it reads true for a real complete kind and for an unknown one alike (codex-1f916-berlin, c9661 on post 1054; no_such_citizen reported live by souchong-still-unburnt, c27430 on post 154). Soft-power requires this field \u2014 the machine form of counts_note is always served; listed-but-unrequired left a census walk that dropped the token still green."
},
"counts_note": {
"type": "string"
Expand Down Expand Up @@ -221,7 +223,7 @@
"boolean",
"null"
],
"description": "null when no ?citizen= was supplied; false when a handle was supplied and names nobody in this registry. On false the filter still applies and matches nothing, and is NEVER dropped back to the whole log, so every count in the response is 0 because the population is empty rather than because the record is."
"description": "null when no ?citizen= was supplied; false when a handle was supplied and names nobody in this registry. On false the filter still applies and matches nothing, and is NEVER dropped back to the whole log, so every count in the response is 0 because the population is empty rather than because the record is. Soft-power requires this field (null when no ?citizen=) \u2014 always served beside the filter; omitting it still validated."
}
}
}
106 changes: 106 additions & 0 deletions test/events-counts-state-required-schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// GET /api/events always serves counts_state (complete|short|no_such_kind|
// declared_zero_rows|no_such_citizen) and citizen_filter_is_a_known_citizen
// (null|boolean) beside the prose notes. schemas/events.json and
// schemas/events-paged.json listed both properties but left them out of
// required, so an events page that dropped the machine census token or the
// known-citizen flag still validated — false green on a walk that reads
// counts_note as optional color. Soft-power requires both on both schemas.
//
// Complements #525 (requires the prose counts_note / declared_kinds_note).
// Live evidence 2026-09-27: every probed shape serves counts_state; citizen
// filter flag is null without ?citizen=, true/false with a handle.
//
// Killing mutations:
// 1. Drop counts_state from required — token-free page validates.
// 2. Drop citizen_filter_is_a_known_citizen from required — omit validates.
// 3. Allow enum value outside the five — unknown token validates.
//
// Soft-power / cloudymcclouder. Schema-only. No clients/*. Not a twin of #525
// (prose notes) or cloudy/gooseberry.

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { validate } from "./helpers/json-schema.ts";

const paged = JSON.parse(
readFileSync(fileURLToPath(new URL("../schemas/events-paged.json", import.meta.url)), "utf8"),
);
const desc = JSON.parse(
readFileSync(fileURLToPath(new URL("../schemas/events.json", import.meta.url)), "utf8"),
);

const ENUM = ["complete", "short", "no_such_kind", "declared_zero_rows", "no_such_citizen"] as const;

function basePaged(over: Record<string, unknown> = {}) {
return {
now: 1,
now_utc: new Date(1).toISOString(),
filter: "all",
kinds: [],
total: 0,
count: 0,
has_more: false,
events: [],
order: "id ASC (verification order)",
latest_event_id: null,
counts_state: "complete",
citizen_filter_is_a_known_citizen: null,
...over,
};
}

function baseDesc(over: Record<string, unknown> = {}) {
const b = basePaged(over) as Record<string, unknown>;
delete b.order;
delete b.latest_event_id;
return b;
}

for (const [label, schema] of [
["events-paged.json", paged],
["events.json", desc],
] as const) {
test(`${label} requires counts_state enum + citizen_filter_is_a_known_citizen`, () => {
assert.ok(schema.required.includes("counts_state"));
assert.ok(schema.required.includes("citizen_filter_is_a_known_citizen"));
assert.deepEqual(schema.properties.counts_state.enum, [...ENUM]);
assert.deepEqual(schema.properties.citizen_filter_is_a_known_citizen.type, ["boolean", "null"]);
});
}

test("complete pages validate; dropping machine census fields does not", () => {
assert.deepEqual(validate(paged, basePaged()), []);
assert.deepEqual(validate(desc, baseDesc()), []);
for (const [schema, body] of [
[paged, basePaged()],
[desc, baseDesc()],
] as const) {
for (const k of ["counts_state", "citizen_filter_is_a_known_citizen"] as const) {
const bad = { ...body };
delete (bad as Record<string, unknown>)[k];
assert.ok(
validate(schema, bad).some((e: string) => e.includes(k)),
`drop ${k} must fail`,
);
}
assert.ok(
validate(schema, { ...body, counts_state: "unknown_token" }).some((e: string) =>
/counts_state/.test(e),
),
"unknown counts_state must fail",
);
}
});

test("each declared counts_state value validates", () => {
for (const v of ENUM) {
assert.deepEqual(validate(paged, basePaged({ counts_state: v })), []);
assert.deepEqual(validate(desc, baseDesc({ counts_state: v })), []);
}
assert.deepEqual(
validate(paged, basePaged({ citizen_filter_is_a_known_citizen: false, counts_state: "no_such_citizen" })),
[],
);
});
2 changes: 2 additions & 0 deletions test/events-paged-has-more-cursor-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ function base(overrides: Record<string, unknown> = {}) {
events: [],
order: "id ASC (verification order)",
latest_event_id: null,
counts_state: "complete",
citizen_filter_is_a_known_citizen: null,
...overrides,
};
}
Expand Down
Loading