This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// The dependencies every use case of the module takes, and how a proposal becomes a line a reviewer
|
||||
// sees. A line is not a row of this module until it is approved: until then it is a proposal of the
|
||||
// proposals module, and this file is the one place the two are read together.
|
||||
|
||||
import type { TenantScope } from "@conusai-kit/core"
|
||||
import type { DataSource, Filter } from "@conusai-kit/data"
|
||||
import { type FilesDeps, sha256Hex } from "@conusai-kit/files"
|
||||
import { type ProposalRow, type ProposalsDeps, type ProposalView, proposals, viewOf } from "@conusai-kit/proposals"
|
||||
import { LINE_TARGET_KEY, type LineView, lineChangeSchema } from "../contract/index.js"
|
||||
|
||||
/** A project scope: the only level a document can live at. */
|
||||
export type DocumentScope = TenantScope & { workspaceId: string; projectId: string }
|
||||
|
||||
export interface DocumentsDeps {
|
||||
readonly data: DataSource
|
||||
/** The files module's ports: a document is made from one of its files. */
|
||||
readonly files: FilesDeps
|
||||
/** The proposals module's ports: an extracted line is one of its proposals. */
|
||||
readonly proposals: ProposalsDeps
|
||||
}
|
||||
|
||||
/** The page size used to read this module's proposals. */
|
||||
const PAGE = 200
|
||||
|
||||
/**
|
||||
* Every proposal of this module's target matching the given record filter, any status, oldest
|
||||
* first.
|
||||
*
|
||||
* The proposals module's own `listProposals` filters by status alone, so the filter by target is
|
||||
* done here on its table, through the entity it exports: the `where` runs against the table's
|
||||
* `(targetKey, targetRecordId)` index instead of paging the project's whole queue into memory and
|
||||
* filtering it in JavaScript (one canonical copy of a line's state still — this adds a query, not
|
||||
* a table). When the kit's list grows a `targetRecordId` filter of its own, this becomes a call
|
||||
* of it again.
|
||||
*/
|
||||
async function proposalsWhere(
|
||||
deps: DocumentsDeps,
|
||||
scope: DocumentScope,
|
||||
target: { recordId: string } | { recordIds: readonly string[] } | null,
|
||||
): Promise<ProposalView[]> {
|
||||
const byRecord: Filter<ProposalRow>[] =
|
||||
target === null
|
||||
? []
|
||||
: "recordId" in target
|
||||
? [{ field: "targetRecordId", op: "eq", value: target.recordId }]
|
||||
: [{ field: "targetRecordId", op: "in", values: target.recordIds }]
|
||||
const found: ProposalView[] = []
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const page = await deps.proposals.data
|
||||
.forScope(scope)
|
||||
.of(proposals)
|
||||
.list({
|
||||
where: [{ field: "targetKey", op: "eq", value: LINE_TARGET_KEY }, ...byRecord],
|
||||
order: [{ field: "createdAt", direction: "asc" }],
|
||||
limit: PAGE,
|
||||
...(cursor === undefined ? {} : { cursor }),
|
||||
})
|
||||
found.push(...page.items.map(viewOf))
|
||||
cursor = page.hasMore ? page.cursor : undefined
|
||||
} while (cursor !== undefined)
|
||||
return found
|
||||
}
|
||||
|
||||
/** Every proposal of this module's target in the project — or only one document's, when given. */
|
||||
export function lineProposals(deps: DocumentsDeps, scope: DocumentScope, documentId?: string): Promise<ProposalView[]> {
|
||||
return proposalsWhere(deps, scope, documentId === undefined ? null : { recordId: documentId })
|
||||
}
|
||||
|
||||
/**
|
||||
* This module's proposals of several documents at once, keyed by document id: the document list's
|
||||
* join, one indexed page chain instead of one pass over the queue per document.
|
||||
*/
|
||||
export async function lineProposalsByDocument(
|
||||
deps: DocumentsDeps,
|
||||
scope: DocumentScope,
|
||||
documentIds: readonly string[],
|
||||
): Promise<Map<string, ProposalView[]>> {
|
||||
const byDocument = new Map<string, ProposalView[]>()
|
||||
if (documentIds.length === 0) return byDocument
|
||||
for (const proposal of await proposalsWhere(deps, scope, { recordIds: documentIds })) {
|
||||
if (proposal.targetRecordId === null) continue
|
||||
const own = byDocument.get(proposal.targetRecordId)
|
||||
if (own) own.push(proposal)
|
||||
else byDocument.set(proposal.targetRecordId, [proposal])
|
||||
}
|
||||
return byDocument
|
||||
}
|
||||
|
||||
/** A stable key for the line at a position of a document: what makes an extraction safe to run twice. */
|
||||
export const lineKeyOf = (documentId: string, page: number, position: number): Promise<string> =>
|
||||
sha256Hex(new TextEncoder().encode(`documents.line:${documentId}:${page}:${position}`))
|
||||
|
||||
/** The review screen's view of a proposal. */
|
||||
export function lineViewOf(proposal: ProposalView): LineView {
|
||||
const change = lineChangeSchema.parse(proposal.change)
|
||||
return {
|
||||
lineId: proposal.proposalId,
|
||||
expectedVersion: proposal.version,
|
||||
status: proposal.status,
|
||||
label: change.label,
|
||||
value: change.value,
|
||||
page: change.page,
|
||||
quote: change.quote,
|
||||
box: proposal.evidence[0]?.box ?? null,
|
||||
confidence: change.confidence,
|
||||
verification: change.verification,
|
||||
note: change.note ?? null,
|
||||
blockId: proposal.evidence[0]?.blockId ?? null,
|
||||
edited: change.edited,
|
||||
model: proposal.model,
|
||||
promptVersion: proposal.promptVersion,
|
||||
decidedAt: proposal.decidedAt,
|
||||
}
|
||||
}
|
||||
|
||||
/** The lines a reviewer should see, in reading order, not creation order: the lines of one extraction are
|
||||
* created within a millisecond of each other and their ids do not order them. */
|
||||
export function visibleLines(proposals: readonly ProposalView[]): LineView[] {
|
||||
return [...proposals]
|
||||
.sort((a, b) => lineChangeSchema.parse(a.change).position - lineChangeSchema.parse(b.change).position)
|
||||
.map(lineViewOf)
|
||||
}
|
||||
Reference in New Issue
Block a user