@inflexa-ai/harness 0.35.8 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/types.d.ts +5 -14
- package/dist/auth/types.js +3 -10
- package/dist/billing/usage-recorder.d.ts +1 -1
- package/dist/contracts/analogy-report.d.ts +1 -1
- package/dist/contracts/analogy-report.js +1 -1
- package/dist/contracts/chat-parts.d.ts +2 -3
- package/dist/contracts/index.d.ts +0 -4
- package/dist/contracts/index.js +0 -2
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -21
- package/dist/lib/metrics.d.ts +1 -1
- package/dist/lib/otel-spans.d.ts +0 -2
- package/dist/lib/otel-spans.js +0 -2
- package/dist/loop/genai-spans.d.ts +14 -0
- package/dist/loop/genai-spans.js +35 -0
- package/dist/loop/run-agent.d.ts +1 -1
- package/dist/loop/run-agent.js +37 -4
- package/dist/runtime/assemble.d.ts +0 -3
- package/dist/runtime/assemble.js +0 -3
- package/dist/state/index.d.ts +0 -2
- package/dist/state/index.js +0 -1
- package/dist/state/init.js +1 -60
- package/dist/state/purge-analysis.d.ts +1 -2
- package/dist/state/purge-analysis.js +1 -2
- package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
- package/dist/tools/bio/gene-preclinical-profile.js +1 -3
- package/dist/tools/lib/chembl-client.d.ts +1 -4
- package/dist/tools/lib/chembl-client.js +1 -4
- package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
- package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
- package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
- package/dist/tools/lib/clinical-trials-client.js +0 -3
- package/dist/tools/lib/identifier-resolver.d.ts +5 -7
- package/dist/tools/lib/identifier-resolver.js +5 -7
- package/dist/tools/lib/iuphar-client.d.ts +1 -3
- package/dist/tools/lib/iuphar-client.js +1 -3
- package/dist/tools/lib/opentargets-client.d.ts +1 -1
- package/dist/tools/lib/opentargets-client.js +1 -1
- package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
- package/dist/tools/lib/protein-family-normalize.js +5 -7
- package/dist/tools/lib/uniprot-client.d.ts +0 -7
- package/dist/tools/lib/uniprot-client.js +2 -9
- package/package.json +1 -1
- package/dist/contracts/target-assessment-row.d.ts +0 -85
- package/dist/contracts/target-assessment-row.js +0 -37
- package/dist/contracts/target-dossier.d.ts +0 -12423
- package/dist/contracts/target-dossier.js +0 -1282
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
- package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
- package/dist/prompts/target-assessment/briefs/index.js +0 -4
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
- package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
- package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
- package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
- package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
- package/dist/prompts/target-assessment/investigation/index.js +0 -3
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
- package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
- package/dist/prompts/target-assessment/modulator-triage.js +0 -31
- package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
- package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
- package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
- package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
- package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
- package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
- package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
- package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
- package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
- package/dist/state/target-assessments.d.ts +0 -68
- package/dist/state/target-assessments.js +0 -222
- package/dist/workflows/execute-target-assessment.d.ts +0 -118
- package/dist/workflows/execute-target-assessment.js +0 -631
- package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
- package/dist/workflows/target-assessment/assemblers/index.js +0 -4
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
- package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
- package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
- package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
- package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
- package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
- package/dist/workflows/target-assessment/collectors/index.js +0 -538
- package/dist/workflows/target-assessment/coverage.d.ts +0 -97
- package/dist/workflows/target-assessment/coverage.js +0 -86
- package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
- package/dist/workflows/target-assessment/decisions/index.js +0 -127
- package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
- package/dist/workflows/target-assessment/fanout/index.js +0 -156
- package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
- package/dist/workflows/target-assessment/investigation/index.js +0 -419
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
- package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
- package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
- package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
- package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
- package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
- package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
- package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
- package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
- package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
- package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
- package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
- package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
- package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
- package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
- package/dist/workflows/target-assessment/metrics.d.ts +0 -16
- package/dist/workflows/target-assessment/metrics.js +0 -30
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
- package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
- package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
- package/dist/workflows/target-assessment/phase5-persist.js +0 -349
- package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
- package/dist/workflows/target-assessment/progress-stream.js +0 -110
- package/dist/workflows/target-assessment/progress.d.ts +0 -39
- package/dist/workflows/target-assessment/progress.js +0 -77
- package/dist/workflows/target-assessment/schemas.d.ts +0 -970
- package/dist/workflows/target-assessment/schemas.js +0 -318
- package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
- package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
- package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
- package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
- package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
- package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
- package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
- package/dist/workflows/target-assessment/synthesis/index.js +0 -354
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
- package/dist/workflows/target-assessment/synthesis-probes.js +0 -320
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 4 — dossier assembly.
|
|
3
|
-
*
|
|
4
|
-
* Calls `assembleDossier` (~3.5K LOC of aggregation over the Phase-3 bundle)
|
|
5
|
-
* and returns the assembled `DossierBody`. The safety-corroboration and
|
|
6
|
-
* claim-investigation sections are stamped by the workflow body between this
|
|
7
|
-
* step and synthesis; the Phase-5 synthesis sections (translational commentary,
|
|
8
|
-
* liability bullets, safety-flags trail) are stamped by the Phase-5 persist
|
|
9
|
-
* step.
|
|
10
|
-
*
|
|
11
|
-
* The DBOS workflow body wraps this in `DBOS.runStep({name: "phase4-assemble"})`
|
|
12
|
-
* so recovery on a fresh replica replays the cached output without re-running
|
|
13
|
-
* the compute. Replay caching earns its keep here because `annotatorDeps`
|
|
14
|
-
* carries the per-row clinical-consequence annotator's LLM seam: assembly makes
|
|
15
|
-
* bounded model calls for off-target rows that have no cached annotation. Those
|
|
16
|
-
* calls are per-row best-effort — a failure leaves the row's original fallback —
|
|
17
|
-
* and their results are cached in Postgres, so a repeat assessment of the same
|
|
18
|
-
* target reaches them without an LLM call at all.
|
|
19
|
-
*/
|
|
20
|
-
import { DossierBodySchema } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
21
|
-
import { z } from "zod";
|
|
22
|
-
import { assembleDossier } from "./assemblers/index.js";
|
|
23
|
-
import { Phase3BundleSchema } from "./steps/phase3-aggregate.js";
|
|
24
|
-
export const Phase4OutputSchema = z.object({
|
|
25
|
-
assessmentId: z.string(),
|
|
26
|
-
dossier: DossierBodySchema,
|
|
27
|
-
});
|
|
28
|
-
export { Phase3BundleSchema };
|
|
29
|
-
/**
|
|
30
|
-
* Run Phase 4 — assemble the dossier body from the Phase-3 bundle.
|
|
31
|
-
* Safely cached on DBOS replay; the caller supplies the annotator seam.
|
|
32
|
-
*/
|
|
33
|
-
export async function phase4Assemble(pool, phase3, annotatorDeps, logger) {
|
|
34
|
-
const dossier = await assembleDossier(pool, phase3.phase2, phase3, annotatorDeps, logger);
|
|
35
|
-
return {
|
|
36
|
-
assessmentId: phase3.phase2.phase1.resolved.assessmentId,
|
|
37
|
-
dossier,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 5 — stamps the four Phase-5 synthesis outputs into the Phase-4 dossier,
|
|
3
|
-
* runs the deterministic refinement passes (coverage-qualifier reconcile,
|
|
4
|
-
* no-liabilities disclosure rewrite, recommendation citation validation +
|
|
5
|
-
* demotion, recommendation quality gates), computes derived fields, and
|
|
6
|
-
* validates against `DossierSchema`.
|
|
7
|
-
*
|
|
8
|
-
* The DB write (`setDossier` / `markFailed`) belongs to the workflow's terminal
|
|
9
|
-
* handler; this module returns the validated dossier instead of writing it.
|
|
10
|
-
* Typed throws (`DossierSchemaViolationError`, `DossierDerivedInvariantError`)
|
|
11
|
-
* let that handler dispatch to `markFailed` with the matching `error.kind`.
|
|
12
|
-
*
|
|
13
|
-
* Every pass here is deterministic. The direction-of-effect audit
|
|
14
|
-
* (`auditCitationDirections`) is not among them because it needs an LLM call,
|
|
15
|
-
* and this module runs after synthesis has closed.
|
|
16
|
-
*/
|
|
17
|
-
import type { Logger } from "../../lib/logger.js";
|
|
18
|
-
import { type DossierBody } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
19
|
-
import type { OrganSignalProjection } from "./lib/fda-label-safety.js";
|
|
20
|
-
import type { Phase2Bundle } from "./steps/phase2-aggregate.js";
|
|
21
|
-
import type { DossierRecommendationStepOutput, LiabilityBulletsStepOutput, SafetyFlagsTrailStepOutput, TranslationalCommentaryStepOutput } from "./synthesis/index.js";
|
|
22
|
-
export declare class DossierSchemaViolationError extends Error {
|
|
23
|
-
readonly issues: readonly unknown[];
|
|
24
|
-
constructor(message: string, issues: readonly unknown[]);
|
|
25
|
-
}
|
|
26
|
-
export declare class DossierDerivedInvariantError extends Error {
|
|
27
|
-
readonly cause?: unknown | undefined;
|
|
28
|
-
constructor(message: string, cause?: unknown | undefined);
|
|
29
|
-
}
|
|
30
|
-
export interface Phase5PersistInput {
|
|
31
|
-
/** Operational logging seam; omitted falls back to no-op. */
|
|
32
|
-
readonly logger?: Logger;
|
|
33
|
-
readonly assessmentId: string;
|
|
34
|
-
readonly phase4Dossier: DossierBody;
|
|
35
|
-
readonly phase2: Phase2Bundle;
|
|
36
|
-
/**
|
|
37
|
-
* Per-organ signals segmented from the FDA labels the approval-precedent
|
|
38
|
-
* step fetched. Null when no indication resolved and no label was queried.
|
|
39
|
-
*/
|
|
40
|
-
readonly regulatoryOrganSignals: OrganSignalProjection | null;
|
|
41
|
-
readonly synthesis: {
|
|
42
|
-
readonly bullets: LiabilityBulletsStepOutput;
|
|
43
|
-
readonly flags: SafetyFlagsTrailStepOutput;
|
|
44
|
-
readonly commentary: TranslationalCommentaryStepOutput;
|
|
45
|
-
readonly recommendation: DossierRecommendationStepOutput;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
export interface Phase5PersistResult {
|
|
49
|
-
readonly assessmentId: string;
|
|
50
|
-
readonly dossier: Record<string, unknown>;
|
|
51
|
-
readonly bytes: number;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Run Phase-5 persist as a pure function. Returns the validated dossier (and
|
|
55
|
-
* computed bytes) on success. Throws `DossierDerivedInvariantError` when
|
|
56
|
-
* `computeDerivedFields` rejects the dossier; throws
|
|
57
|
-
* `DossierSchemaViolationError` when the schema rejects the final dossier.
|
|
58
|
-
*/
|
|
59
|
-
export declare function phase5Persist(input: Phase5PersistInput): Promise<Phase5PersistResult>;
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 5 — stamps the four Phase-5 synthesis outputs into the Phase-4 dossier,
|
|
3
|
-
* runs the deterministic refinement passes (coverage-qualifier reconcile,
|
|
4
|
-
* no-liabilities disclosure rewrite, recommendation citation validation +
|
|
5
|
-
* demotion, recommendation quality gates), computes derived fields, and
|
|
6
|
-
* validates against `DossierSchema`.
|
|
7
|
-
*
|
|
8
|
-
* The DB write (`setDossier` / `markFailed`) belongs to the workflow's terminal
|
|
9
|
-
* handler; this module returns the validated dossier instead of writing it.
|
|
10
|
-
* Typed throws (`DossierSchemaViolationError`, `DossierDerivedInvariantError`)
|
|
11
|
-
* let that handler dispatch to `markFailed` with the matching `error.kind`.
|
|
12
|
-
*
|
|
13
|
-
* Every pass here is deterministic. The direction-of-effect audit
|
|
14
|
-
* (`auditCitationDirections`) is not among them because it needs an LLM call,
|
|
15
|
-
* and this module runs after synthesis has closed.
|
|
16
|
-
*/
|
|
17
|
-
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
18
|
-
import { DossierSchema, } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
19
|
-
import { assembleRegulatoryOrganSignals, deterministicTranslationalCommentary } from "./assemblers/index.js";
|
|
20
|
-
import { validateRecommendationCitations } from "./lib/citation-validator.js";
|
|
21
|
-
import { computeDerivedFields } from "./lib/compute-derived.js";
|
|
22
|
-
import { detectStructuralEvidenceConflicts } from "./lib/evidence-conflict-detector.js";
|
|
23
|
-
import { applyRecommendationQualityGates } from "./lib/recommendation-quality-gates.js";
|
|
24
|
-
import { sanitizeRecommendation } from "./lib/recommendation-sanitizer.js";
|
|
25
|
-
const SOFT_CAP_BYTES = 16 * 1024 * 1024;
|
|
26
|
-
// ── Typed throws — handled in §6 terminal handler ────────────────────
|
|
27
|
-
export class DossierSchemaViolationError extends Error {
|
|
28
|
-
issues;
|
|
29
|
-
constructor(message, issues) {
|
|
30
|
-
super(message);
|
|
31
|
-
this.issues = issues;
|
|
32
|
-
this.name = "DossierSchemaViolationError";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export class DossierDerivedInvariantError extends Error {
|
|
36
|
-
cause;
|
|
37
|
-
constructor(message, cause) {
|
|
38
|
-
super(message);
|
|
39
|
-
this.cause = cause;
|
|
40
|
-
this.name = "DossierDerivedInvariantError";
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// ── Internal helpers ─────────────────────────────────────────────────
|
|
44
|
-
function approximateSize(d) {
|
|
45
|
-
return Buffer.byteLength(JSON.stringify(d), "utf-8");
|
|
46
|
-
}
|
|
47
|
-
function truncateOversize(d) {
|
|
48
|
-
const next = structuredClone(d);
|
|
49
|
-
const ta = next.reference_biology.therapeutic_area_associations;
|
|
50
|
-
if (ta.coverage === "available" && ta.data.rows.length > 50) {
|
|
51
|
-
ta.data.rows = ta.data.rows.slice(0, 50);
|
|
52
|
-
ta.truncated = true;
|
|
53
|
-
}
|
|
54
|
-
const ind = next.indications;
|
|
55
|
-
if (ind.coverage === "available" && ind.data.rows.length > 25) {
|
|
56
|
-
ind.data.rows = ind.data.rows.slice(0, 25);
|
|
57
|
-
ind.truncated = true;
|
|
58
|
-
}
|
|
59
|
-
const ppi = next.reference_biology.ppi_network;
|
|
60
|
-
if (ppi.coverage === "available" && ppi.data.partners.length > 50) {
|
|
61
|
-
ppi.data.partners = ppi.data.partners.slice(0, 50);
|
|
62
|
-
ppi.truncated = true;
|
|
63
|
-
}
|
|
64
|
-
return next;
|
|
65
|
-
}
|
|
66
|
-
function pickCommentarySeverity(row) {
|
|
67
|
-
const blob = `${row.predicate} ${row.commentary}`.toLowerCase();
|
|
68
|
-
if (row.topic === "ko_phenotype" && /pre[- ]?weaning|embryonic|lethal/i.test(blob)) {
|
|
69
|
-
return "caution";
|
|
70
|
-
}
|
|
71
|
-
if (row.topic === "expression_translation" && /(≥|>=|two[- ]?step|2[- ]?step|diverge|divergence|rank.*differ)/i.test(blob)) {
|
|
72
|
-
return "caution";
|
|
73
|
-
}
|
|
74
|
-
if (/queried.+(no data|no calls|empty|no tissue)/i.test(blob)) {
|
|
75
|
-
return "gap";
|
|
76
|
-
}
|
|
77
|
-
return "ok";
|
|
78
|
-
}
|
|
79
|
-
const CITED_PMID_RE = /\bPMID[:\s]?(\d{6,9})\b/gi;
|
|
80
|
-
/**
|
|
81
|
-
* A synthesised claim stands on the papers it cites. One that cites none says
|
|
82
|
-
* so — a section path is a pointer into this dossier, not a record a reader
|
|
83
|
-
* can independently check.
|
|
84
|
-
*/
|
|
85
|
-
function supportFromCitedPmids(text, reasonWhenUncited) {
|
|
86
|
-
const pmids = [...new Set([...text.matchAll(CITED_PMID_RE)].map((m) => m[1]))];
|
|
87
|
-
if (pmids.length === 0)
|
|
88
|
-
return { state: "unknown", reason: reasonWhenUncited };
|
|
89
|
-
return { state: "scored", evidence: pmids.map((pmid) => ({ pmid, source: "pubmed" })) };
|
|
90
|
-
}
|
|
91
|
-
function stampSynthesis(logger, phase4Dossier, syn, phase2, regulatoryOrganSignals) {
|
|
92
|
-
const next = structuredClone(phase4Dossier);
|
|
93
|
-
next.safety_profile.regulatory_organ_signals = assembleRegulatoryOrganSignals(regulatoryOrganSignals);
|
|
94
|
-
if (syn.bullets.coverage === "available") {
|
|
95
|
-
next.liability_summary.liability_bullets = syn.bullets.data.bullets.map((b) => ({
|
|
96
|
-
text: b.text,
|
|
97
|
-
rationale: b.rationale,
|
|
98
|
-
category: b.category,
|
|
99
|
-
support: supportFromCitedPmids(`${b.text} ${b.rationale} ${b.evidence_pointer}`, `bullet cites ${b.evidence_pointer} rather than a paper`),
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
if (syn.flags.coverage === "available" && syn.flags.data.flags.length > 0) {
|
|
103
|
-
next.safety_profile.target_organ_liabilities = {
|
|
104
|
-
coverage: "available",
|
|
105
|
-
data: {
|
|
106
|
-
rows: syn.flags.data.flags.map((f) => ({
|
|
107
|
-
organ: f.organ,
|
|
108
|
-
severity: f.severity,
|
|
109
|
-
trail: f.trail,
|
|
110
|
-
mechanism_hypothesis: f.mechanism_hypothesis,
|
|
111
|
-
support: supportFromCitedPmids(f.trail, "the audit trail is assembled from dossier sections and cites no paper"),
|
|
112
|
-
})),
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
else if (syn.flags.coverage === "queried_no_data") {
|
|
117
|
-
next.safety_profile.target_organ_liabilities = { coverage: "queried_no_data", error: syn.flags.error };
|
|
118
|
-
}
|
|
119
|
-
const deterministicAll = deterministicTranslationalCommentary(phase2);
|
|
120
|
-
const deterministicSorted = [...deterministicAll].sort((a, b) => {
|
|
121
|
-
const order = { caution: 0, gap: 1, ok: 2 };
|
|
122
|
-
return order[a.severity] - order[b.severity];
|
|
123
|
-
});
|
|
124
|
-
const deterministic = deterministicSorted.slice(0, 4);
|
|
125
|
-
const agentRows = syn.commentary.coverage === "available"
|
|
126
|
-
? syn.commentary.data.rows.slice(0, 4).map((r) => ({
|
|
127
|
-
severity: pickCommentarySeverity(r),
|
|
128
|
-
text: `${r.predicate}. ${r.commentary}`,
|
|
129
|
-
}))
|
|
130
|
-
: [];
|
|
131
|
-
const allRows = [...deterministic, ...agentRows].slice(0, 8);
|
|
132
|
-
if (allRows.length > 0) {
|
|
133
|
-
next.reference_biology.preclinical.translational_commentary = {
|
|
134
|
-
coverage: "available",
|
|
135
|
-
data: { rows: allRows },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
else if (syn.commentary.coverage === "queried_no_data") {
|
|
139
|
-
next.reference_biology.preclinical.translational_commentary = {
|
|
140
|
-
coverage: "queried_no_data",
|
|
141
|
-
error: syn.commentary.error,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
const recommendation = syn.recommendation.coverage === "available"
|
|
145
|
-
? {
|
|
146
|
-
coverage: "available",
|
|
147
|
-
data: sanitizeRecommendation(syn.recommendation.data),
|
|
148
|
-
}
|
|
149
|
-
: {
|
|
150
|
-
coverage: "queried_no_data",
|
|
151
|
-
error: syn.recommendation.error,
|
|
152
|
-
};
|
|
153
|
-
next.executive_recommendation = recommendation;
|
|
154
|
-
if (syn.flags.coverage !== "available" && next.executive_recommendation.coverage === "available") {
|
|
155
|
-
const qual = next.executive_recommendation.data.coverage_qualifier;
|
|
156
|
-
if (qual) {
|
|
157
|
-
if (!qual.sections_unavailable.includes("safety_profile.target_organ_liabilities")) {
|
|
158
|
-
qual.sections_unavailable.push("safety_profile.target_organ_liabilities");
|
|
159
|
-
}
|
|
160
|
-
const errorKind = syn.flags.error?.kind ?? "unavailable";
|
|
161
|
-
const annotation = `The deterministic safety-flag audit trail was not generated for this dossier (synthesis step \`safety-flags-trail\` failed with \`${errorKind}\`); the safety-bullet citations below are not anchored to a per-flag audit row.`;
|
|
162
|
-
qual.note = qual.note ? `${annotation} ${qual.note}` : annotation;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const diagnostics = [syn.bullets.diagnostic, syn.flags.diagnostic, syn.commentary.diagnostic, syn.recommendation.diagnostic];
|
|
166
|
-
next.analytics.synthesis_diagnostics = {
|
|
167
|
-
coverage: "available",
|
|
168
|
-
data: { rows: diagnostics },
|
|
169
|
-
};
|
|
170
|
-
if (syn.recommendation.coverage === "available") {
|
|
171
|
-
try {
|
|
172
|
-
const audit = validateRecommendationCitations(syn.recommendation.data, next);
|
|
173
|
-
if (audit.citations_unresolved.length > 0) {
|
|
174
|
-
logger.warn("recommendation_audit: unresolved citation(s)", {
|
|
175
|
-
unresolvedCount: audit.citations_unresolved.length,
|
|
176
|
-
unresolved: audit.citations_unresolved,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
next.analytics.recommendation_audit = {
|
|
180
|
-
coverage: "available",
|
|
181
|
-
data: audit,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
next.analytics.recommendation_audit = {
|
|
186
|
-
coverage: "queried_no_data",
|
|
187
|
-
error: { message: err instanceof Error ? err.message : String(err) },
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return next;
|
|
192
|
-
}
|
|
193
|
-
function applyCitationAuditDemotion(rec, audit) {
|
|
194
|
-
const flagged = audit.citations_unresolved.filter((u) => u.surface === "organ_claim_without_probe_pass");
|
|
195
|
-
if (flagged.length === 0)
|
|
196
|
-
return rec;
|
|
197
|
-
const flaggedExcerpts = flagged.map((f) => ("excerpt" in f ? f.excerpt : ""));
|
|
198
|
-
const keptRisks = [];
|
|
199
|
-
const demotedTexts = new Set();
|
|
200
|
-
for (const risk of rec.key_risks) {
|
|
201
|
-
const isFlagged = flaggedExcerpts.some((excerpt) => excerpt.length > 0 && risk.includes(excerpt.slice(0, Math.min(80, excerpt.length))));
|
|
202
|
-
if (isFlagged) {
|
|
203
|
-
demotedTexts.add(risk);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
keptRisks.push(risk);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const unverified = flagged.map((f) => ({
|
|
210
|
-
text: [...demotedTexts].find((t) => "excerpt" in f && t.includes(f.excerpt.slice(0, Math.min(80, f.excerpt.length)))) ?? ("excerpt" in f ? f.excerpt : ""),
|
|
211
|
-
bullet_category: f.bullet_category,
|
|
212
|
-
surface: "organ_claim_without_probe_pass",
|
|
213
|
-
}));
|
|
214
|
-
return {
|
|
215
|
-
...rec,
|
|
216
|
-
key_risks: keptRisks,
|
|
217
|
-
coverage_qualifier: {
|
|
218
|
-
...rec.coverage_qualifier,
|
|
219
|
-
unverified_bullets: unverified,
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
function rewriteNoLiabilitiesDisclosure(dossier) {
|
|
224
|
-
const ls = dossier.liability_summary;
|
|
225
|
-
if (!ls)
|
|
226
|
-
return;
|
|
227
|
-
const bullets = Array.isArray(ls.liability_bullets)
|
|
228
|
-
? ls.liability_bullets
|
|
229
|
-
: [];
|
|
230
|
-
const current = String(ls.no_liabilities_disclosure ?? "");
|
|
231
|
-
if (current.startsWith("insufficient safety data"))
|
|
232
|
-
return;
|
|
233
|
-
const m = current.match(/(\d+) of 4 safety data sources returned data/);
|
|
234
|
-
if (!m)
|
|
235
|
-
return;
|
|
236
|
-
const N = m[1];
|
|
237
|
-
if (bullets.length === 0) {
|
|
238
|
-
ls.no_liabilities_disclosure =
|
|
239
|
-
`${N} of 4 safety data sources returned data; no class-level liabilities synthesised`;
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
ls.no_liabilities_disclosure =
|
|
243
|
-
`${bullets.length} liabilities identified across ${N} of 4 safety data sources`;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
function resolveSection(dossier, path) {
|
|
247
|
-
let cur = dossier;
|
|
248
|
-
for (const part of path.split(".")) {
|
|
249
|
-
if (cur == null || typeof cur !== "object")
|
|
250
|
-
return { resolved: false };
|
|
251
|
-
cur = cur[part];
|
|
252
|
-
}
|
|
253
|
-
if (cur === undefined)
|
|
254
|
-
return { resolved: false };
|
|
255
|
-
const coverage = cur && typeof cur === "object" && typeof cur.coverage === "string" ? cur.coverage : undefined;
|
|
256
|
-
return { resolved: true, coverage };
|
|
257
|
-
}
|
|
258
|
-
function reconcileCoverageQualifier(dossier) {
|
|
259
|
-
const cq = dossier.executive_recommendation
|
|
260
|
-
?.data?.coverage_qualifier;
|
|
261
|
-
if (!cq || !Array.isArray(cq.sections_unavailable))
|
|
262
|
-
return;
|
|
263
|
-
cq.sections_unavailable = cq.sections_unavailable.filter((entry) => {
|
|
264
|
-
const path = String(entry).split(" (")[0].trim();
|
|
265
|
-
const { resolved, coverage } = resolveSection(dossier, path);
|
|
266
|
-
if (!resolved)
|
|
267
|
-
return false;
|
|
268
|
-
return coverage !== "available";
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
// ── Pure-function entry point ────────────────────────────────────────
|
|
272
|
-
/**
|
|
273
|
-
* Run Phase-5 persist as a pure function. Returns the validated dossier (and
|
|
274
|
-
* computed bytes) on success. Throws `DossierDerivedInvariantError` when
|
|
275
|
-
* `computeDerivedFields` rejects the dossier; throws
|
|
276
|
-
* `DossierSchemaViolationError` when the schema rejects the final dossier.
|
|
277
|
-
*/
|
|
278
|
-
export async function phase5Persist(input) {
|
|
279
|
-
const logger = (input.logger ?? createNoopLogger()).named("phase5-persist").with({ assessmentId: input.assessmentId });
|
|
280
|
-
// The dossier rides through this function as an untyped working blob; the
|
|
281
|
-
// `as unknown as DossierBody` casts below let the typed helpers operate on
|
|
282
|
-
// it. None of these casts is load-bearing for soundness:
|
|
283
|
-
// `DossierSchema.safeParse(fullDossier)` at the end is the single
|
|
284
|
-
// validation gate, and it throws before anything is returned.
|
|
285
|
-
let dossier = stampSynthesis(logger, input.phase4Dossier, input.synthesis, input.phase2, input.regulatoryOrganSignals);
|
|
286
|
-
// Demote organ-claim flagged bullets into coverage_qualifier.unverified_bullets.
|
|
287
|
-
const exec = dossier.executive_recommendation;
|
|
288
|
-
if (exec?.coverage === "available") {
|
|
289
|
-
const audit = dossier.analytics.recommendation_audit?.data;
|
|
290
|
-
if (audit && exec.data) {
|
|
291
|
-
dossier.executive_recommendation.data = applyCitationAuditDemotion(exec.data, audit);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
reconcileCoverageQualifier(dossier);
|
|
295
|
-
rewriteNoLiabilitiesDisclosure(dossier);
|
|
296
|
-
const recommendationAudit = dossier.analytics.recommendation_audit?.data;
|
|
297
|
-
const synthesisDiagnostics = dossier.analytics.synthesis_diagnostics?.coverage ===
|
|
298
|
-
"available"
|
|
299
|
-
? dossier.analytics.synthesis_diagnostics.data.rows
|
|
300
|
-
: [];
|
|
301
|
-
const trials = dossier.clinical_development
|
|
302
|
-
.trials;
|
|
303
|
-
const hasDirectClinicalEvidence = trials?.coverage === "available" && (trials.data.rows ?? []).some((row) => row.eligible_for_toxicology_aggregation === true);
|
|
304
|
-
const gated = applyRecommendationQualityGates({
|
|
305
|
-
executive_recommendation: dossier.executive_recommendation,
|
|
306
|
-
synthesis_diagnostics: synthesisDiagnostics,
|
|
307
|
-
recommendation_audit: recommendationAudit,
|
|
308
|
-
has_direct_clinical_evidence: hasDirectClinicalEvidence,
|
|
309
|
-
});
|
|
310
|
-
dossier.executive_recommendation = gated.executive_recommendation;
|
|
311
|
-
const qg = dossier.analytics.quality_gates;
|
|
312
|
-
dossier.analytics.quality_gates = {
|
|
313
|
-
coverage: "available",
|
|
314
|
-
data: {
|
|
315
|
-
rows: [...(qg?.data?.rows ?? []), ...gated.quality_gates],
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
let bytes = approximateSize(dossier);
|
|
319
|
-
if (bytes > SOFT_CAP_BYTES) {
|
|
320
|
-
dossier = truncateOversize(dossier);
|
|
321
|
-
bytes = approximateSize(dossier);
|
|
322
|
-
}
|
|
323
|
-
let derived;
|
|
324
|
-
try {
|
|
325
|
-
derived = computeDerivedFields(dossier);
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
throw new DossierDerivedInvariantError(err instanceof Error ? err.message : String(err), err);
|
|
329
|
-
}
|
|
330
|
-
const fullDossier = { ...dossier, derived };
|
|
331
|
-
const structuralConflicts = detectStructuralEvidenceConflicts(fullDossier);
|
|
332
|
-
if (structuralConflicts.length > 0) {
|
|
333
|
-
const existing = fullDossier.analytics.evidence_conflicts;
|
|
334
|
-
const existingRows = existing?.coverage === "available" ? (existing.data?.rows ?? []) : [];
|
|
335
|
-
fullDossier.analytics.evidence_conflicts = {
|
|
336
|
-
coverage: "available",
|
|
337
|
-
data: { rows: [...existingRows, ...structuralConflicts] },
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
const parsed = DossierSchema.safeParse(fullDossier);
|
|
341
|
-
if (!parsed.success) {
|
|
342
|
-
throw new DossierSchemaViolationError("Dossier failed schema validation", parsed.error.issues);
|
|
343
|
-
}
|
|
344
|
-
return {
|
|
345
|
-
assessmentId: input.assessmentId,
|
|
346
|
-
dossier: parsed.data,
|
|
347
|
-
bytes,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TargetAssessmentProgressStream — the read side of a target assessment's
|
|
3
|
-
* durable progress stream.
|
|
4
|
-
*
|
|
5
|
-
* `emitProgress` (the sibling `progress.ts`) writes one part per phase
|
|
6
|
-
* transition and nothing reads it back, so an embedder that wants to show a
|
|
7
|
-
* user how far an assessment has got had to reach for the durability engine
|
|
8
|
-
* itself. This seam is that read, and it quarantines the engine the way
|
|
9
|
-
* `RunLauncher` does for starting workflows: the delivered value is the
|
|
10
|
-
* harness's own `TargetAssessmentProgressEvent` contract type, and no engine
|
|
11
|
-
* type appears in the exported signature.
|
|
12
|
-
*
|
|
13
|
-
* It is far simpler than `execution/run-event-stream.ts`, and deliberately so.
|
|
14
|
-
* A run fans out over a parent workflow and every sandbox-step child, each
|
|
15
|
-
* writing its own stream, so that seam polls a ledger to discover children and
|
|
16
|
-
* folds reconciling parts before delivery. A target assessment writes progress
|
|
17
|
-
* from the parent body alone, under `workflowID === assessmentId`. One stream,
|
|
18
|
-
* no discovery, no pool.
|
|
19
|
-
*
|
|
20
|
-
* It is a push subscription rather than a returned iterator because handing the
|
|
21
|
-
* engine's async generator back to a caller would put the engine's model on the
|
|
22
|
-
* very surface this seam exists to remove, and would leave nowhere to contain a
|
|
23
|
-
* failure. A handler gives both, and matches the `EmitFn` idiom the writer side
|
|
24
|
-
* already speaks.
|
|
25
|
-
*/
|
|
26
|
-
import { type TargetAssessmentProgressEvent } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
27
|
-
import type { Logger } from "../../lib/logger.js";
|
|
28
|
-
/**
|
|
29
|
-
* Construction-time dependencies. The logger is where every contained failure
|
|
30
|
-
* is reported, and defaults to silence so an embedder that wired none sees
|
|
31
|
-
* nothing rather than having console forced on it.
|
|
32
|
-
*/
|
|
33
|
-
export interface TargetAssessmentProgressStreamDeps {
|
|
34
|
-
readonly logger?: Logger;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Receives one progress event. May be async — the subscription awaits it, so a
|
|
38
|
-
* handler is never invoked concurrently with itself. A throw is logged and
|
|
39
|
-
* swallowed.
|
|
40
|
-
*/
|
|
41
|
-
export type TargetAssessmentProgressHandler = (event: TargetAssessmentProgressEvent) => void | Promise<void>;
|
|
42
|
-
/** Call-time parameters of one subscription. */
|
|
43
|
-
export interface TargetAssessmentProgressSubscribeOptions {
|
|
44
|
-
/** The assessment to observe. Also the workflow's id — TA sets the two equal. */
|
|
45
|
-
readonly assessmentId: string;
|
|
46
|
-
/** Where each delivered event goes. */
|
|
47
|
-
readonly onEvent: TargetAssessmentProgressHandler;
|
|
48
|
-
/** Aborting stops delivery and settles the returned promise. */
|
|
49
|
-
readonly signal: AbortSignal;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* The progress read seam. One method, because an assessment-scoped subscription
|
|
53
|
-
* is the whole capability — a general-purpose stream reader would leak the
|
|
54
|
-
* engine's model back into the surface this exists to quarantine.
|
|
55
|
-
*/
|
|
56
|
-
export interface TargetAssessmentProgressStream {
|
|
57
|
-
/**
|
|
58
|
-
* Deliver every progress event the assessment produces to `onEvent`,
|
|
59
|
-
* resolving when the assessment's stream drains or when the signal aborts.
|
|
60
|
-
*
|
|
61
|
-
* The stream drains when the workflow stops being active. A workflow that
|
|
62
|
-
* self-cancels on a 402 is no longer active, so the stream drains at the
|
|
63
|
-
* `suspended` phase and this settles there — it does NOT wait for a later
|
|
64
|
-
* `DBOS.resumeWorkflow`. Re-opening across a resume would mean polling the
|
|
65
|
-
* row's status, which is the caller's business, not this seam's.
|
|
66
|
-
*
|
|
67
|
-
* Events arrive in the order the workflow wrote them, each exactly once.
|
|
68
|
-
* There is no fold: the body emits each phase once and the engine caches a
|
|
69
|
-
* step's stream-write offset, so a recovered workflow does not re-write one.
|
|
70
|
-
* A fold would collapse nothing and cost a buffer.
|
|
71
|
-
*/
|
|
72
|
-
subscribe(options: TargetAssessmentProgressSubscribeOptions): Promise<void>;
|
|
73
|
-
}
|
|
74
|
-
/** Build the DBOS-backed realization of {@link TargetAssessmentProgressStream}. */
|
|
75
|
-
export declare function createTargetAssessmentProgressStream(deps?: TargetAssessmentProgressStreamDeps): TargetAssessmentProgressStream;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TargetAssessmentProgressStream — the read side of a target assessment's
|
|
3
|
-
* durable progress stream.
|
|
4
|
-
*
|
|
5
|
-
* `emitProgress` (the sibling `progress.ts`) writes one part per phase
|
|
6
|
-
* transition and nothing reads it back, so an embedder that wants to show a
|
|
7
|
-
* user how far an assessment has got had to reach for the durability engine
|
|
8
|
-
* itself. This seam is that read, and it quarantines the engine the way
|
|
9
|
-
* `RunLauncher` does for starting workflows: the delivered value is the
|
|
10
|
-
* harness's own `TargetAssessmentProgressEvent` contract type, and no engine
|
|
11
|
-
* type appears in the exported signature.
|
|
12
|
-
*
|
|
13
|
-
* It is far simpler than `execution/run-event-stream.ts`, and deliberately so.
|
|
14
|
-
* A run fans out over a parent workflow and every sandbox-step child, each
|
|
15
|
-
* writing its own stream, so that seam polls a ledger to discover children and
|
|
16
|
-
* folds reconciling parts before delivery. A target assessment writes progress
|
|
17
|
-
* from the parent body alone, under `workflowID === assessmentId`. One stream,
|
|
18
|
-
* no discovery, no pool.
|
|
19
|
-
*
|
|
20
|
-
* It is a push subscription rather than a returned iterator because handing the
|
|
21
|
-
* engine's async generator back to a caller would put the engine's model on the
|
|
22
|
-
* very surface this seam exists to remove, and would leave nowhere to contain a
|
|
23
|
-
* failure. A handler gives both, and matches the `EmitFn` idiom the writer side
|
|
24
|
-
* already speaks.
|
|
25
|
-
*/
|
|
26
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
27
|
-
import { TargetAssessmentProgressEventSchema } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
28
|
-
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
29
|
-
import { TA_PROGRESS_STREAM_KEY } from "./progress.js";
|
|
30
|
-
/**
|
|
31
|
-
* Shape `emitProgress` writes. The `data-target-assessment-progress` key is not
|
|
32
|
-
* in the chat-part registry — it is the writer's private envelope — so the
|
|
33
|
-
* payload, not the envelope, is what a caller gets.
|
|
34
|
-
*/
|
|
35
|
-
function readEvent(value) {
|
|
36
|
-
if (typeof value !== "object" || value === null)
|
|
37
|
-
return null;
|
|
38
|
-
const parsed = TargetAssessmentProgressEventSchema.safeParse(value.payload);
|
|
39
|
-
return parsed.success ? parsed.data : null;
|
|
40
|
-
}
|
|
41
|
-
/** Build the DBOS-backed realization of {@link TargetAssessmentProgressStream}. */
|
|
42
|
-
export function createTargetAssessmentProgressStream(deps = {}) {
|
|
43
|
-
const baseLogger = (deps.logger ?? createNoopLogger()).named("ta-progress-stream");
|
|
44
|
-
return {
|
|
45
|
-
async subscribe({ assessmentId, onEvent, signal }) {
|
|
46
|
-
const logger = baseLogger.with({ assessmentId });
|
|
47
|
-
let resolveAbort;
|
|
48
|
-
const aborted = new Promise((resolve) => {
|
|
49
|
-
resolveAbort = resolve;
|
|
50
|
-
});
|
|
51
|
-
const generator = DBOS.readStream(assessmentId, TA_PROGRESS_STREAM_KEY);
|
|
52
|
-
// The engine's reader exposes no cancellation: while it waits on a
|
|
53
|
-
// quiet-but-active workflow it is suspended inside an await, where a
|
|
54
|
-
// queued `return()` is only honoured once it next reaches a yield.
|
|
55
|
-
// Teardown is therefore best-effort by construction, and the read's
|
|
56
|
-
// own promise is raced against the signal rather than waiting on it.
|
|
57
|
-
const onAbort = () => {
|
|
58
|
-
resolveAbort();
|
|
59
|
-
void generator.return(undefined).catch(() => {
|
|
60
|
-
/* the read is already being abandoned */
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
if (signal.aborted)
|
|
64
|
-
onAbort();
|
|
65
|
-
else
|
|
66
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
67
|
-
const read = (async () => {
|
|
68
|
-
try {
|
|
69
|
-
for (;;) {
|
|
70
|
-
const next = await generator.next();
|
|
71
|
-
// Checked after the read, not before: `next()` is what
|
|
72
|
-
// notices the workflow going inactive, and an aborted
|
|
73
|
-
// subscription must deliver nothing further.
|
|
74
|
-
if (next.done === true || signal.aborted)
|
|
75
|
-
return;
|
|
76
|
-
const event = readEvent(next.value);
|
|
77
|
-
if (event === null) {
|
|
78
|
-
logger.debug("dropped a stream value outside the progress contract");
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
try {
|
|
82
|
-
await onEvent(event);
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
logger.warn("progress handler threw", { phase: event.phase, ...logger.errorFields(err) });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
// Observation is a diagnostic channel: `emitProgress` already
|
|
91
|
-
// treats a failed write as best-effort so a lost frame cannot
|
|
92
|
-
// fail the workflow. A read that propagated here would be
|
|
93
|
-
// strictly less robust than the writer it observes.
|
|
94
|
-
logger.error("progress stream read failed", logger.errorFields(err));
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
void generator.return(undefined).catch(() => {
|
|
98
|
-
/* already finished, or abandoned under abort */
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
})();
|
|
102
|
-
try {
|
|
103
|
-
await Promise.race([read, aborted]);
|
|
104
|
-
}
|
|
105
|
-
finally {
|
|
106
|
-
signal.removeEventListener("abort", onAbort);
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progress emission for the target-assessment DBOS workflow.
|
|
3
|
-
*
|
|
4
|
-
* Emits a typed `data-target-assessment-progress` part via
|
|
5
|
-
* `DBOS.writeStream("progress", part)` and updates `cortex_target_assessments.progress`.
|
|
6
|
-
* Both writes happen inside a `DBOS.runStep` so a recovered workflow does not
|
|
7
|
-
* duplicate the event.
|
|
8
|
-
*
|
|
9
|
-
* Wire shape: `TargetAssessmentProgressEventSchema` in
|
|
10
|
-
* `harness/src/contracts/target-dossier.ts`. The SSE route reads via
|
|
11
|
-
* `DBOS.readStream(workflowId, "progress")` and folds-on-read by `phase`.
|
|
12
|
-
*/
|
|
13
|
-
import type { Pool } from "pg";
|
|
14
|
-
import type { TargetAssessmentPhase, TargetAssessmentProgressEvent } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
15
|
-
import type { Logger } from "../../lib/logger.js";
|
|
16
|
-
/** DBOS stream key the SSE route reads via `DBOS.readStream(wfId, "progress")`. */
|
|
17
|
-
export declare const TA_PROGRESS_STREAM_KEY = "progress";
|
|
18
|
-
export type ProgressPhase = TargetAssessmentPhase;
|
|
19
|
-
/**
|
|
20
|
-
* Wire shape of one progress event. Matches the harness's contracts
|
|
21
|
-
* `TargetAssessmentProgressEventSchema`. Wrapped in a typed chat part
|
|
22
|
-
* envelope by the SSE route on the way out.
|
|
23
|
-
*/
|
|
24
|
-
export interface ProgressPart {
|
|
25
|
-
readonly type: "data-target-assessment-progress";
|
|
26
|
-
readonly payload: TargetAssessmentProgressEvent;
|
|
27
|
-
}
|
|
28
|
-
export declare const PROGRESS_PERCENTS: Record<ProgressPhase, number>;
|
|
29
|
-
export declare const PROGRESS_MESSAGES: Record<ProgressPhase, string>;
|
|
30
|
-
/**
|
|
31
|
-
* Emit one progress transition. MUST be called from inside the workflow
|
|
32
|
-
* body — the `DBOS.runStep` wrapper makes it replay-safe (DBOS persists
|
|
33
|
-
* the stream-write offset under `(workflowID, function_id)`; replay
|
|
34
|
-
* returns cached without re-writing the part).
|
|
35
|
-
*
|
|
36
|
-
* Best-effort: a DB or stream failure logs and continues — progress
|
|
37
|
-
* emission MUST NOT block the workflow.
|
|
38
|
-
*/
|
|
39
|
-
export declare function emitProgress(pool: Pool, logger: Logger, assessmentId: string, phase: ProgressPhase): Promise<void>;
|