deploy: c2c7937
Browse files
space/src/consumer-coverage-update.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 1 |
import type Database from "better-sqlite3";
|
| 2 |
import { canonicalJson } from "@xtap-pool/shared";
|
| 3 |
import type { ResolvedConsumerContext } from "./consumer-context.js";
|
|
|
|
| 4 |
import { historicalSelection } from "./consumer-changes.js";
|
| 5 |
import { selectedCompleteThrough } from "./enrich-store.js";
|
| 6 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import { ConsumerCoverageEffects } from "./consumer-coverage-effects.js";
|
| 8 |
|
| 9 |
export type ConsumerCoverage = {
|
|
@@ -63,6 +68,27 @@ function metricCoverage(
|
|
| 63 |
function recalculate(
|
| 64 |
database: Database.Database,
|
| 65 |
target: ResolvedConsumerContext,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
): ConsumerCoverage {
|
| 67 |
return {
|
| 68 |
kind: "coverage",
|
|
|
|
| 1 |
import type Database from "better-sqlite3";
|
| 2 |
import { canonicalJson } from "@xtap-pool/shared";
|
| 3 |
import type { ResolvedConsumerContext } from "./consumer-context.js";
|
| 4 |
+
import { ConsumerBootstrapRequired, ConsumerIndexState } from "./consumer-index-state.js";
|
| 5 |
import { historicalSelection } from "./consumer-changes.js";
|
| 6 |
import { selectedCompleteThrough } from "./enrich-store.js";
|
| 7 |
+
import {
|
| 8 |
+
selectedCurrentCoverageUnitIds,
|
| 9 |
+
selectedCurrentObservationThrough,
|
| 10 |
+
selectedObservationThrough,
|
| 11 |
+
} from "./consumer-coverage.js";
|
| 12 |
import { ConsumerCoverageEffects } from "./consumer-coverage-effects.js";
|
| 13 |
|
| 14 |
export type ConsumerCoverage = {
|
|
|
|
| 68 |
function recalculate(
|
| 69 |
database: Database.Database,
|
| 70 |
target: ResolvedConsumerContext,
|
| 71 |
+
): ConsumerCoverage {
|
| 72 |
+
try {
|
| 73 |
+
new ConsumerIndexState(database, target.context.contract, "read").require(
|
| 74 |
+
target.context.source,
|
| 75 |
+
);
|
| 76 |
+
} catch (error) {
|
| 77 |
+
if (error instanceof ConsumerBootstrapRequired) return historicalCoverage(database, target);
|
| 78 |
+
throw error;
|
| 79 |
+
}
|
| 80 |
+
const unitIds = selectedCurrentCoverageUnitIds(database, target);
|
| 81 |
+
return {
|
| 82 |
+
kind: "coverage",
|
| 83 |
+
mode: "semantic",
|
| 84 |
+
completeThrough: selectedCompleteThrough(database, { unitIds }) ?? null,
|
| 85 |
+
observationsThrough: selectedCurrentObservationThrough(database, target, unitIds),
|
| 86 |
+
};
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function historicalCoverage(
|
| 90 |
+
database: Database.Database,
|
| 91 |
+
target: ResolvedConsumerContext,
|
| 92 |
): ConsumerCoverage {
|
| 93 |
return {
|
| 94 |
kind: "coverage",
|
space/src/consumer-coverage.ts
CHANGED
|
@@ -4,6 +4,36 @@ import { eligibleUnits } from "./enrich-store.js";
|
|
| 4 |
import { historicalSelection } from "./consumer-changes.js";
|
| 5 |
import type { ResolvedConsumerContext } from "./consumer-context.js";
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
/** Current selected, completed public content only. Counters have their own clock;
|
| 8 |
* pending, unselected, subscriber-only and retweet samples cannot advance it. */
|
| 9 |
export function selectedObservationThrough(
|
|
@@ -11,8 +41,6 @@ export function selectedObservationThrough(
|
|
| 11 |
target: ResolvedConsumerContext,
|
| 12 |
postIds?: readonly string[],
|
| 13 |
): string | null {
|
| 14 |
-
const context = target.context;
|
| 15 |
-
const selection = historicalSelection(target);
|
| 16 |
const unitIds =
|
| 17 |
postIds === undefined
|
| 18 |
? undefined
|
|
@@ -21,13 +49,40 @@ export function selectedObservationThrough(
|
|
| 21 |
"SELECT DISTINCT unit_id FROM unit_members WHERE tweet_id IN (SELECT value FROM json_each(?))",
|
| 22 |
)
|
| 23 |
.all(JSON.stringify(postIds))
|
| 24 |
-
.map((row) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
const eligible = eligibleUnits({
|
| 26 |
-
...selection,
|
| 27 |
-
|
| 28 |
taxonomyVersion: context.taxonomy.version,
|
| 29 |
contractHash: context.contract,
|
| 30 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
// Read each selected post once and stop at its newest permitted observation.
|
| 32 |
// Scanning every historical counter sample repeats source and visibility checks.
|
| 33 |
const row = database
|
|
@@ -51,8 +106,7 @@ export function selectedObservationThrough(
|
|
| 51 |
AND (json_type(c.payload_json, '$.is_subscriber_only') IS NULL OR json_type(c.payload_json, '$.is_subscriber_only') = 'false')
|
| 52 |
AND (json_type(c.payload_json, '$.is_retweet') IS NULL OR json_type(c.payload_json, '$.is_retweet') = 'false')
|
| 53 |
AND json_extract(c.payload_json, '$.author.id') IN (SELECT value FROM json_each(?))
|
| 54 |
-
|
| 55 |
-
AND s.segment_key IN (SELECT json_extract(value, '$.key') FROM json_each(?)))
|
| 56 |
ORDER BY o.observed_at DESC, o.observation_id DESC LIMIT 1
|
| 57 |
)) AS latest FROM selected_posts p
|
| 58 |
`,
|
|
@@ -65,7 +119,7 @@ export function selectedObservationThrough(
|
|
| 65 |
postIds === undefined ? null : JSON.stringify(postIds),
|
| 66 |
postIds === undefined ? null : JSON.stringify(postIds),
|
| 67 |
JSON.stringify(selection.authorIds),
|
| 68 |
-
JSON.stringify(target.snapshot.files),
|
| 69 |
);
|
| 70 |
return z.object({ latest: z.string().nullable() }).parse(row).latest;
|
| 71 |
}
|
|
|
|
| 4 |
import { historicalSelection } from "./consumer-changes.js";
|
| 5 |
import type { ResolvedConsumerContext } from "./consumer-context.js";
|
| 6 |
|
| 7 |
+
const unitRow = z.object({ unit_id: z.string() });
|
| 8 |
+
|
| 9 |
+
/** Historical membership is an indexed superset of current selected units.
|
| 10 |
+
* Start there, then apply the exact current author and publication rules once
|
| 11 |
+
* for both coverage clocks. */
|
| 12 |
+
export function selectedCurrentCoverageUnitIds(
|
| 13 |
+
database: Database.Database,
|
| 14 |
+
target: ResolvedConsumerContext,
|
| 15 |
+
): string[] {
|
| 16 |
+
const context = target.context;
|
| 17 |
+
const authors = JSON.stringify(context.selection.author_ids);
|
| 18 |
+
return database
|
| 19 |
+
.prepare(
|
| 20 |
+
`WITH candidates AS MATERIALIZED (
|
| 21 |
+
SELECT DISTINCT unit_id FROM consumer_post_units
|
| 22 |
+
WHERE author_id IN (SELECT value FROM json_each(@authors))
|
| 23 |
+
) SELECT c.unit_id FROM candidates c WHERE
|
| 24 |
+
NOT EXISTS (SELECT 1 FROM unit_members m JOIN tweets t ON t.id = m.tweet_id
|
| 25 |
+
WHERE m.unit_id = c.unit_id AND (
|
| 26 |
+
json_extract(t.json, '$.author.id') IS NULL
|
| 27 |
+
OR json_extract(t.json, '$.author.id') NOT IN (SELECT value FROM json_each(@authors))
|
| 28 |
+
OR json_extract(t.json, '$.is_subscriber_only') = 1))
|
| 29 |
+
AND EXISTS (SELECT 1 FROM unit_members m JOIN tweets t ON t.id = m.tweet_id
|
| 30 |
+
WHERE m.unit_id = c.unit_id AND COALESCE(json_extract(t.json, '$.is_retweet'), 0) != 1)
|
| 31 |
+
ORDER BY c.unit_id`,
|
| 32 |
+
)
|
| 33 |
+
.all({ authors })
|
| 34 |
+
.map((row) => unitRow.parse(row).unit_id);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
/** Current selected, completed public content only. Counters have their own clock;
|
| 38 |
* pending, unselected, subscriber-only and retweet samples cannot advance it. */
|
| 39 |
export function selectedObservationThrough(
|
|
|
|
| 41 |
target: ResolvedConsumerContext,
|
| 42 |
postIds?: readonly string[],
|
| 43 |
): string | null {
|
|
|
|
|
|
|
| 44 |
const unitIds =
|
| 45 |
postIds === undefined
|
| 46 |
? undefined
|
|
|
|
| 49 |
"SELECT DISTINCT unit_id FROM unit_members WHERE tweet_id IN (SELECT value FROM json_each(?))",
|
| 50 |
)
|
| 51 |
.all(JSON.stringify(postIds))
|
| 52 |
+
.map((row) => unitRow.parse(row).unit_id);
|
| 53 |
+
return observationThrough(database, target, unitIds, postIds, false);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/** The caller proved that the database is exactly the target source and applied
|
| 57 |
+
* current author and publication rules to unitIds. Do not repeat either scan. */
|
| 58 |
+
export function selectedCurrentObservationThrough(
|
| 59 |
+
database: Database.Database,
|
| 60 |
+
target: ResolvedConsumerContext,
|
| 61 |
+
unitIds: readonly string[],
|
| 62 |
+
): string | null {
|
| 63 |
+
return observationThrough(database, target, unitIds, undefined, true);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function observationThrough(
|
| 67 |
+
database: Database.Database,
|
| 68 |
+
target: ResolvedConsumerContext,
|
| 69 |
+
unitIds: readonly string[] | undefined,
|
| 70 |
+
postIds: readonly string[] | undefined,
|
| 71 |
+
currentSource: boolean,
|
| 72 |
+
): string | null {
|
| 73 |
+
const context = target.context;
|
| 74 |
+
const selection = historicalSelection(target);
|
| 75 |
const eligible = eligibleUnits({
|
| 76 |
+
...(currentSource ? { labels: selection.labels, labelMode: selection.labelMode } : selection),
|
| 77 |
+
unitIds,
|
| 78 |
taxonomyVersion: context.taxonomy.version,
|
| 79 |
contractHash: context.contract,
|
| 80 |
});
|
| 81 |
+
const sourceSql = currentSource
|
| 82 |
+
? ""
|
| 83 |
+
: `AND EXISTS (SELECT 1 FROM observation_sources s INDEXED BY idx_observation_source_id
|
| 84 |
+
WHERE s.observation_id = o.observation_id
|
| 85 |
+
AND s.segment_key IN (SELECT json_extract(value, '$.key') FROM json_each(?)))`;
|
| 86 |
// Read each selected post once and stop at its newest permitted observation.
|
| 87 |
// Scanning every historical counter sample repeats source and visibility checks.
|
| 88 |
const row = database
|
|
|
|
| 106 |
AND (json_type(c.payload_json, '$.is_subscriber_only') IS NULL OR json_type(c.payload_json, '$.is_subscriber_only') = 'false')
|
| 107 |
AND (json_type(c.payload_json, '$.is_retweet') IS NULL OR json_type(c.payload_json, '$.is_retweet') = 'false')
|
| 108 |
AND json_extract(c.payload_json, '$.author.id') IN (SELECT value FROM json_each(?))
|
| 109 |
+
${sourceSql}
|
|
|
|
| 110 |
ORDER BY o.observed_at DESC, o.observation_id DESC LIMIT 1
|
| 111 |
)) AS latest FROM selected_posts p
|
| 112 |
`,
|
|
|
|
| 119 |
postIds === undefined ? null : JSON.stringify(postIds),
|
| 120 |
postIds === undefined ? null : JSON.stringify(postIds),
|
| 121 |
JSON.stringify(selection.authorIds),
|
| 122 |
+
...(currentSource ? [] : [JSON.stringify(target.snapshot.files)]),
|
| 123 |
);
|
| 124 |
return z.object({ latest: z.string().nullable() }).parse(row).latest;
|
| 125 |
}
|
space/src/enrich-store.ts
CHANGED
|
@@ -1536,6 +1536,7 @@ function nullable<T>(value: T | undefined): T | null {
|
|
| 1536 |
}
|
| 1537 |
|
| 1538 |
type UnitSelection = {
|
|
|
|
| 1539 |
authorIds?: readonly string[] | undefined;
|
| 1540 |
labels?: readonly string[] | undefined;
|
| 1541 |
labelMode?: "any" | "all" | undefined;
|
|
@@ -1544,7 +1545,6 @@ type UnitSelection = {
|
|
| 1544 |
};
|
| 1545 |
|
| 1546 |
type EligibleUnitOptions = UnitSelection & {
|
| 1547 |
-
unitIds?: readonly string[];
|
| 1548 |
taxonomyVersion: number;
|
| 1549 |
contractHash: string;
|
| 1550 |
};
|
|
@@ -1626,10 +1626,16 @@ function selectedUnits(options: UnitSelection): { sql: string; params: unknown[]
|
|
| 1626 |
const cutoffParams = options.cutoff === undefined ? [] : [options.cutoff];
|
| 1627 |
const authorParams = options.authorIds ?? [];
|
| 1628 |
return {
|
| 1629 |
-
sql: `WITH unit_ids AS MATERIALIZED (SELECT DISTINCT unit_id FROM unit_members
|
|
|
|
|
|
|
| 1630 |
SELECT um.unit_id FROM unit_ids um
|
| 1631 |
WHERE 1 = 1${cutoffSql}${authorSql}${publicationSql}`,
|
| 1632 |
-
params: [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1633 |
};
|
| 1634 |
}
|
| 1635 |
|
|
|
|
| 1536 |
}
|
| 1537 |
|
| 1538 |
type UnitSelection = {
|
| 1539 |
+
unitIds?: readonly string[] | undefined;
|
| 1540 |
authorIds?: readonly string[] | undefined;
|
| 1541 |
labels?: readonly string[] | undefined;
|
| 1542 |
labelMode?: "any" | "all" | undefined;
|
|
|
|
| 1545 |
};
|
| 1546 |
|
| 1547 |
type EligibleUnitOptions = UnitSelection & {
|
|
|
|
| 1548 |
taxonomyVersion: number;
|
| 1549 |
contractHash: string;
|
| 1550 |
};
|
|
|
|
| 1626 |
const cutoffParams = options.cutoff === undefined ? [] : [options.cutoff];
|
| 1627 |
const authorParams = options.authorIds ?? [];
|
| 1628 |
return {
|
| 1629 |
+
sql: `WITH unit_ids AS MATERIALIZED (SELECT DISTINCT unit_id FROM unit_members${
|
| 1630 |
+
options.unitIds === undefined ? "" : " WHERE unit_id IN (SELECT value FROM json_each(?))"
|
| 1631 |
+
})
|
| 1632 |
SELECT um.unit_id FROM unit_ids um
|
| 1633 |
WHERE 1 = 1${cutoffSql}${authorSql}${publicationSql}`,
|
| 1634 |
+
params: [
|
| 1635 |
+
...(options.unitIds === undefined ? [] : [JSON.stringify(options.unitIds)]),
|
| 1636 |
+
...cutoffParams,
|
| 1637 |
+
...authorParams,
|
| 1638 |
+
],
|
| 1639 |
};
|
| 1640 |
}
|
| 1641 |
|
space/tests/consumer-coverage-update.test.ts
CHANGED
|
@@ -156,6 +156,36 @@ describe("source coverage maintenance", () => {
|
|
| 156 |
expect(plan.some((line) => line.includes("segment_key=? AND observation_id=?"))).toBe(false);
|
| 157 |
});
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
it("does zero body reads for twenty no-op polls and twenty harmless revisions, including restart and replay", async () => {
|
| 160 |
await f.postMany(Array.from({ length: 250 }, (_, n) => consumerTweet(String(100 + n))));
|
| 161 |
let source = await finish(BOOTSTRAP);
|
|
|
|
| 156 |
expect(plan.some((line) => line.includes("segment_key=? AND observation_id=?"))).toBe(false);
|
| 157 |
});
|
| 158 |
|
| 159 |
+
it("uses the indexed current selection for both semantic coverage clocks", async () => {
|
| 160 |
+
await f.postMany([
|
| 161 |
+
consumerTweet("100"),
|
| 162 |
+
consumerTweet("200", {
|
| 163 |
+
conversation_id: "100",
|
| 164 |
+
captured_at: "2026-09-06T05:00:00.000Z",
|
| 165 |
+
is_subscriber_only: "yes",
|
| 166 |
+
}),
|
| 167 |
+
consumerTweet("300", {
|
| 168 |
+
conversation_id: "100",
|
| 169 |
+
captured_at: "2026-09-06T04:00:00.000Z",
|
| 170 |
+
}),
|
| 171 |
+
consumerTweet("400", { captured_at: "2026-09-06T01:00:00.000Z" }),
|
| 172 |
+
consumerTweet("500", { author: { id: "22", username: "other" } }),
|
| 173 |
+
]);
|
| 174 |
+
const source = await finish(BOOTSTRAP);
|
| 175 |
+
expect(source.observations_through).toBe("2026-09-06T01:00:00.000Z");
|
| 176 |
+
const pinned = await context(source.cursor);
|
| 177 |
+
const database = f.index.store.database;
|
| 178 |
+
const plan = consumerQueryPlan(
|
| 179 |
+
database,
|
| 180 |
+
/WITH candidates AS MATERIALIZED|AS latest FROM selected_posts/u,
|
| 181 |
+
);
|
| 182 |
+
const coverage = updateConsumerCoverage(database, pinned);
|
| 183 |
+
expect(coverage.completeThrough).toBe(source.complete_through);
|
| 184 |
+
expect(coverage.observationsThrough).toBe(source.observations_through);
|
| 185 |
+
expect(plan.some((line) => line.includes("idx_consumer_post_units_author"))).toBe(true);
|
| 186 |
+
expect(plan.some((line) => line.includes("observation_sources"))).toBe(false);
|
| 187 |
+
});
|
| 188 |
+
|
| 189 |
it("does zero body reads for twenty no-op polls and twenty harmless revisions, including restart and replay", async () => {
|
| 190 |
await f.postMany(Array.from({ length: 250 }, (_, n) => consumerTweet(String(100 + n))));
|
| 191 |
let source = await finish(BOOTSTRAP);
|