mirror of
https://github.com/langgenius/dify.git
synced 2026-09-02 22:36:15 +08:00
986 lines
38 KiB
TypeScript
986 lines
38 KiB
TypeScript
import {
|
|
DateTimeSchema,
|
|
type ProjectionSetFingerprintMaterial,
|
|
ProjectionSetFingerprintMaterialSchema,
|
|
PublicationGenerationIdSchema,
|
|
TenantIdSchema,
|
|
UuidSchema,
|
|
buildProjectionSetFingerprint,
|
|
stableJson,
|
|
} from "@knowledge/core";
|
|
|
|
import {
|
|
DeletionLifecycleFenceActiveError,
|
|
type DeletionLifecycleFenceGuard,
|
|
type DeletionLifecycleFenceToken,
|
|
} from "./deletion-lifecycle-fence";
|
|
import type {
|
|
DocumentCompilationAttempt,
|
|
DocumentCompilationProfileReference,
|
|
} from "./document-compilation-attempt-repository";
|
|
import type { DocumentCompilationCandidateValidator } from "./document-compilation-candidate-validator";
|
|
import type {
|
|
AdvanceDocumentCompilationExecutionInput,
|
|
DocumentCompilationExecutionContext,
|
|
} from "./document-compilation-runtime";
|
|
import type { DocumentRevisionPublicationFenceResolver } from "./logical-document-repository";
|
|
import {
|
|
ProjectionSetPublicationComponentTypes,
|
|
type ProjectionSetPublicationDocumentComponentInput,
|
|
type ProjectionSetPublicationMember,
|
|
ProjectionSetPublicationMemberAttemptFenceConflictError,
|
|
type ProjectionSetPublicationMemberRepository,
|
|
} from "./projection-publication-member-repository";
|
|
import {
|
|
DuplicateProjectionSetPublicationError,
|
|
type ProjectionSetPublication,
|
|
ProjectionSetPublicationAttemptFenceConflictError,
|
|
ProjectionSetPublicationDeletionFenceConflictError,
|
|
ProjectionSetPublicationHeadConflictError,
|
|
type ProjectionSetPublicationRepository,
|
|
type PublishProjectionSetResult,
|
|
} from "./projection-publication-repository";
|
|
|
|
export const DocumentCompilationCandidateMetadataKey = "documentCompilationCandidate";
|
|
|
|
export interface DocumentCompilationPublicationCoordinator {
|
|
composeCandidate(
|
|
input: ComposeDocumentCompilationCandidateInput,
|
|
): Promise<ComposeDocumentCompilationCandidateResult>;
|
|
evaluateAndPublishCandidate(
|
|
input: EvaluateAndPublishDocumentCompilationCandidateInput,
|
|
): Promise<EvaluateAndPublishDocumentCompilationCandidateResult>;
|
|
}
|
|
|
|
export interface DocumentCompilationPublicationCoordinatorOptions {
|
|
readonly deletionFence?: DeletionLifecycleFenceGuard | undefined;
|
|
readonly maxComponents: number;
|
|
readonly logicalDocumentFences?: DocumentRevisionPublicationFenceResolver | undefined;
|
|
readonly members: Pick<
|
|
ProjectionSetPublicationMemberRepository,
|
|
"composeDocumentCandidate" | "listByFingerprint"
|
|
>;
|
|
readonly publications: Pick<
|
|
ProjectionSetPublicationRepository,
|
|
| "createCandidate"
|
|
| "deactivate"
|
|
| "getByFingerprint"
|
|
| "getPublished"
|
|
| "publishDocumentCompilationCandidate"
|
|
>;
|
|
readonly validator: DocumentCompilationCandidateValidator;
|
|
}
|
|
|
|
export interface DocumentCompilationCandidateEvaluationSnapshot {
|
|
readonly compilationAttemptId?: string | undefined;
|
|
readonly candidateFingerprint: string;
|
|
readonly candidatePublicationId: string;
|
|
readonly documentAssetId: string;
|
|
readonly documentVersion: number;
|
|
readonly embeddingProfile?: DocumentCompilationProfileReference | undefined;
|
|
readonly expectedHeadRevision: number;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly members: readonly ProjectionSetPublicationMember[];
|
|
readonly publicationGenerationId: string;
|
|
readonly retrievalProfile?: DocumentCompilationProfileReference | undefined;
|
|
readonly tenantId: string;
|
|
}
|
|
|
|
export type DocumentCompilationCandidateEvaluationResult =
|
|
| { readonly decision: "passed" }
|
|
| { readonly decision: "failed"; readonly reason: string };
|
|
|
|
/** Candidate evaluators receive an exact member snapshot and have no unscoped fallback input. */
|
|
export interface DocumentCompilationCandidateEvaluator {
|
|
evaluate(
|
|
snapshot: DocumentCompilationCandidateEvaluationSnapshot,
|
|
): Promise<DocumentCompilationCandidateEvaluationResult>;
|
|
}
|
|
|
|
export interface EvaluateAndPublishDocumentCompilationCandidateInput {
|
|
readonly evaluator: DocumentCompilationCandidateEvaluator;
|
|
readonly execution: Pick<
|
|
DocumentCompilationExecutionContext,
|
|
"advance" | "attempt" | "heartbeat" | "signal" | "withLeaseSnapshot"
|
|
>;
|
|
readonly updatedAt: string;
|
|
}
|
|
|
|
export interface EvaluateAndPublishDocumentCompilationCandidateResult {
|
|
readonly attempt: DocumentCompilationAttempt;
|
|
readonly evaluation: "passed" | "previously-passed";
|
|
readonly publication: PublishProjectionSetResult;
|
|
}
|
|
|
|
export interface ComposeDocumentCompilationCandidateInput {
|
|
readonly candidateId: string;
|
|
readonly componentReceipt: DocumentCompilationCandidateComponentReceipt;
|
|
readonly createdAt: string;
|
|
readonly execution: Pick<
|
|
DocumentCompilationExecutionContext,
|
|
"advance" | "attempt" | "heartbeat" | "signal" | "withLeaseSnapshot"
|
|
>;
|
|
readonly fingerprintMaterial: ProjectionSetFingerprintMaterial;
|
|
readonly metadata?: Readonly<Record<string, unknown>> | undefined;
|
|
readonly projectionVersion: number;
|
|
}
|
|
|
|
export interface DocumentCompilationCandidateComponentReference {
|
|
readonly componentKey: string;
|
|
readonly generationId: string;
|
|
}
|
|
|
|
/** Every key is required so an omitted builder receipt cannot be mistaken for an empty result. */
|
|
export interface DocumentCompilationCandidateComponentReceipt {
|
|
readonly documentOutlines: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly graphEntities: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly graphRelations: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly indexProjections: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly knowledgePaths: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly multimodalManifests: readonly DocumentCompilationCandidateComponentReference[];
|
|
readonly schemaVersion: 1;
|
|
}
|
|
|
|
export interface ComposeDocumentCompilationCandidateResult {
|
|
readonly attempt: DocumentCompilationAttempt;
|
|
readonly candidate: ProjectionSetPublication;
|
|
readonly inheritedMemberCount: number;
|
|
readonly replacedMemberCount: number;
|
|
}
|
|
|
|
export class DocumentCompilationCandidateIdentityConflictError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "DocumentCompilationCandidateIdentityConflictError";
|
|
}
|
|
}
|
|
|
|
export class DocumentCompilationCandidateComponentError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "DocumentCompilationCandidateComponentError";
|
|
}
|
|
}
|
|
|
|
export class DocumentCompilationCandidateLeaseLostError extends Error {
|
|
constructor() {
|
|
super("Document compilation candidate composition lost its execution fence");
|
|
this.name = "DocumentCompilationCandidateLeaseLostError";
|
|
}
|
|
}
|
|
|
|
export class DocumentCompilationCandidateEvaluationError extends Error {
|
|
constructor(reason: string, options?: ErrorOptions) {
|
|
super(`Document compilation candidate evaluation failed: ${reason}`, options);
|
|
this.name = "DocumentCompilationCandidateEvaluationError";
|
|
}
|
|
}
|
|
|
|
const receiptComponentFields = [
|
|
["indexProjections", ProjectionSetPublicationComponentTypes[0]],
|
|
["documentOutlines", ProjectionSetPublicationComponentTypes[1]],
|
|
["multimodalManifests", ProjectionSetPublicationComponentTypes[2]],
|
|
["knowledgePaths", ProjectionSetPublicationComponentTypes[3]],
|
|
["graphEntities", ProjectionSetPublicationComponentTypes[4]],
|
|
["graphRelations", ProjectionSetPublicationComponentTypes[5]],
|
|
] as const;
|
|
const receiptKeys = new Set<string>([
|
|
"schemaVersion",
|
|
...receiptComponentFields.map(([field]) => field),
|
|
]);
|
|
|
|
/**
|
|
* Composes the immutable member snapshot owned by one durable attempt. The candidate is bound to
|
|
* the attempt before member writes, and the member repository performs inherit + complete owner
|
|
* replacement as one transaction-local operation. A retry reuses only the exact same candidate.
|
|
*/
|
|
export function createDocumentCompilationPublicationCoordinator({
|
|
deletionFence,
|
|
logicalDocumentFences,
|
|
maxComponents,
|
|
members,
|
|
publications,
|
|
validator,
|
|
}: DocumentCompilationPublicationCoordinatorOptions): DocumentCompilationPublicationCoordinator {
|
|
positiveInteger(maxComponents, "maxComponents");
|
|
|
|
return {
|
|
composeCandidate: async (input) => {
|
|
assertExecutionFence(input.execution);
|
|
const initialAttempt = validateAttempt(input.execution.attempt);
|
|
const deletionToken = await captureCompilationDeletionFence(deletionFence, initialAttempt);
|
|
const assertWritable = () => assertCompilationDeletionFence(deletionFence, deletionToken);
|
|
const proposedCandidateId = normalizeUuid(input.candidateId);
|
|
const candidateId = initialAttempt.candidatePublicationId
|
|
? normalizeUuid(initialAttempt.candidatePublicationId)
|
|
: proposedCandidateId;
|
|
const createdAt = DateTimeSchema.parse(input.createdAt);
|
|
const projectionVersion = positiveInteger(input.projectionVersion, "projectionVersion");
|
|
const fingerprintMaterial = ProjectionSetFingerprintMaterialSchema.parse(
|
|
input.fingerprintMaterial,
|
|
);
|
|
validateFingerprintMaterial(fingerprintMaterial, initialAttempt);
|
|
const fingerprint = await buildProjectionSetFingerprint(fingerprintMaterial);
|
|
const components = flattenComponentReceipt(
|
|
input.componentReceipt,
|
|
initialAttempt,
|
|
maxComponents,
|
|
);
|
|
assertAttemptCandidateBinding(initialAttempt, candidateId, fingerprint);
|
|
const metadata = candidateMetadata(initialAttempt, input.metadata);
|
|
|
|
await assertWritable();
|
|
await input.execution.heartbeat();
|
|
assertExecutionFence(input.execution);
|
|
await validator.validate({
|
|
attempt: initialAttempt,
|
|
components,
|
|
fingerprintMaterial,
|
|
});
|
|
await assertWritable();
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
await input.execution.heartbeat();
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
const candidate = await ensureExclusiveCandidate(publications, {
|
|
createdAt,
|
|
fingerprint,
|
|
id: candidateId,
|
|
knowledgeSpaceId: initialAttempt.knowledgeSpaceId,
|
|
metadata,
|
|
projectionVersion,
|
|
tenantId: initialAttempt.tenantId,
|
|
});
|
|
const reusesCurrentPublication = await isCurrentPublishedSnapshot(publications, candidate, {
|
|
attempt: initialAttempt,
|
|
fingerprint,
|
|
projectionVersion,
|
|
});
|
|
if (!reusesCurrentPublication) {
|
|
assertCandidateIdentity(candidate, {
|
|
attempt: initialAttempt,
|
|
candidateId: proposedCandidateId,
|
|
fingerprint,
|
|
projectionVersion,
|
|
});
|
|
}
|
|
|
|
let attempt = validateAttempt(input.execution.attempt);
|
|
assertSameAttemptScope(attempt, initialAttempt);
|
|
if (!attempt.candidatePublicationId) {
|
|
await assertWritable();
|
|
attempt = await bindCandidate(input.execution, attempt, candidate.id, fingerprint);
|
|
} else {
|
|
assertAttemptCandidateBinding(attempt, candidate.id, fingerprint);
|
|
}
|
|
|
|
if (reusesCurrentPublication) {
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
attempt = validateAttempt(await input.execution.heartbeat());
|
|
assertSameAttemptScope(attempt, initialAttempt);
|
|
assertAttemptCandidateBinding(attempt, candidate.id, fingerprint);
|
|
if (attempt.checkpoint === "nodes_generated") {
|
|
attempt = await input.execution.advance({
|
|
candidateFingerprint: fingerprint,
|
|
candidatePublicationId: candidate.id,
|
|
checkpoint: "projection_built",
|
|
});
|
|
}
|
|
return {
|
|
attempt,
|
|
candidate,
|
|
inheritedMemberCount: 0,
|
|
replacedMemberCount: 0,
|
|
};
|
|
}
|
|
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
attempt = validateAttempt(await input.execution.heartbeat());
|
|
assertSameAttemptScope(attempt, initialAttempt);
|
|
assertAttemptCandidateBinding(attempt, candidateId, fingerprint);
|
|
assertExecutionFence(input.execution);
|
|
let composition: Awaited<
|
|
ReturnType<ProjectionSetPublicationMemberRepository["composeDocumentCandidate"]>
|
|
>;
|
|
try {
|
|
composition = await input.execution.withLeaseSnapshot(async (leaseSnapshot) => {
|
|
const fencedAttempt = validateAttempt(leaseSnapshot);
|
|
assertSameAttemptScope(fencedAttempt, initialAttempt);
|
|
assertAttemptCandidateBinding(fencedAttempt, candidateId, fingerprint);
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
return members.composeDocumentCandidate({
|
|
attemptFence: compilationAttemptFence(fencedAttempt, candidateId, fingerprint),
|
|
candidateFingerprint: fingerprint,
|
|
components,
|
|
createdAt,
|
|
documentAssetId: initialAttempt.documentAssetId,
|
|
expectedHeadRevision: initialAttempt.baseHeadRevision,
|
|
knowledgeSpaceId: initialAttempt.knowledgeSpaceId,
|
|
tenantId: initialAttempt.tenantId,
|
|
});
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ProjectionSetPublicationMemberAttemptFenceConflictError) {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
throw error;
|
|
}
|
|
if (composition.replaced !== components.length) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate member replacement count mismatch: expected=${components.length} actual=${composition.replaced}`,
|
|
);
|
|
}
|
|
|
|
assertExecutionFence(input.execution);
|
|
await assertWritable();
|
|
await input.execution.heartbeat();
|
|
attempt = validateAttempt(input.execution.attempt);
|
|
assertSameAttemptScope(attempt, initialAttempt);
|
|
assertAttemptCandidateBinding(attempt, candidateId, fingerprint);
|
|
if (attempt.checkpoint === "nodes_generated") {
|
|
await assertWritable();
|
|
attempt = await input.execution.advance({
|
|
candidateFingerprint: fingerprint,
|
|
candidatePublicationId: candidateId,
|
|
checkpoint: "projection_built",
|
|
});
|
|
}
|
|
|
|
return {
|
|
attempt,
|
|
candidate,
|
|
inheritedMemberCount: composition.inherited,
|
|
replacedMemberCount: composition.replaced,
|
|
};
|
|
},
|
|
evaluateAndPublishCandidate: async ({ evaluator, execution, updatedAt: rawUpdatedAt }) => {
|
|
assertExecutionFence(execution);
|
|
const updatedAt = DateTimeSchema.parse(rawUpdatedAt);
|
|
const initialAttempt = validatePublicationAttempt(execution.attempt);
|
|
const deletionToken = await captureCompilationDeletionFence(deletionFence, initialAttempt);
|
|
const assertWritable = () => assertCompilationDeletionFence(deletionFence, deletionToken);
|
|
const candidatePublicationId = requireBoundCandidateId(initialAttempt);
|
|
const candidateFingerprint = requireBoundCandidateFingerprint(initialAttempt);
|
|
const lookup = {
|
|
fingerprint: candidateFingerprint,
|
|
knowledgeSpaceId: initialAttempt.knowledgeSpaceId,
|
|
tenantId: initialAttempt.tenantId,
|
|
};
|
|
const candidate = await publications.getByFingerprint(lookup);
|
|
if (!candidate) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation candidate publication was not found",
|
|
);
|
|
}
|
|
const reusesCurrentPublication = await isCurrentPublishedSnapshot(publications, candidate, {
|
|
attempt: initialAttempt,
|
|
fingerprint: candidateFingerprint,
|
|
projectionVersion: candidate.projectionVersion,
|
|
});
|
|
if (!reusesCurrentPublication) {
|
|
assertCandidateIdentity(candidate, {
|
|
allowedStatuses: ["candidate", "published"],
|
|
attempt: initialAttempt,
|
|
candidateId: candidatePublicationId,
|
|
fingerprint: candidateFingerprint,
|
|
projectionVersion: candidate.projectionVersion,
|
|
});
|
|
}
|
|
|
|
try {
|
|
if (candidate.status === "published") {
|
|
const published = await publications.getPublished(lookup);
|
|
if (
|
|
!published ||
|
|
published.id !== candidatePublicationId ||
|
|
published.fingerprint !== candidateFingerprint ||
|
|
published.headRevision !==
|
|
initialAttempt.baseHeadRevision + (reusesCurrentPublication ? 0 : 1)
|
|
) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Published document compilation candidate is not the expected publication head",
|
|
);
|
|
}
|
|
const attempt =
|
|
initialAttempt.checkpoint === "projection_built"
|
|
? await (async () => {
|
|
await assertWritable();
|
|
return execution.advance({ checkpoint: "smoke_eval_passed" });
|
|
})()
|
|
: initialAttempt;
|
|
return {
|
|
attempt,
|
|
evaluation: "previously-passed",
|
|
publication: { headRevision: published.headRevision, published },
|
|
};
|
|
}
|
|
if (candidate.status !== "candidate") {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
`Document compilation candidate cannot evaluate from status=${candidate.status}`,
|
|
);
|
|
}
|
|
|
|
const candidateMembersForPublication = await members.listByFingerprint(lookup);
|
|
if (candidateMembersForPublication.length === 0) {
|
|
await assertWritable();
|
|
await deactivateCandidate(publications, lookup, updatedAt);
|
|
throw new DocumentCompilationCandidateEvaluationError(
|
|
"candidate member snapshot is empty",
|
|
);
|
|
}
|
|
|
|
let attempt = initialAttempt;
|
|
let evaluation: "passed" | "previously-passed" = "previously-passed";
|
|
if (attempt.checkpoint === "projection_built") {
|
|
let result: DocumentCompilationCandidateEvaluationResult;
|
|
try {
|
|
result = await evaluator.evaluate(
|
|
freezeEvaluationSnapshot({
|
|
candidateFingerprint,
|
|
candidatePublicationId,
|
|
compilationAttemptId: attempt.id,
|
|
documentAssetId: attempt.documentAssetId,
|
|
documentVersion: attempt.documentVersion,
|
|
...(attempt.embeddingProfile ? { embeddingProfile: attempt.embeddingProfile } : {}),
|
|
expectedHeadRevision: attempt.baseHeadRevision,
|
|
knowledgeSpaceId: attempt.knowledgeSpaceId,
|
|
members: candidateMembersForPublication,
|
|
publicationGenerationId: attempt.publicationGenerationId,
|
|
...(attempt.retrievalProfile ? { retrievalProfile: attempt.retrievalProfile } : {}),
|
|
tenantId: attempt.tenantId,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
await assertWritable();
|
|
await deactivateCandidate(publications, lookup, updatedAt);
|
|
throw new DocumentCompilationCandidateEvaluationError(
|
|
error instanceof Error ? error.message : "candidate evaluator failed",
|
|
{ cause: error },
|
|
);
|
|
}
|
|
if (result.decision === "failed") {
|
|
await assertWritable();
|
|
await deactivateCandidate(publications, lookup, updatedAt);
|
|
throw new DocumentCompilationCandidateEvaluationError(
|
|
requiredReason(result.reason, "candidate evaluation reason"),
|
|
);
|
|
}
|
|
if (result.decision !== "passed") {
|
|
await assertWritable();
|
|
await deactivateCandidate(publications, lookup, updatedAt);
|
|
throw new DocumentCompilationCandidateEvaluationError(
|
|
"candidate evaluator returned an invalid decision",
|
|
);
|
|
}
|
|
|
|
assertExecutionFence(execution);
|
|
await assertWritable();
|
|
attempt = validatePublicationAttempt(await execution.heartbeat());
|
|
assertSameAttemptScope(attempt, initialAttempt);
|
|
assertAttemptCandidateBinding(attempt, candidatePublicationId, candidateFingerprint);
|
|
await assertWritable();
|
|
attempt = validatePublicationAttempt(
|
|
await execution.advance({ checkpoint: "smoke_eval_passed" }),
|
|
);
|
|
evaluation = "passed";
|
|
}
|
|
|
|
assertExecutionFence(execution);
|
|
await assertWritable();
|
|
let publication: PublishProjectionSetResult;
|
|
try {
|
|
publication = await execution.withLeaseSnapshot(async (leaseSnapshot) => {
|
|
const fencedAttempt = validatePublicationAttempt(leaseSnapshot);
|
|
assertSameAttemptScope(fencedAttempt, initialAttempt);
|
|
assertAttemptCandidateBinding(
|
|
fencedAttempt,
|
|
candidatePublicationId,
|
|
candidateFingerprint,
|
|
);
|
|
assertExecutionFence(execution);
|
|
await assertWritable();
|
|
const logicalDocumentFence = logicalDocumentFences
|
|
? await logicalDocumentFences.resolve({
|
|
attemptId: fencedAttempt.id,
|
|
documentAssetId: fencedAttempt.documentAssetId,
|
|
documentAssetVersion: fencedAttempt.documentVersion,
|
|
knowledgeSpaceId: fencedAttempt.knowledgeSpaceId,
|
|
tenantId: fencedAttempt.tenantId,
|
|
})
|
|
: null;
|
|
return publications.publishDocumentCompilationCandidate({
|
|
attemptFence: compilationPublicationFence(
|
|
fencedAttempt,
|
|
candidatePublicationId,
|
|
candidateFingerprint,
|
|
),
|
|
expectedHeadRevision: initialAttempt.baseHeadRevision,
|
|
expectedMembers: candidateMembersForPublication,
|
|
fingerprint: candidateFingerprint,
|
|
knowledgeSpaceId: initialAttempt.knowledgeSpaceId,
|
|
...(logicalDocumentFence ? { logicalDocumentFence } : {}),
|
|
tenantId: initialAttempt.tenantId,
|
|
updatedAt,
|
|
});
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ProjectionSetPublicationAttemptFenceConflictError) {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
if (error instanceof ProjectionSetPublicationHeadConflictError) {
|
|
await assertWritable();
|
|
await deactivateCandidate(publications, lookup, updatedAt);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return { attempt, evaluation, publication };
|
|
} catch (error) {
|
|
if (
|
|
(error instanceof DeletionLifecycleFenceActiveError ||
|
|
error instanceof ProjectionSetPublicationDeletionFenceConflictError) &&
|
|
candidate.status === "candidate"
|
|
) {
|
|
await deactivateCandidate(publications, lookup, updatedAt).catch(() => undefined);
|
|
}
|
|
throw error;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
async function isCurrentPublishedSnapshot(
|
|
publications: Pick<ProjectionSetPublicationRepository, "getPublished">,
|
|
candidate: ProjectionSetPublication,
|
|
expected: {
|
|
readonly attempt: DocumentCompilationAttempt;
|
|
readonly fingerprint: string;
|
|
readonly projectionVersion: number;
|
|
},
|
|
): Promise<boolean> {
|
|
if (
|
|
candidate.status !== "published" ||
|
|
candidate.fingerprint !== expected.fingerprint ||
|
|
candidate.projectionVersion !== expected.projectionVersion
|
|
) {
|
|
return false;
|
|
}
|
|
const published = await publications.getPublished({
|
|
knowledgeSpaceId: expected.attempt.knowledgeSpaceId,
|
|
tenantId: expected.attempt.tenantId,
|
|
});
|
|
return (
|
|
published?.id === candidate.id &&
|
|
published.fingerprint === expected.fingerprint &&
|
|
published.headRevision === expected.attempt.baseHeadRevision
|
|
);
|
|
}
|
|
|
|
async function ensureExclusiveCandidate(
|
|
publications: Pick<ProjectionSetPublicationRepository, "createCandidate" | "getByFingerprint">,
|
|
input: Parameters<ProjectionSetPublicationRepository["createCandidate"]>[0],
|
|
): Promise<ProjectionSetPublication> {
|
|
const lookup = {
|
|
fingerprint: input.fingerprint,
|
|
knowledgeSpaceId: input.knowledgeSpaceId,
|
|
tenantId: input.tenantId,
|
|
};
|
|
const existing = await publications.getByFingerprint(lookup);
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
|
|
try {
|
|
return await publications.createCandidate(input);
|
|
} catch (error) {
|
|
if (!(error instanceof DuplicateProjectionSetPublicationError)) {
|
|
throw error;
|
|
}
|
|
const concurrent = await publications.getByFingerprint(lookup);
|
|
if (!concurrent) {
|
|
throw error;
|
|
}
|
|
return concurrent;
|
|
}
|
|
}
|
|
|
|
async function bindCandidate(
|
|
execution: Pick<DocumentCompilationExecutionContext, "advance">,
|
|
attempt: DocumentCompilationAttempt,
|
|
candidatePublicationId: string,
|
|
candidateFingerprint: string,
|
|
): Promise<DocumentCompilationAttempt> {
|
|
const input: AdvanceDocumentCompilationExecutionInput = {
|
|
candidateFingerprint,
|
|
candidatePublicationId,
|
|
checkpoint: attempt.checkpoint,
|
|
};
|
|
const bound = await execution.advance(input);
|
|
assertAttemptCandidateBinding(bound, candidatePublicationId, candidateFingerprint);
|
|
return bound;
|
|
}
|
|
|
|
function validateAttempt(attempt: DocumentCompilationAttempt): DocumentCompilationAttempt {
|
|
if (attempt.runState !== "running") {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
if (attempt.checkpoint !== "nodes_generated" && attempt.checkpoint !== "projection_built") {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Document compilation candidate cannot compose from checkpoint=${attempt.checkpoint}`,
|
|
);
|
|
}
|
|
TenantIdSchema.parse(attempt.tenantId);
|
|
normalizeUuid(attempt.id);
|
|
normalizeUuid(attempt.knowledgeSpaceId);
|
|
normalizeUuid(attempt.documentAssetId);
|
|
PublicationGenerationIdSchema.parse(attempt.publicationGenerationId);
|
|
nonnegativeInteger(attempt.baseHeadRevision, "baseHeadRevision");
|
|
positiveInteger(attempt.documentVersion, "documentVersion");
|
|
return attempt;
|
|
}
|
|
|
|
function validatePublicationAttempt(
|
|
attempt: DocumentCompilationAttempt,
|
|
): DocumentCompilationAttempt {
|
|
if (attempt.runState !== "running") {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
if (attempt.checkpoint !== "projection_built" && attempt.checkpoint !== "smoke_eval_passed") {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Document compilation candidate cannot publish from checkpoint=${attempt.checkpoint}`,
|
|
);
|
|
}
|
|
TenantIdSchema.parse(attempt.tenantId);
|
|
normalizeUuid(attempt.id);
|
|
normalizeUuid(attempt.knowledgeSpaceId);
|
|
normalizeUuid(attempt.documentAssetId);
|
|
PublicationGenerationIdSchema.parse(attempt.publicationGenerationId);
|
|
nonnegativeInteger(attempt.baseHeadRevision, "baseHeadRevision");
|
|
positiveInteger(attempt.documentVersion, "documentVersion");
|
|
nonnegativeInteger(attempt.rowVersion, "rowVersion");
|
|
return attempt;
|
|
}
|
|
|
|
function requireBoundCandidateId(attempt: DocumentCompilationAttempt): string {
|
|
if (!attempt.candidatePublicationId) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation attempt has no candidate publication id",
|
|
);
|
|
}
|
|
return normalizeUuid(attempt.candidatePublicationId);
|
|
}
|
|
|
|
function requireBoundCandidateFingerprint(attempt: DocumentCompilationAttempt): string {
|
|
if (!attempt.candidateFingerprint) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation attempt has no candidate fingerprint",
|
|
);
|
|
}
|
|
return attempt.candidateFingerprint;
|
|
}
|
|
|
|
function validateFingerprintMaterial(
|
|
material: ProjectionSetFingerprintMaterial,
|
|
attempt: DocumentCompilationAttempt,
|
|
): void {
|
|
if (normalizeUuid(material.knowledgeSpaceId) !== normalizeUuid(attempt.knowledgeSpaceId)) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Projection fingerprint material belongs to another knowledge space",
|
|
);
|
|
}
|
|
const ownerSnapshots = material.sourceSnapshots.filter(
|
|
(snapshot) =>
|
|
normalizeUuid(snapshot.documentAssetId) === normalizeUuid(attempt.documentAssetId),
|
|
);
|
|
if (ownerSnapshots.length !== 1 || ownerSnapshots[0]?.version !== attempt.documentVersion) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Projection fingerprint material must contain exactly the attempt owner document version",
|
|
);
|
|
}
|
|
}
|
|
|
|
function flattenComponentReceipt(
|
|
receipt: DocumentCompilationCandidateComponentReceipt,
|
|
attempt: DocumentCompilationAttempt,
|
|
maxComponents: number,
|
|
): readonly ProjectionSetPublicationDocumentComponentInput[] {
|
|
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
"Candidate component receipt must be an object",
|
|
);
|
|
}
|
|
const actualKeys = Object.keys(receipt);
|
|
if (
|
|
actualKeys.length !== receiptKeys.size ||
|
|
actualKeys.some((key) => !receiptKeys.has(key)) ||
|
|
receipt.schemaVersion !== 1
|
|
) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
"Candidate component receipt must contain schemaVersion=1 and all six component arrays",
|
|
);
|
|
}
|
|
|
|
const expectedGeneration = PublicationGenerationIdSchema.parse(attempt.publicationGenerationId);
|
|
const identities = new Set<string>();
|
|
const components: ProjectionSetPublicationDocumentComponentInput[] = [];
|
|
|
|
for (const [field, componentType] of receiptComponentFields) {
|
|
const references = receipt[field];
|
|
if (!Array.isArray(references)) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component receipt ${field} must be an array`,
|
|
);
|
|
}
|
|
if (components.length + references.length > maxComponents) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component receipt exceeds maxComponents=${maxComponents}`,
|
|
);
|
|
}
|
|
|
|
for (const reference of references) {
|
|
if (!reference || typeof reference !== "object" || Array.isArray(reference)) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component receipt ${field} contains an invalid reference`,
|
|
);
|
|
}
|
|
const referenceKeys = Object.keys(reference);
|
|
if (
|
|
referenceKeys.length !== 2 ||
|
|
!referenceKeys.includes("componentKey") ||
|
|
!referenceKeys.includes("generationId")
|
|
) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component receipt ${field} reference must contain only componentKey and generationId`,
|
|
);
|
|
}
|
|
const componentKey = normalizeUuid(reference.componentKey);
|
|
const generationId = PublicationGenerationIdSchema.parse(reference.generationId);
|
|
if (generationId !== expectedGeneration) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component=${componentKey} generation does not match its attempt`,
|
|
);
|
|
}
|
|
const identity = `${componentType}:${componentKey}`;
|
|
if (identities.has(identity)) {
|
|
throw new DocumentCompilationCandidateComponentError(
|
|
`Candidate component identity is duplicated: ${identity}`,
|
|
);
|
|
}
|
|
identities.add(identity);
|
|
components.push({ componentKey, componentType, generationId });
|
|
}
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
function candidateMetadata(
|
|
attempt: DocumentCompilationAttempt,
|
|
metadata: Readonly<Record<string, unknown>> | undefined,
|
|
): Record<string, unknown> {
|
|
return {
|
|
...(metadata ?? {}),
|
|
[DocumentCompilationCandidateMetadataKey]: {
|
|
attemptId: normalizeUuid(attempt.id),
|
|
baseHeadRevision: attempt.baseHeadRevision,
|
|
ownerDocumentAssetId: normalizeUuid(attempt.documentAssetId),
|
|
ownerDocumentVersion: attempt.documentVersion,
|
|
publicationGenerationId: PublicationGenerationIdSchema.parse(attempt.publicationGenerationId),
|
|
...(attempt.embeddingProfile ? { embeddingProfile: attempt.embeddingProfile } : {}),
|
|
...(attempt.retrievalProfile ? { retrievalProfile: attempt.retrievalProfile } : {}),
|
|
schemaVersion: 1,
|
|
},
|
|
};
|
|
}
|
|
|
|
function compilationAttemptFence(
|
|
attempt: DocumentCompilationAttempt,
|
|
candidatePublicationId: string,
|
|
candidateFingerprint: string,
|
|
) {
|
|
assertAttemptCandidateBinding(attempt, candidatePublicationId, candidateFingerprint);
|
|
if (!attempt.leaseToken) {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
return {
|
|
attemptId: normalizeUuid(attempt.id),
|
|
candidatePublicationId,
|
|
documentVersion: positiveInteger(attempt.documentVersion, "documentVersion"),
|
|
expectedRowVersion: nonnegativeInteger(attempt.rowVersion, "rowVersion"),
|
|
leaseToken: normalizeUuid(attempt.leaseToken),
|
|
publicationGenerationId: PublicationGenerationIdSchema.parse(attempt.publicationGenerationId),
|
|
};
|
|
}
|
|
|
|
function compilationPublicationFence(
|
|
attempt: DocumentCompilationAttempt,
|
|
candidatePublicationId: string,
|
|
candidateFingerprint: string,
|
|
) {
|
|
const fence = compilationAttemptFence(attempt, candidatePublicationId, candidateFingerprint);
|
|
return {
|
|
...fence,
|
|
documentAssetId: normalizeUuid(attempt.documentAssetId),
|
|
};
|
|
}
|
|
|
|
function freezeEvaluationSnapshot(
|
|
snapshot: DocumentCompilationCandidateEvaluationSnapshot,
|
|
): DocumentCompilationCandidateEvaluationSnapshot {
|
|
const members = snapshot.members.map((member) => Object.freeze({ ...member }));
|
|
return Object.freeze({
|
|
...snapshot,
|
|
...(snapshot.embeddingProfile
|
|
? { embeddingProfile: Object.freeze({ ...snapshot.embeddingProfile }) }
|
|
: {}),
|
|
members: Object.freeze(members),
|
|
...(snapshot.retrievalProfile
|
|
? { retrievalProfile: Object.freeze({ ...snapshot.retrievalProfile }) }
|
|
: {}),
|
|
});
|
|
}
|
|
|
|
async function deactivateCandidate(
|
|
publications: Pick<ProjectionSetPublicationRepository, "deactivate">,
|
|
lookup: {
|
|
readonly fingerprint: string;
|
|
readonly knowledgeSpaceId: string;
|
|
readonly tenantId: string;
|
|
},
|
|
updatedAt: string,
|
|
): Promise<void> {
|
|
await publications.deactivate({ ...lookup, updatedAt });
|
|
}
|
|
|
|
function requiredReason(value: string, label: string): string {
|
|
const normalized = value.trim();
|
|
if (!normalized) {
|
|
throw new DocumentCompilationCandidateEvaluationError(`${label} must not be empty`);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function assertCandidateIdentity(
|
|
candidate: ProjectionSetPublication,
|
|
expected: {
|
|
readonly allowedStatuses?: readonly ProjectionSetPublication["status"][] | undefined;
|
|
readonly attempt: DocumentCompilationAttempt;
|
|
readonly candidateId: string;
|
|
readonly fingerprint: string;
|
|
readonly projectionVersion: number;
|
|
},
|
|
): void {
|
|
const identity = candidate.metadata[DocumentCompilationCandidateMetadataKey];
|
|
const expectedIdentity = candidateMetadata(expected.attempt, {})[
|
|
DocumentCompilationCandidateMetadataKey
|
|
];
|
|
if (
|
|
normalizeUuid(candidate.id) !== expected.candidateId ||
|
|
candidate.fingerprint !== expected.fingerprint ||
|
|
normalizeUuid(candidate.knowledgeSpaceId) !==
|
|
normalizeUuid(expected.attempt.knowledgeSpaceId) ||
|
|
candidate.tenantId !== expected.attempt.tenantId ||
|
|
candidate.projectionVersion !== expected.projectionVersion ||
|
|
!(expected.allowedStatuses ?? ["candidate"]).includes(candidate.status) ||
|
|
stableJson(identity) !== stableJson(expectedIdentity)
|
|
) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Projection candidate is not exclusively owned by this compilation attempt",
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertAttemptCandidateBinding(
|
|
attempt: Pick<DocumentCompilationAttempt, "candidateFingerprint" | "candidatePublicationId">,
|
|
candidatePublicationId: string,
|
|
candidateFingerprint: string,
|
|
): void {
|
|
const hasId = attempt.candidatePublicationId !== undefined;
|
|
const hasFingerprint = attempt.candidateFingerprint !== undefined;
|
|
if (hasId !== hasFingerprint) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation attempt has a partial candidate binding",
|
|
);
|
|
}
|
|
if (
|
|
(attempt.candidatePublicationId &&
|
|
normalizeUuid(attempt.candidatePublicationId) !== candidatePublicationId) ||
|
|
(attempt.candidateFingerprint && attempt.candidateFingerprint !== candidateFingerprint)
|
|
) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation attempt is already bound to another candidate",
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertSameAttemptScope(
|
|
current: DocumentCompilationAttempt,
|
|
initial: DocumentCompilationAttempt,
|
|
): void {
|
|
if (
|
|
normalizeUuid(current.id) !== normalizeUuid(initial.id) ||
|
|
normalizeUuid(current.knowledgeSpaceId) !== normalizeUuid(initial.knowledgeSpaceId) ||
|
|
normalizeUuid(current.documentAssetId) !== normalizeUuid(initial.documentAssetId) ||
|
|
current.documentVersion !== initial.documentVersion ||
|
|
current.tenantId !== initial.tenantId ||
|
|
current.baseHeadRevision !== initial.baseHeadRevision ||
|
|
current.publicationGenerationId !== initial.publicationGenerationId ||
|
|
stableJson(current.embeddingProfile ?? null) !== stableJson(initial.embeddingProfile ?? null) ||
|
|
stableJson(current.retrievalProfile ?? null) !== stableJson(initial.retrievalProfile ?? null)
|
|
) {
|
|
throw new DocumentCompilationCandidateIdentityConflictError(
|
|
"Document compilation execution scope changed during candidate composition",
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertExecutionFence(
|
|
execution: Pick<DocumentCompilationExecutionContext, "signal">,
|
|
): void {
|
|
if (execution.signal.aborted) {
|
|
throw new DocumentCompilationCandidateLeaseLostError();
|
|
}
|
|
}
|
|
|
|
async function captureCompilationDeletionFence(
|
|
guard: DeletionLifecycleFenceGuard | undefined,
|
|
attempt: DocumentCompilationAttempt,
|
|
): Promise<DeletionLifecycleFenceToken | undefined> {
|
|
return guard?.captureDeletionFence({
|
|
documentAssetId: attempt.documentAssetId,
|
|
knowledgeSpaceId: attempt.knowledgeSpaceId,
|
|
tenantId: attempt.tenantId,
|
|
});
|
|
}
|
|
|
|
async function assertCompilationDeletionFence(
|
|
guard: DeletionLifecycleFenceGuard | undefined,
|
|
token: DeletionLifecycleFenceToken | undefined,
|
|
): Promise<void> {
|
|
if (token) {
|
|
await guard?.assertDeletionFenceUnchanged(token);
|
|
}
|
|
}
|
|
|
|
function normalizeUuid(value: string): string {
|
|
return UuidSchema.parse(value).toLowerCase();
|
|
}
|
|
|
|
function positiveInteger(value: number, field: string): number {
|
|
if (!Number.isSafeInteger(value) || value < 1) {
|
|
throw new Error(`Document compilation candidate ${field} must be a positive integer`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function nonnegativeInteger(value: number, field: string): number {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new Error(`Document compilation candidate ${field} must be a non-negative integer`);
|
|
}
|
|
return value;
|
|
}
|